Home · About · Services · Industries · Projects · Products · Process · Blog · FAQ · Contact
SSH hardening checklist for Linux production servers (2026) The sshd settings, key practices and network controls we apply to every production Linux server, with a config you can test before you lock yourself out.
Put a new VPS on the internet and check its auth log an hour later. You will find hundreds of failed logins for root, admin and ubuntu from addresses you have never heard of. Most of that noise is harmless. The question is whether your SSH setup would still be safe on the day one of those guesses is right, or a laptop with a key on it goes missing.
Short answer: to harden SSH on a production server, allow key-based logins only, disable root and password authentication, limit which users and networks can connect, keep OpenSSH patched, and log enough to see who logged in with which key. Test every change with sshd -t and keep a second session open while you apply it.
This is the checklist we run on the Linux servers we look after, from single VPS deployments to campus servers hosting many APIs and databases. It assumes a current Debian or Ubuntu system, but the sshd settings are the same on any distribution running OpenSSH.
Before you change anything: avoid locking yourself out Most SSH hardening incidents are self-inflicted. Someone disables passwords before their key works, or restarts sshd with a typo in the config. A few habits prevent almost all of them.
Confirm you have out-of-band access: the provider's web console, a serial console or physical access. Know how to reach it before you need it. Log in with your key and confirm it works before turning passwords off. Keep one SSH session open while you change the config, and test from a second, new session. Validate the config with sshd -t before every reload. It reports syntax errors without touching the running daemon. Reload rather than restart. On Debian and Ubuntu, a reload runs sshd -t first and refuses a broken config, while a restart with a bad config leaves nothing listening for new connections. Put your settings in a drop-in file Debian and Ubuntu include /etc/ssh/sshd_config.d/*.conf at the top of the main config. For most keywords sshd uses the first value it reads, so a drop-in wins over anything set later in sshd_config. Watch for files that cloud images add: some ship a 50-cloud-init.conf that sets PasswordAuthentication yes. Naming your file 00-hardening.conf means it is read first and takes precedence. One file is also easy to review and roll out with Ansible.
# /etc/ssh/sshd_config.d/00-hardening.conf
# Authentication
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey
# Who may log in
AllowGroups ssh-users
# Limit guessing and idle sessions
MaxAuthTries 3
LoginGraceTime 30
MaxStartups 10:30:60
ClientAliveInterval 300
ClientAliveCountMax 2
# Turn off features most servers do not need
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
PermitUserEnvironment no
# Also log keys that were offered and rejected
LogLevel VERBOSECreate the group and add your admin users to it before applying this, or AllowGroups will lock everyone out. If you use SSH tunnels to reach a database or admin UI, set AllowTcpForwarding local instead of no, or allow it only for specific users with a Match block. KbdInteractiveAuthentication replaces the older ChallengeResponseAuthentication name, which is now a deprecated alias.
sudo groupadd --force ssh-users
sudo usermod -aG ssh-users deploy
# Check syntax, then confirm the values sshd will actually use
sudo sshd -t
sudo sshd -T | grep -Ei 'permitrootlogin|passwordauthentication|kbdinteractive|allowgroups|maxauthtries'
# Apply (the service is called ssh on Debian and Ubuntu, sshd elsewhere)
sudo systemctl reload sshsshd -T prints the effective configuration after every include and default is applied. It is the only reliable way to know that a cloud-init file or an old line further down has not overridden your setting.
Keys: what to issue and how to manage them Disabling passwords moves all the risk onto keys, so treat them as credentials with an owner and a lifetime.
Use Ed25519 keys for new users. RSA keys should be at least 3072 bits; DSA support was removed entirely in OpenSSH 10.0. Protect private keys with a passphrase and use an agent, so the key on disk is not usable on its own. For administrators, prefer hardware-backed keys (ed25519-sk) on a FIDO2 security key. A stolen laptop then does not mean a stolen key. One key per person per device. No shared deploy keys copied between laptops. Keep authorized_keys files under configuration management, and remove keys the same day someone leaves or a device is retired. Restrict automation keys with options such as from=, command= and restrict, so a CI key can only do the one job it exists for. # Hardware-backed key that requires touch and PIN on every use
ssh-keygen -t ed25519-sk -O resident -O verify-required -C "alex@laptop-2026"
# authorized_keys entry for a CI deploy key: one source range, one command
from="203.0.113.0/24",command="/usr/local/bin/deploy.sh",restrict ssh-ed25519 AAAAC3Nz... ci-deployOnce a team grows past a handful of people or servers, consider SSH certificates. You sign each user's public key with a certificate authority for a limited period, and servers trust the CA through TrustedUserCAKeys instead of a long list of individual keys. Access expires on its own, and offboarding no longer means editing authorized_keys on every machine.
# Sign a user's key, valid for 8 hours, for the login name deploy
ssh-keygen -s /secure/ssh_user_ca -I alex -n deploy -V +8h id_ed25519.pub
# On each server, in the drop-in file
TrustedUserCAKeys /etc/ssh/user_ca.pubNetwork: shrink who can reach port 22 The strongest control is not letting the internet talk to sshd at all. Our order of preference is: SSH reachable only over a private network such as Tailscale or WireGuard, then SSH restricted by cloud security group or firewall to known office or bastion addresses, and only then SSH open to the world with rate limiting.
# ufw: allow SSH only from the office range and the tailnet interface
sudo ufw default deny incoming
sudo ufw allow from 198.51.100.0/24 to any port 22 proto tcp
sudo ufw allow in on tailscale0 to any port 22 proto tcp
sudo ufw enable
# If SSH has to stay public, at least rate-limit new connections
sudo ufw limit 22/tcpMoving SSH to a non-standard port cuts log noise but does not stop anyone who scans properly, so treat it as tidiness, not security. If you do change the port on Ubuntu 24.04, note that SSH is socket-activated by default: run systemctl daemon-reload and restart ssh.socket, or the old port stays open.
OpenSSH 9.8 and later also include PerSourcePenalties, enabled by default, which temporarily refuse connections from addresses that repeatedly fail authentication. On older versions, or if you want longer bans across services, fail2ban with an sshd jail still works. On systems that log only to the journal, set backend = systemd in the jail, otherwise fail2ban has no log file to read.
Patching and cryptography Configuration cannot protect a vulnerable daemon. CVE-2024-6387 (regreSSHion) allowed remote code execution as root in OpenSSH 8.5p1 to 9.7p1 on glibc systems, with no valid credentials needed. Distributions shipped fixes quickly, but only to servers that installed them. Enable unattended security updates, and make sure a reboot or service restart actually happens afterwards.
For cryptography, current OpenSSH defaults are good, and hand-written cipher lists copied from old guides often make things worse. OpenSSH 10.0 made the post-quantum hybrid mlkem768x25519-sha256 the default key exchange, and 10.1 warns when a connection falls back to one that is not post-quantum. Rather than pinning algorithms, keep OpenSSH current, remove legacy host keys you do not need, and audit what the server offers.
# What your installed OpenSSH supports
ssh -V
ssh -Q kex
# Audit the algorithms a server offers (ssh-audit is packaged on Debian/Ubuntu)
ssh-audit server.example.comIf ssh-audit flags SHA-1 based algorithms or small RSA host keys, remove them. If your policy does require an explicit KexAlgorithms line, build it from ssh -Q kex on the server, because algorithm names differ between versions and an unknown name stops sshd from starting.
Logging and review Current OpenSSH already logs the key fingerprint of every accepted login, and LogLevel VERBOSE adds the keys that were offered and rejected. Together that turns "someone logged in as deploy" into "the CI key logged in as deploy from this address", which is what you need during an incident. Ship auth logs off the server, so an intruder with root cannot quietly edit them, and alert on logins from unexpected sources or at unusual times.
# Recent successful logins and the key used
journalctl -u ssh --since today | grep 'Accepted publickey'
# Fingerprints of keys currently authorised for a user
ssh-keygen -lf /home/deploy/.ssh/authorized_keysOnce a quarter, compare the fingerprints in authorized_keys against the people and systems that should have access. Stale keys from former staff or retired CI runners are one of the most common findings in any access review.
The checklist Out-of-band console access confirmed, second session open during changes. Settings in /etc/ssh/sshd_config.d/00-hardening.conf, checked with sshd -t and sshd -T. Root login, passwords and keyboard-interactive authentication disabled. Logins limited to an ssh-users group; forwarding and X11 off unless needed. Ed25519 or hardware-backed keys, one per person per device, removed on leaving. Automation keys restricted with from=, command= and restrict. Port 22 reachable only over a private network or from known addresses. OpenSSH kept patched through unattended security updates. LogLevel VERBOSE, auth logs shipped off the server, quarterly key review. If you have many servers with keys nobody can account for, or SSH is the only way in and you are nervous about changing it, that is a good moment to get help. The work itself is not complicated, but doing it across a fleet without an outage needs a plan, a rollback path and a tested console.
Infigini IT Services · 11 Kanwali Road, Dehradun 248001, India · info@infigini.com · +91 95684 87789