๐งฑ Hardening That Is Worth Doing
Hardening Checklists That Are Actually Worth Doing
Most published hardening guides run to hundreds of controls, which is precisely why they do not get implemented. What follows is the short version: the items that remove whole classes of attack, the reasoning for each, and the tool here that verifies it afterwards. Verification is the part people skip โ an untested control is a belief, not a defence.
1 ยท Linux host
SSH: keys only
Internet-facing SSH is under constant automated password attack. Key-only authentication ends that category outright โ no password to guess, spray or reuse. Put overrides in a drop-in file so package upgrades do not clobber them:
# /etc/ssh/sshd_config.d/99-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
MaxAuthTries 3
LoginGraceTime 20
AllowGroups ssh-users
X11Forwarding no
# Validate BEFORE restarting, and keep your current session open:
# sshd -t && systemctl reload ssh # unit is "sshd" on RHEL-family
PermitRootLogin no forces attribution: people log in as themselves and escalate, so auth.log names a human. AllowGroups is an allow-list, which fails safe when somebody adds a service account later. On older releases the keyboard-interactive setting is named ChallengeResponseAuthentication.
Patching, firewall, surface
- Unattended security updates. Debian/Ubuntu:
unattended-upgradeswithUnattended-Upgrade::Allowed-Originsrestricted to the security pocket. RHEL family:dnf-automaticwithapply_updates = yes. Addneedrestartor a reboot window โ a patched library is still exploitable inside a process that never restarted. - Default-deny inbound. Everything not explicitly allowed is dropped:
ufw default deny incoming ufw default allow outgoing ufw limit 22/tcp # rate-limits repeated connections from one source ufw allow 80,443/tcp ufw enable - Fewer packages, fewer listeners.
ss -tulpnlists every listening socket with its process. Anything you cannot justify gets removed or bound to127.0.0.1. Databases, caches and admin interfaces should never be listening on a public address. - Least-privilege service accounts. One unprivileged system user per service,
--shell /usr/sbin/nologin, plus systemd sandboxing:
A remote-code-execution bug in the app then lands in a sandbox with a read-only filesystem and no capabilities.[Service] User=appsvc NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes ReadWritePaths=/var/lib/myapp CapabilityBoundingSet= SystemCallFilter=@system-service - Sudo policy. No blanket
ALL=(ALL) NOPASSWD: ALLfor humans. Scope entries to commands in/etc/sudoers.d/and validate withvisudo -c -f. Many "harmless" commands grant a shell โ an editor, a pager orfind -exechanded out via sudo is equivalent to full root. - Mount options.
noexec,nosuid,nodevon/tmp,/var/tmpand/dev/shmstops the laziest payload pattern (drop to /tmp, chmod, run). Some installers legitimately execute from/tmp, so test first. - auditd for the events you will want during an investigation:
-w /etc/passwd -p wa -k identity -w /etc/sudoers -p wa -k scope -w /etc/ssh/sshd_config -p wa -k sshdconfig -a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=4294967295 -k exec - Rate limiting.
fail2banor the firewall's own limiting trims brute-force noise. Be honest about what it is: with key-only SSH it is mostly log hygiene, not a control. - Time sync โ chrony or
systemd-timesyncd, servers in UTC. See the detection module for why. - Backups that are tested and offline. Ransomware deliberately destroys backups (ATT&CK T1490 Inhibit System Recovery), so at least one copy must be offline or immutable, and the backup credentials must not be reachable from the systems being backed up. A backup you have never restored is a hypothesis: schedule a real restore and time it.
Verify: port scan โ the host from outside and compare against the services you intended to expose โ this is the single fastest way to find a listener you forgot. TCP latency check โ distinguishes filtered from slow.
2 ยท Web server and application
TLS
RFC 8996 (2021) formally deprecates TLS 1.0 and 1.1. Floor at TLS 1.2, prefer 1.3, and use only AEAD suites with ephemeral key exchange so a future compromise of the server key cannot decrypt recorded traffic:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# one directive, one line โ nginx rejects a wrapped argument list
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Notes that matter: TLS 1.3 suites are not set by ssl_ciphers in nginx โ they negotiate separately and are fine at defaults. max-age=31536000 is one year; add preload only when every subdomain is HTTPS, because removal from the preload list is slow. Drop RC4 (prohibited by RFC 7465), 3DES, and any export, NULL or anonymous suite.
Response header baseline
| Header | Value | What it stops |
|---|---|---|
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Downgrade to HTTP and SSL-stripping on later visits |
| Content-Security-Policy | Start with default-src 'self'; add sources deliberately | The impact of an XSS โ the biggest single win, and the most work |
| X-Content-Type-Options | nosniff | MIME sniffing turning an upload into executable script |
| Content-Security-Policy: frame-ancestors | frame-ancestors 'none' (or X-Frame-Options: DENY for old clients) | Clickjacking |
| Referrer-Policy | strict-origin-when-cross-origin | Leaking paths and query strings (tokens!) to third parties |
| Permissions-Policy | geolocation=(), camera=(), microphone=() | Embedded content reaching for device APIs |
| Cross-Origin-Opener-Policy | same-origin | Cross-window references from other origins |
X-XSS-Protection is obsolete; modern browsers ignore it or it is set to 0. Do not add it as a "score booster".
The rest of the web checklist
- Banners.
server_tokens off;(nginx),ServerTokens Prod+ServerSignature Off(Apache),expose_php = Off, and stripX-Powered-By. This is obscurity, not security โ it will not stop a deliberate fingerprint, but it drops you out of mass version-matching scans for one line of config. - Directory listing off:
autoindex off;(nginx) /Options -Indexes(Apache). - Methods. Allow only what the application uses โ typically GET, HEAD, POST, plus PUT/PATCH/DELETE for APIs. Apache:
TraceEnable off. - Keep
.git,.envand backups out of the webroot. An exposed/.git/directory usually means the whole source history, including secrets that were "removed" in a later commit. Deploy build artefacts, not working copies. Belt and braces:
A naive# nginx โ deny dotfiles but keep /.well-known/ working (ACME, security.txt) location ~ ^/\.(?!well-known/) { deny all; return 404; }location ~ /\.also blocks/.well-known/and will break certificate renewal. - Dependencies. Lockfiles committed, automated update PRs, and a scheduled audit (
npm audit,pip-audit,composer audit). Most risk is transitive, several levels down from anything you chose. - Secrets in a manager, not the repo. If a secret was ever committed, git history still has it โ rotate it, do not just delete the line. Run a scanner such as gitleaks in CI to stop the next one.
- Rate limiting on authentication, password reset and anything expensive:
http { limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; } # http context location /login { limit_req zone=login burst=5 nodelay; } # server context
Verify: grade security headers โ, analyse the CSP โ, audit cookie flags โ, probe allowed methods โ, test CORS โ, inspect negotiated cipher suites โ, check the certificate โ and check for exposed paths โ (authorised targets only).
3 ยท DNS and email
Domain control is authentication for everything else โ mail, certificates, password resets. Losing it is worse than losing a server.
- Registrar lock and MFA. Confirm the EPP status codes
clientTransferProhibited,clientUpdateProhibitedandclientDeleteProhibited, enable multi-factor on the registrar and DNS provider, and recover through a mailbox not hosted on the domain itself. Keep auto-renew on; expiry is a self-inflicted outage. - CAA (RFC 8659) restricts which CAs may issue for the domain:
example.com. IN CAA 0 issue "letsencrypt.org" example.com. IN CAA 0 iodef "mailto:security@example.com" - DNSSEC signs your zone so a validating resolver can detect tampering. Be honest about the operational cost: a botched key rollover or an expired signature takes the domain off the internet for validating resolvers. Automate it, monitor it, or do not deploy it.
- SPF โ one TXT record, and mind the ten-DNS-lookup limit in RFC 7208 ยง4.6.4; exceeding it is a permanent error and receivers may treat the whole record as invalid:
v=spf1 include:_spf.example.net -all.-allis a hard fail,~alla soft fail. - DKIM โ 2048-bit RSA keys, a per-provider selector, and a rotation plan. The key lives at
selector._domainkey.example.com. - DMARC at enforcement. Start at
p=none, read the aggregate reports until every legitimate sender aligns, then move toquarantineand finally:
A domain that stops at_dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=s; aspf=s"p=noneis publishing telemetry, not protection. - MTA-STS (RFC 8461) stops a downgrade attack on inbound mail โ a DNS record points to a policy served over HTTPS:
Pair it with TLS-RPT (RFC 8460):_mta-sts.example.com. IN TXT "v=STSv1; id=20260726T000000;" # https://mta-sts.example.com/.well-known/mta-sts.txt version: STSv1 mode: enforce mx: mx1.example.com mx: mx2.example.com max_age: 604800_smtp._tls.example.com. IN TXT "v=TLSRPTv1; rua=mailto:tlsrpt@example.com". - Domains that do not send or receive mail โ parked domains, brand defensives and typo catchers โ should say so explicitly, or spoofers will speak for them:
v=spf1 -all, a DMARC record atp=reject, and, where the domain also accepts no mail, a null MX (RFC 7505):example.com. IN MX 0 ..
Verify: full DNS audit โ, DNSSEC validation โ, SPF/DKIM/DMARC check โ, MX, STARTTLS and MTA-STS โ, and build records with the SPF builder โ and DMARC builder โ. If you run outbound mail, check the sending address against blocklists โ too.
4 ยท Cloud
- No long-lived credentials. Instance roles or workload identity for compute; OIDC federation for CI/CD, so pipelines receive short-lived tokens instead of a static key pair sitting in a repository setting. Where static keys are unavoidable, scope them narrowly and rotate them on a schedule you enforce.
- IMDSv2 required. The instance metadata service hands out role credentials, making it the prize at the end of every server-side request forgery. Session-oriented IMDSv2 with a hop limit of 1 blocks the classic "make the app fetch
169.254.169.254" path:aws ec2 modify-instance-metadata-options \ --instance-id i-0123456789abcdef0 \ --http-tokens required \ --http-put-response-hop-limit 1 \ --http-endpoint enabled - Least-privilege IAM. No
"Action": "*"on"Resource": "*"outside break-glass. Grant per-service, per-resource; constrain with permission boundaries or service control policies; review unused permissions periodically and remove them. - Storage: encrypted, private by default. Account-level public-access blocking, default encryption, versioning on anything that matters, and a periodic audit for objects made public by exception.
- Audit logging on, and out of reach. CloudTrail across all regions with log file validation, Azure activity and sign-in logs, Google Cloud Audit Logs โ delivered to a separate account or project so that compromising production does not include deleting the record of it.
- Network segmentation. Application and database tiers in private subnets, no
0.0.0.0/0on 22 or 3389, session-manager style access instead of open bastions, and egress filtering so a compromised workload cannot freely call out.
Verify: cloud posture needs provider-native tooling, because those checks live in the control plane rather than on the wire. What this site confirms is the internet-facing result: what is reachable โ, what the edge returns โ, whether a WAF or CDN is in front โ, and whether certificates are about to expire โ.
- Key-only SSH, automatic security updates and a default-deny firewall remove more real risk than any other three host controls.
- A TLS 1.2 floor with ECDHE + AEAD suites plus the header baseline covers most transport and browser-side work. CSP is the expensive one, and the one worth doing.
.gitin the webroot leaks the entire source history โ deploy artefacts, and exclude dotfiles without breaking/.well-known/.- Domain control is the root of trust: registrar lock, MFA, CAA, DMARC at
p=reject. Domains that send no mail must say so. - In cloud: short-lived credentials, IMDSv2, and audit logs stored where an attacker in production cannot delete them.
- A control you have not verified from outside is a belief. Every block above names the tool that checks it.