How to Harden Your SSH Server Config

The sshd_config file is the entire security posture of your SSH server in one text file — and on most systems, it ships in a state optimised for compatibility, not security. Password login is on by default, root login is allowed as long as a key is used, idle sessions never time out, and the algorithms your server offers are whatever the installed OpenSSH version decided years ago. Hardening it is not a matter of copying a checklist from 2021: algorithms get removed, defaults change, and a config that was exemplary five years ago can be weak today. This article explains what each change actually does, why the order of your decisions matters, and how to test a new config without locking yourself out. You can audit your own file line by line in the SSH Server Config Auditor, which checks it against a baseline dated 2026-11-13, entirely in your browser.

Defaults are a starting point, not a hardening posture

OpenSSH ships with defaults chosen so that a fresh install works for as many people as possible — and that is exactly why they are not a security posture. The current defaults, as documented in the sshd_config man page, include PasswordAuthentication yes, PermitRootLogin prohibit-password, MaxAuthTries 6, LoginGraceTime 120 seconds, and ClientAliveInterval 0, which means idle sessions are never terminated. None of these is a bug: each is a deliberate compatibility choice, and each is a finding in a hardening audit.

The practical consequence is that a server nobody has ever hardened is still reachable by password guessing, still accepts root logins with a stolen key, and still leaves abandoned sessions open indefinitely. The defaults also change over time — what OpenSSH 10.6 ships is not what OpenSSH 7 shipped — so "I never changed anything" is not a neutral statement. It means your security posture is whatever the version you happened to install decided, and the only way to know is to read the file. That is the first job of an audit: to make the invisible defaults visible, because a directive that is absent from the file is still in force.

Passwords off beats a different port, every time

The single most effective change in sshd_config is PasswordAuthentication no combined with PubkeyAuthentication yes: from that moment, no password — guessed, leaked, or phished — can open a session, because the daemon no longer accepts that kind of proof at all. Key authentication changes the attack from "guess a secret a human chose" to "steal a private key file that never crosses the network", which is why every hardening baseline, including Mozilla’s, starts here. If you take one thing from this article, take this: generate a key pair (see the SSH Key Suite and the companion article SSH keys explained), install the public key, verify you can log in with it, and only then turn passwords off.

Changing the port from 22 to something obscure, by contrast, is not hardening — it is obscurity. It cuts the noise in your logs, because most internet-wide scanners only knock on port 22, but it stops no attacker who bothers to scan: a port scan finds the daemon in seconds, and the daemon behind it is exactly as strong as its config. The auditor therefore treats a non-22 port as information only, never as a scored improvement. There is one trap worth knowing before you flip the switch: on systems using PAM, PasswordAuthentication no alone is not sufficient — keyboard-interactive authentication can still let PAM accept a password, so the PAM stack in /etc/pam.d/sshd must also refuse passwords. Mozilla’s guidelines say this explicitly, and it is the most commonly missed half of the change.

Root login: what prohibit-password actually allows

PermitRootLogin has four values, and the difference between them is the difference between an audit trail and none. The default, prohibit-password, allows root to log in — just not with a password. A stolen root key still walks straight in, and once the session is root, nothing in the logs distinguishes one administrator from another. Mozilla’s Modern profile sets it to No, and that is the hardened answer: root should never be a login account at all. Log in as a named user and use sudo; then every privileged action is attributable to a person.

The middle values exist for a reason, and they are worth knowing rather than fearing. forced-commands-only allows root login only when the offered key is restricted to a single command in authorized_keys — the pattern backup and monitoring scripts use — and is a legitimate, deliberate choice for automation. What is not legitimate is yes, which permits root login with a password and is an error in any audit. The practical rule: if a human logs in as root, the config is wrong; if a script does, restrict the key to one command and say so in a comment, because the next person to read the file will otherwise "fix" it.

Algorithm deprecation is a moving target

The algorithms in your config have an expiry date, and it is shorter than you think. OpenSSH 10.0, released 2025-04-09, removed DSA (ssh-dss) support entirely, dropped finite-field Diffie-Hellman from the server’s default key exchange, and made mlkem768x25519-sha256 — a post-quantum hybrid — the default key agreement. A config copied from a 2021 blog post may still list diffie-hellman-group1-sha1, 3des-cbc, arcfour, or hmac-md5, all of which are now errors in a modern audit, and some of which the installed daemon no longer even supports. The config did not change; the world did.

