Skip to content

SSH Hardening and Key-Based Authentication

Every internet-facing Linux server gets scanned for SSH logins within minutes of being connected. This article covers moving from password to key-based authentication and the sshd_config changes that meaningfully reduce exposure.

How it works

Generating and deploying a key pair

ssh-keygen -t ed25519 -C "[email protected]"

-t ed25519 selects the Ed25519 algorithm — smaller keys and faster verification than RSA, with equivalent or better real-world security; prefer it unless you need to support a genuinely ancient client/server that predates it (RSA with a 4096-bit key, -t rsa -b 4096, is the fallback). This creates a private key (~/.ssh/id_ed25519, never share this) and a public key (~/.ssh/id_ed25519.pub, safe to share — this is what goes on servers).

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

This appends your public key to ~/.ssh/authorized_keys on the remote server (creating the file and directory with correct permissions if needed). Equivalent manually:

cat ~/.ssh/id_ed25519.pub | ssh user@server 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'

Permissions matter: sshd silently refuses to use an authorized_keys file (or .ssh directory) that's group- or world-writable, as a security measure against other local users tampering with it. 700 for ~/.ssh, 600 for authorized_keys and the private key itself.

Hardening sshd_config

Key settings in /etc/ssh/sshd_config (confirm your OpenSSH version supports each — these have been stable for many years, ssh -V shows your version):

PasswordAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
MaxAuthTries 3
AllowUsers deploy admin
  • PasswordAuthentication no — only after confirming key-based login actually works, in a separate session, before closing your current one. This is the single most important hardening step: it makes brute-force password guessing against SSH entirely moot, since there's no password to guess.
  • PermitRootLogin no — forces even legitimate admins to log in as a named user and sudo, which also means every privileged action is attributable to a specific account in logs.
  • MaxAuthTries 3 — disconnects after this many failed attempts within a single connection (not a long-term ban — see fail2ban below for that). Caution: every key your SSH agent offers counts as one attempt, so with several keys loaded a low value can disconnect you before the right key is even tried — pin the key with ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 user@server.
  • AllowUsers — an explicit allowlist of which local accounts may SSH in at all, regardless of whether they have valid credentials. A typo or a missing name here locks that account out of SSH entirely, including yours.

After any sshd_config change:

sudo sshd -t              # syntax-check BEFORE restarting — catches typos safely
sudo systemctl restart sshd   # Debian/Ubuntu: the unit is ssh.service (the sshd alias usually works too)

sshd -t validates the config without affecting the running daemon — always run this before restarting, since a syntax error combined with a restart can leave you with no running SSH daemon at all and no way back in remotely.

Also check the effective configuration, not just the file you edited:

sudo sshd -T | grep -iE 'passwordauthentication|permitrootlogin|maxauthtries'

OpenSSH uses the first value it finds for each keyword, and distributions include drop-in files from /etc/ssh/sshd_config.d/*.conf near the top of sshd_config. A cloud image's 50-cloud-init.conf containing PasswordAuthentication yes therefore silently overrides your PasswordAuthentication no further down the main file.

Rate-limiting repeated failures with fail2ban

sudo apt install fail2ban          # Debian/Ubuntu
sudo dnf install fail2ban          # RHEL-family (from EPEL)
# /etc/fail2ban/jail.local
[sshd]
enabled = true
maxretry = 4
findtime = 10m
bantime = 1h

fail2ban watches the auth log, and after maxretry failed attempts from the same source within findtime, inserts a firewall rule banning that source IP for bantime. This is a different, complementary layer to sshd_config's MaxAuthTries — the latter limits attempts per connection, fail2ban limits attempts (and distinct connections) per source IP over time.

Ubuntu/Debian vs. RHEL-family differences

Ubuntu/Debian RHEL-family
Service name ssh (unit ssh.service) sshd (unit sshd.service)
SELinux Not enforcing by default Enforcing — a key/file in a non-standard location may need restorecon to get the right SELinux context before sshd will use it

Practical exercise

  1. Generate an Ed25519 key pair and deploy the public key to a test VM.
  2. Confirm key-based login works, in a new terminal session, without closing your current one.
  3. Only after confirming step 2 works: set PasswordAuthentication no, run sshd -t, then restart, then open yet another new session to confirm password login is now refused.

How to verify you did it correctly

  • ssh -v user@server (verbose) should show Authentication succeeded (publickey) and never prompt for a password.
  • Attempting ssh -o PreferredAuthentications=password user@server after the hardening should be refused outright, not even prompt for a password.
  • sudo sshd -T | grep -i passwordauthentication should print passwordauthentication no. If it prints yes although your file says no, a drop-in under /etc/ssh/sshd_config.d/ is overriding it.

Common mistakes

  • Disabling password authentication before confirming key-based login works. If your only active session closes and the key doesn't actually work, you're locked out — always test in a second, independent session first.
  • Copying a private key instead of the public key to the server (the file without .pub). This doesn't grant access and typically also leaves your private key sitting insecurely on a remote host.
  • Forgetting sshd -t before restarting after an edit — a typo in sshd_config can prevent the daemon from starting at all, and you find out only after it's already down.

⚠ Dangerous commands

  • Restarting sshd/ssh.service immediately after an untested config change, on your only active remote connection, with no console/out-of-band access as a fallback. Always keep an existing session open and verify the new config in a fresh one first.
  • chmod -R on ~/.ssh (recursive) can change the private key's permissions to something sshd refuses — set permissions on the directory and specific files explicitly instead.

Review questions

  1. Why must you test key-based login in a second session before disabling password authentication?
  2. What's the difference between what MaxAuthTries in sshd_config limits and what fail2ban limits?
  3. Why does sshd refuse to use an authorized_keys file that's group-writable?
  4. What does sshd -t actually check, and why run it before every restart?