An SSH configuration can pass sshd -t and still lock out every administrator. Syntax validation cannot prove that your key works, that your account belongs to an allowed group, or that PAM will not offer another password path. Safe SSH server hardening starts with the effective configuration, changes only the controls you need, and keeps a tested recovery path open until a new login succeeds.
This guide targets current OpenSSH releases while calling out settings that vary by version or Linux distribution. Do not paste the whole block into a remote server and reload it blindly. Read the effective values first, test each authentication path, and keep provider console or physical-console access available.
Read the Effective Configuration First
/etc/ssh/sshd_config is not always the whole configuration. Distributions may load files through Include, package updates may change defaults, and Match blocks can alter values for a user or source address. The upstream default is only a reference; the output from your server is what matters.
sudo sshd -t
sudo sshd -T | grep -E \
'^(permitrootlogin|pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|authenticationmethods|authorizedkeysfile|allowgroups|allowusers|ciphers|kexalgorithms|macs|channeltimeout) '
sshd -t checks configuration syntax and host-key sanity. sshd -T prints the effective server configuration. OpenSSH generally uses the first obtained value for each keyword, so an early vendor or cloud-image drop-in may win over a later edit in the main file. Inspect the Include order and files such as sshd_config.d/50-cloud-init.conf. If you use Match rules, add -C with representative connection values. The official sshd_config manual documents those parameters.
One common misconception is that root password login is enabled by default everywhere. Current upstream OpenSSH defaults PermitRootLogin to prohibit-password, which blocks password and keyboard-interactive authentication for root but still permits public-key login. A distribution or existing configuration can override that value, so check sshd -T rather than guessing.
Configure Key-Only Authentication Without a PAM Back Door
For a server that should accept public keys only and does not use keyboard-interactive MFA, this is a clear baseline:
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey
PermitRootLogin no
PasswordAuthentication no disables the SSH password method. It does not, by itself, prove that a PAM-backed keyboard-interactive prompt cannot accept a password. Disabling KbdInteractiveAuthentication closes that second interactive path, while AuthenticationMethods publickey makes the intended policy explicit.
Do not copy this block if the server intentionally uses Duo, an OATH token, or another PAM-backed second factor. That design normally needs KbdInteractiveAuthentication yes and an authentication chain such as publickey,keyboard-interactive:pam. The PAM stack must then be audited so it accepts the intended factor rather than silently restoring password login. Also avoid changing UsePAM casually; distributions use PAM for account and session controls in addition to authentication.
Prefer a named administrative account with sudo over direct root login. If an automation key genuinely requires root, PermitRootLogin prohibit-password can retain key access, but restrict that key with an effective forced command and review its lifecycle.
Restrict Accounts Before Tuning Cryptography
An allowlist removes SSH access from application and service accounts that never need a shell:
AllowGroups sshusers
MaxAuthTries 4
LoginGraceTime 30
LogLevel VERBOSE
Create the group and add every required administrator before enabling AllowGroups. Test automation and emergency accounts too. Each public key offered by an agent can consume an authentication attempt, so test MaxAuthTries 4 with real clients; use client-side IdentitiesOnly yes per host when an agent holds many keys. LoginGraceTime 30 limits how long an unauthenticated connection may wait.
LogLevel VERBOSE can add the public-key fingerprint used during authentication, which is useful when keys have owners and expiry dates. Check the logs on your distribution before relying on this: logging destinations and the detail already present at the default level vary by OpenSSH build and system logger.
Changing port 22 may reduce automated log noise, but it is not an access-control boundary. If you move the port, update the host firewall, cloud firewall, NAT rules, monitoring, configuration management, and any SELinux port policy before reloading SSH. On Ubuntu systems where ssh.socket is active, the socket unit controls the listener and changing Port alone may not move it. A firewall allowlist or VPN is a stronger control than a surprising port number.
Do Not Freeze a Modern Server to an Old Algorithm List
Hard-coding complete Ciphers, KexAlgorithms, and MACs lists often makes a current server worse. A replacement list discards the vendor defaults, including algorithms added in newer releases. The current upstream proposal list includes ChaCha20-Poly1305, modern AES modes, and hybrid post-quantum key exchange.
Inspect what the installed versions support and what the daemon will actually offer:
ssh -Q cipher
ssh -Q kex
ssh -Q mac
sudo sshd -T | grep -E '^(ciphers|kexalgorithms|macs) '
Keep a patched OpenSSH release and its distribution crypto policy unless you have a documented compliance requirement. If one legacy algorithm must be removed or one compatibility algorithm temporarily added, OpenSSH supports list modifiers such as -, +, and ^. Make the smallest version-tested change instead of replacing the entire set. The OpenSSH release notes show why static lists age badly.
Dead-Client Detection Is Not an Idle-Session Timeout
ClientAliveInterval and ClientAliveCountMax detect an unresponsive client. A user who is idle at a shell still has a responsive SSH client, which answers the encrypted keepalive message and remains connected. Those settings therefore do not enforce a ten-minute inactivity policy.
OpenSSH 9.2 and later can use ChannelTimeout to expire an inactive session channel:
ChannelTimeout session=10m
This applies to session channels, including interactive shells, commands, SCP, and SFTP. It can terminate legitimate quiet work, and closing a channel does not guarantee that every child resource disappears. Treat it as an explicit policy choice, test it with the installed OpenSSH version, and keep it out of shared baselines for hosts that run long quiet commands. Older releases will reject the directive; sshd -t is the compatibility check. Later, OpenSSH 9.7 added a global timeout type for connections with several channel types.
Audit Every Authorized-Key Source
Do not assume every home directory is under /home. Root, service accounts, directory-backed users, and custom home paths will be missed. First inspect AuthorizedKeysFile and AuthorizedKeysCommand in the effective configuration. The upstream file default is .ssh/authorized_keys .ssh/authorized_keys2, relative to each user’s actual home directory.
For the default relative paths, this shell loop reports fingerprints for files that exist across accounts returned by getent:
getent passwd |
while IFS=: read -r user _ uid gid gecos home shell; do
for rel in .ssh/authorized_keys .ssh/authorized_keys2; do
file="$home/$rel"
[ -f "$file" ] || continue
printf '\n%s\t%s\n' "$user" "$file"
sudo ssh-keygen -lf "$file"
done
done
If AuthorizedKeysFile contains absolute paths or tokens such as %u and %h, audit those resolved locations instead. If AuthorizedKeysCommand is configured, review that program, its dedicated execution user, and the external key source. Removing a file does not revoke a key supplied by a command or trusted user CA.
For hardware-backed keys, the related YubiKey SSH authentication guide covers OpenSSH security-key key types. The secure homelab remote-access guide covers the network layer around SSH.
Apply Changes Without Locking Yourself Out
- Confirm that an out-of-band console or physical console works. A theoretical recovery path is not enough.
- Keep the current SSH session open and verify public-key login in a second session.
- Back up the active configuration and change the smallest possible set of directives.
- Run
sudo sshd -t, then inspect the relevant values withsudo sshd -T. - Reload rather than stop the daemon. The unit is commonly
sshdon RHEL-family systems andsshon Debian-family systems; use the installed name. If socket activation controls a changed port, update and restart the socket unit too. - Open a third, completely new connection. Test the allowed key, a disallowed account, and a forced password-only attempt.
- Close the backup session only after every expected success and failure behaves correctly.
A syntax check cannot detect a missing group membership, a wrong key, a firewall mistake, or an unintended PAM path. That is why a new connection and working console access are part of the change, not optional cleanup afterward.
References and Useful Hardware
Start with the current sshd_config manual and sshd manual, then compare them with your distribution’s package documentation. For deeper reading and hardware-backed access, these are practical options:
Full disclosure: the Amazon links below are affiliate links. I may earn a commission from qualifying purchases at no extra cost to you.
- SSH Mastery by Michael W. Lucas for focused OpenSSH administration.
- Linux server-hardening books for the controls around SSH.
- FIDO2 hardware security keys for OpenSSH security-key credentials on compatible clients.
- USB serial-console cables for out-of-band recovery on compatible homelab and network hardware.
For more security field notes and practical infrastructure checks, follow the author’s Alpha Signal channel on Telegram.