That is why a hardening baseline must carry a date, and why re-checking is part of the job, not an afterthought. Mozilla’s Modern profile — curve25519 key exchange, chacha20-poly1305 and AES-GCM ciphers, SHA-2 encrypt-then-MAC — is the reference, but even it predates the post-quantum key exchanges that OpenSSH 10.0 made default, so a 2026 audit accepts mlkem768x25519-sha256 and sntrup761x25519-sha512 alongside it. The habit that keeps you current is cheap: once or twice a year, run ssh -Q kex, ssh -Q cipher, and ssh -Q mac on the server to see what the installed version actually offers, compare it with the current Mozilla guidelines, and re-run the auditor against the same file. A clean score from 2021 is a historical document, not a current one.

Match blocks and Include: the two ways a clean audit lies

A config file is not a flat list of settings — it is a program with conditional branches, and two features can make a file that reads as hardened behave as if it were not. The first is the Match block: directives inside Match User, Match Group, or Match Address apply only when the criteria hold, and the man page is explicit that if a keyword appears in several satisfied Match blocks, only the first instance applies. A global PasswordAuthentication no followed by a Match block that quietly re-enables passwords for one group is a real configuration, and it is exactly the kind of thing a line-by-line read misses. Findings from inside a Match block are conditional, never global, and any honest audit scopes them that way.

The second is Include. Modern distributions split sshd_config across files — /etc/ssh/sshd_config.d/*.conf is the usual pattern — and an Include directive pulls those files in, in lexical order, at the point where it appears, which on most distributions is the top of the file. Because the first obtained value wins, a directive set in an included file shadows the same directive later in the main file: the line you read is not the line that governs. An auditor that runs in the browser cannot follow Includes — the files are not on your machine — so it flags them as "not followed" and lists the path. When you audit, audit the whole directory, not the one file, and remember that a duplicate directive is not a typo to ignore: the first one wins and the second is dead text that will confuse the next person to edit the file.

The safe rollout: test before you close the door

Every sshd_config change carries the same risk: a typo can refuse the daemon a restart, and a correct-but-wrong setting can refuse you. The rollout that avoids both is short and non-negotiable. First, check the syntax with sshd -t before touching the running service — it parses the file and reports errors without starting anything. Then restart or reload the daemon, and only then test. Never edit the config and close your only session in the same breath.

The test itself is the discipline: keep your existing session open, open a second session, and log in fresh with the new rules in force. If the second login works, the change is safe and the first session can be closed; if it does not, the open session is your way back in, and you can revert the file and reload. This is also why changes should be made one at a time on a server you cannot physically reach — a bad key file and a disabled password method in the same edit leave no way back at all. The auditor can produce a hardened version of your file to start from, but it is a draft to review, Match block by Match block, and test with sshd -t — never a file to drop in unreviewed.

Sources

Frequently asked questions

Is changing the SSH port from 22 worth doing?

It reduces log noise, because most automated scanners only probe port 22 — but it is not hardening. A port scan finds the daemon on any port in seconds, and the daemon is exactly as strong as its config. Treat a non-standard port as a tidy-up, never as a substitute for disabling password authentication, restricting root login, and removing weak algorithms. The auditor scores it as information only.

I set PasswordAuthentication no, but I can still log in with a password. Why?

Almost always PAM. On systems where sshd uses PAM, keyboard-interactive authentication can still hand the password check to PAM even after PasswordAuthentication is off — Mozilla’s guidelines warn explicitly that "PasswordAuthentication no is not sufficient" in that setup. Check /etc/pam.d/sshd and make sure no password module (such as pam_unix.so or the password-auth include) is reachable for sshd, then test a fresh login before closing your existing session.

How often should I re-audit my sshd_config?

At least once a year, and after every OpenSSH upgrade. Algorithms are removed on a schedule — OpenSSH 10.0 dropped DSA entirely and changed the default key exchange — so a config that scored clean in 2021 can be weak in 2026 without a single line changing. Re-run the auditor against the same file, check the baseline date on the page, and confirm the installed version with ssh -Q kex, ssh -Q cipher, and ssh -Q mac.