SSH hardening checklist for internet-facing Linux servers
An ordered checklist for a Linux server that has to accept SSH from the internet — what to do first, what most lists get wrong, and what you can safely leave until later.
How to use this list
Hardening checklists usually fail for one reason: they are ordered by topic rather than by risk, so people work down them and run out of time somewhere around banner configuration, having never got to authentication.
This one is ordered. Section 1 removes the way Linux servers actually get compromised. If you only have an hour, do section 1 and stop — it is worth more than everything below it combined.
It assumes a server that must accept SSH from arbitrary addresses. If yours does not, section 3 collapses into a single firewall rule and you should read that as good news.
1. Authentication
Compromises begin with valid credentials far more often than with an unpatched daemon. This section is the whole game.
- Disable password authentication entirely: PasswordAuthentication no. This is the single highest-value line in this document — with no password to guess, guessing attacks cannot succeed at all.
- Also set KbdInteractiveAuthentication no. Miss it and PAM still offers a password prompt, so passwords keep working and you have changed nothing. Verify with sshd -T, not by reading the file.
- Set PermitRootLogin to prohibit-password, or no if root never connects directly. Root is the first name on every attack list.
- Protect private keys with a passphrase and keep them off shared machines. A leaked key is exactly as bad as a leaked password, and it usually leaks the same ways: a stolen laptop, an accidental commit, a CI runner's environment.
- Rotate keys when someone leaves, and audit ~/.ssh/authorized_keys on every account. Old keys accumulate silently and nobody ever removes them.
- Set AllowUsers or AllowGroups to an explicit list. Even with keys-only, this stops a newly created service account from being remotely reachable by accident.
2. Reduce what is reachable
The second question after "who can log in" is "what else is listening". Every open port is a service somebody is testing right now.
- Inventory what actually listens: ss -tlnp on the host, then verify from another network which of those answer. The two lists are rarely the same.
- Bind services to localhost where they do not need to be remote — databases especially. A PostgreSQL or Redis on 0.0.0.0 is a far bigger problem than SSH ever was.
- Run a default-deny firewall. nftables or the distribution's front end, permitting only what you named.
- Restrict SSH source addresses if you can. If the legitimate set is small and stable, this is the cheapest real control available — and it stops working the moment somebody travels.
- Consider moving off port 22 for noise reduction, understanding that it is noise reduction and not protection.
3. Automatic blocking of failed logins
Keys-only removes the risk of a successful guess. It does not remove the traffic, and this section is about the traffic.
Bots do not know your configuration. They keep connecting, keep offering passwords, keep being rejected — each attempt costing a TCP connection, an sshd fork, a log line and a slice of CPU. Left alone, an exposed server absorbs thousands a day and the auth log rotates fast enough to lose the events you actually needed.
fail2ban is the standard answer and a reasonable starting point. Configure it properly — bantime -1, ignoreip with your own addresses, and monitoring on the ban count rather than the service state, because its failure mode is silence. Its structural limits are single-address bans against range-rotating botnets, state that does not survive a rebuild, and no shared knowledge between your servers.
SSH Protector is the same job as a managed agent: subnet bans, permanent and restored on boot, the real SSH port detected from sshd's configuration, your address whitelisted at install, shared reputation across every protected server, and a console that answers whether it is still running.
4. sshd configuration details
Worth setting, individually minor, collectively a smaller attack surface. Run sshd -t after each change and reload rather than restart.
- MaxAuthTries 3 — fewer credential attempts per connection, so a bot needs more connections for the same number of guesses.
- MaxSessions and MaxStartups tuned down — limits how many unauthenticated connections can be in flight at once, which is what a flood actually consumes.
- LoginGraceTime 30 — an unauthenticated connection should not sit open for two minutes.
- X11Forwarding no and AllowAgentForwarding no unless something genuinely needs them. Agent forwarding in particular lets a compromised destination use your key.
- Modern algorithms only: drop legacy ciphers, MACs and key exchange methods. Current distributions default sensibly, so check before you copy a hardening snippet written a decade ago — several popular ones now break more than they fix.
5. Logging and detection
You cannot investigate what you did not record, and the default configuration keeps less than you expect.
- Confirm sshd's LogLevel is at least INFO. Set to QUIET it records no authentication failures at all.
- Raise journald's retention, or ship logs off the host. A compromised machine's local journal is evidence an attacker can edit; a copy elsewhere is not.
- Alert on what matters rather than on volume: a successful login from an unfamiliar address, an Accepted password line on a server that should be keys-only, a new sudoers entry, a new authorized_keys entry.
- Review successful logins occasionally and deliberately. Failures are noise; a success you cannot account for is the whole game.
6. Patching and recovery
The last two, and the order between them matters less than that both exist.
Keep sshd and the kernel current — enable unattended security updates if your change process allows it. OpenSSH has had pre-authentication vulnerabilities and will have more, and the window between disclosure and mass exploitation is now measured in days.
Then backups, with one property that matters more than all the others: at least one copy the server itself cannot reach or delete. Ransomware operators look for the backup target before they encrypt anything, and a mount the compromised host can write to is not a backup, it is a second copy waiting to be destroyed. And restore one — a backup that has never been restored is a hypothesis, not a plan.
FAQ
- What is the single most important SSH hardening step?
- Disabling password authentication. With PasswordAuthentication no and KbdInteractiveAuthentication no, there is no password to guess, so the entire category of credential-guessing attacks becomes impossible rather than merely harder. Nothing else on a hardening list comes close for the effort involved.
- Should I change the SSH port as part of hardening?
- It is worth doing for noise reduction, not for protection. Moving off 22 typically cuts failed-login volume by over 90% and makes your auth log readable again, but SSH announces itself in its version banner, so any full-range scanner finds it immediately. Treat it as log hygiene, never as a control.
- Is fail2ban part of a proper SSH hardening setup?
- It is a reasonable baseline, and much better than nothing. Configure bantime to -1, whitelist your own addresses in ignoreip, and monitor the ban count rather than the service state — a jail whose filter stopped matching after a distribution upgrade looks identical to a quiet week.
- Does MaxAuthTries protect against brute-force attacks?
- Only marginally. It limits credential attempts within a single connection, so an attacker simply opens more connections; it raises their cost slightly without changing the outcome. It is worth setting to 3, but it is not a defence — blocking the source at the firewall is what actually reduces the traffic.
