SSH hardening: keys only, one allowed user, and a way back in

SSH is the front door of most servers, and bots try passwords on it all day. Two changes remove most of that risk: keys instead of passwords, and an explicit list of users who may log in. The one rule that matters: keep your current session open while you test, so a mistake can't lock you out.

See what is enabled now

bash
sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|pubkeyauthentication|maxauthtries|allowusers|x11forwarding) '

Add a key before you turn passwords off

From your own machine, copy a public key to the account you'll use, then log in with it. Don't go on until that works.

bash
ssh-keygen -t ed25519 -C "admin laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@server.example.com
ssh -i ~/.ssh/id_ed25519 deploy@server.example.com 'echo key login works'

Harden sshd with a drop-in file

Ubuntu reads /etc/ssh/sshd_config.d/*.conf by default, so the hardening stays in one file that's easy to review. The name starts with 00 on purpose: sshd uses the first value it reads, and cloud images ship 50-cloud-init.conf with PasswordAuthentication yes. Replace deploy with the users who need SSH.

conf
# /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
AllowUsers deploy
X11Forwarding no

Check the syntax, then reload

sshd -t parses the config without touching the running service. Reload only when it passes.

bash
sudo sshd -t && echo "config OK"
sudo systemctl reload ssh

Audit it with a script

Settings drift when someone edits a file in a hurry. This script reads the effective configuration from sshd -T and compares it with the values you expect. Run it as root.

python
import subprocess

EXPECTED = {
    "permitrootlogin": "no",
    "passwordauthentication": "no",
    "pubkeyauthentication": "yes",
    "x11forwarding": "no",
}


def effective_config() -> dict[str, str]:
    out = subprocess.run(["sshd", "-T"], capture_output=True, text=True, check=True).stdout
    settings = {}
    for line in out.splitlines():
        key, _, value = line.partition(" ")
        settings[key] = value
    return settings


def main() -> int:
    current = effective_config()
    failed = 0
    for key, want in EXPECTED.items():
        got = current.get(key, "missing")
        if got != want:
            failed += 1
        print(f"{'ok' if got == want else 'FAIL':4} {key:24} want={want:3} got={got}")
    return 1 if failed else 0


if __name__ == "__main__":
    raise SystemExit(main())

Keep the other layers in view

Key-only login stops password guessing, but it doesn't stop the connection attempts themselves. Pair it with a rate limit or a ban list from the fail2ban article, and keep the firewall rule that limits port 22 in the ufw article.

Certificates and uptime break quietly, too. osec Monitor watches both for you, free.

Flow: key check, password refused, user check, root refused, second session test
Key login only, with a second session as the safety net.

Sources

• OpenSSH sshd_config reference: https://man.openbsd.org/sshd_config
• Ubuntu sshd drop-in configuration: https://manpages.ubuntu.com/manpages/noble/en/man5/sshd_config.5.html