Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

SSH Hardening: How to Lock Down Your VPS in 7 Steps (2026)

Every server with a public IP address is being probed on port 22 right now. Automated botnets try common usernames and passwords against SSH daemons around the clock — thousands of attempts a day against a completely fresh VPS. The good news: SSH hardening is one of the highest-return security tasks in all of system administration. About thirty minutes of careful config work takes you from “guessing game target” to “mathematically out of reach.”

This guide walks you through SSH hardening in seven steps, in the exact order you should do them. Every directive below was verified against the current OpenSSH sshd_config manual. No outdated advice, no cargo-cult settings, and — most importantly — a safety routine that keeps you from locking yourself out of your own box.

Step 0: The rule that saves you from yourself

Before you change a single SSH setting, open two SSH sessions to the server. Do the work in one; keep the other idle as your lifeline. After every config change, you test the new settings from a third session before closing either of the first two. If something breaks, the idle session is still authenticated and you can revert instantly.

This discipline matters more than any directive below. Most SSH lockouts aren’t caused by bad settings — they’re caused by reloading sshd and closing the only working session in one motion.

Step 1: Switch to SSH key authentication

Passwords can be brute-forced. A 256-bit Ed25519 private key cannot, practically speaking. Key-based auth is the single biggest SSH hardening win, and everything below builds on it.

On your local machine (not the server), generate a key:

ssh-keygen -t ed25519 -C "you@example.com"

Use a passphrase. It encrypts the private key at rest, so a stolen laptop doesn’t hand over your servers.

Copy the public half to the server:

ssh-copy-id youruser@your-server-ip

Then log in and confirm key auth works:

ssh youruser@your-server-ip

You should connect without a password prompt (or with only your passphrase prompt, which is your local key decrypting itself). Open a second session and confirm again. Only when key login is proven do you touch the password settings in step 3.

Step 2: Create a drop-in config file instead of editing sshd_config directly

Ubuntu and Debian ship an sshd_config that already includes a drop-in directory via an Include /etc/ssh/sshd_config.d/*.conf line near the top of the file. There is a subtlety worth knowing: for most keywords, the first value sshd finds wins. Because the Include line sits above the stock settings, anything you put in a drop-in file like 99-hardened.conf overrides the defaults cleanly — and your changes survive package updates that replace the main config file.

Create the file:

sudo nano /etc/ssh/sshd_config.d/99-hardened.conf

You will fill this file in the next steps. One more tip you’ll thank yourself for later: after writing it, always validate before reloading:

sudo sshd -t && echo "config OK"

sshd -t checks syntax and exits silently on success. It catches typos that would otherwise leave sshd running the old config — or worse, failing to reload at all.

Step 3: Disable password and keyboard-interactive authentication

Add these lines to your drop-in file:

PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes

Two things to know here. First, PasswordAuthentication defaults to yes, so you must explicitly turn it off — with keys proven working, passwords are pure risk. Second, use KbdInteractiveAuthentication, not the older ChallengeResponseAuthentication: the old name is a deprecated alias in current OpenSSH. Many old hardening guides still print it; the current manual lists KbdInteractiveAuthentication as the real keyword.

While you’re at it, ignore another relic: Protocol 2. That keyword doesn’t exist in the current manual at all — OpenSSH speaks SSH-2 only, and has for years. If a guide tells you to add it, the guide is stale.

Step 4: Lock down who can log in as whom

Add these to the drop-in file:

PermitRootLogin no
AllowUsers youruser
MaxAuthTries 3
LoginGraceTime 60
X11Forwarding no

What each one does:

  • PermitRootLogin no — root never logs in directly. You log in as your unprivileged user and use sudo, which also leaves an audit trail. (The stock default is prohibit-password, which blocks root passwords but still allows root keys — no is the stricter choice.)
  • AllowUsers youruser — sshd rejects every username that isn’t on this list before authentication even starts. One line, and botnets guessing admin, test, or oracle get nothing.
  • MaxAuthTries 3 — the default is 6; 3 is plenty for a human with a key and punishes guessing faster.
  • LoginGraceTime 60 — disconnect clients that don’t authenticate within 60 seconds (default 120). Cheap connections cost attackers almost nothing, so give them less time.
  • X11Forwarding no — you almost certainly don’t need X11 forwarding on a headless server. Off means one less attack surface.

Step 5: Reload sshd — the safe way

Validate, then reload (not restart — a reload keeps existing sessions alive while the daemon picks up the new config):

sudo sshd -t && sudo systemctl reload ssh

On Ubuntu the service unit is called ssh. Now the critical part: from your local machine, open a brand-new third session and log in. If it works, close the two old ones at your leisure. If it fails, your idle session from step 0 is still connected — read the error, fix the config, reload again.

Quick sanity checks after reload:

# Should FAIL (passwords are off)
ssh -o PreferredAuthentications=password youruser@your-server-ip

# Should SUCCEED (key auth)
ssh youruser@your-server-ip

# Should FAIL (root is off)
ssh root@your-server-ip

Step 6: Add fail2ban as a second layer

With passwords disabled, brute force can’t succeed — but bots still burn connection slots and fill logs. Fail2ban watches the auth log and bans repeat offenders at the firewall:

sudo apt update && sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

On Debian and Ubuntu the sshd jail is enabled out of the box, watching /var/log/auth.log with default limits (5 failed attempts in 10 minutes earns a 10-minute ban). Tighten it and whitelist yourself in a local override file — never edit jail.conf directly:

sudo nano /etc/fail2ban/jail.local
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.42

[sshd]
enabled = true
maxretry = 3
findtime = 10m
bantime = 1h

Replace 203.0.113.42 with your own IP (or your office/home range). Locking yourself out with fail2ban is the second most common self-inflicted SSH outage — ignoreip is the seatbelt.

sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

You should see the jail active with the auth log under “File list”. Within a day or two on a public IP you’ll see the ban counter start climbing — that’s the bots finding a closed door.

Step 7: Watch the logs and finish the job

Hardening without verification is just vibes. Check that the noise is actually dying down:

# Failed login attempts (should trend toward zero after step 3)
sudo grep "Failed password" /var/log/auth.log | tail -20

# Top offenders by IP
sudo grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -10

# Currently banned by fail2ban
sudo fail2ban-client status sshd

Once password auth is off, the “Failed password” lines should nearly vanish — the server rejects password attempts before they ever become a credential check. A sudden return of that noise is worth investigating.

Two final layers, in order of value:

  • Firewall first. UFW with a deny-by-default policy: sudo ufw default deny incoming && sudo ufw allow ssh && sudo ufw enable. Fail2ban needs a firewall to actually enforce bans.
  • Log verbosity. Set LogLevel VERBOSE in your drop-in file. The default INFO logs logins; VERBOSE also logs which key fingerprint was used, which is gold when auditing who touched the box.

What about changing the SSH port?

You will see every hardening guide suggest moving SSH off port 22. Be honest about what it buys you: it eliminates almost all automated botnet noise, but it stops no determined attacker — port scanners find the new port in seconds. It’s a noise-reduction trick, not a security control. If you do it, pair it with everything above, never as a substitute. (If you change it, remember to update UFW and fail2ban’s port setting first, and keep that second session open.)

The 30-second checklist

When you spin up the next VPS, run this list top to bottom:

  1. Ed25519 key generated, copied with ssh-copy-id, login proven in two sessions.
  2. /etc/ssh/sshd_config.d/99-hardened.conf with passwords off, keyboard-interactive off, root off, AllowUsers set, MaxAuthTries 3, LoginGraceTime 60.
  3. sshd -t passes; systemctl reload ssh; new session proves key login; password and root login proven dead.
  4. fail2ban installed, ignoreip includes your IP, sshd jail tightened.
  5. UFW deny-by-default with SSH allowed; logs checked the next day.

If you run OpenSSH 10.6 or later, you’re also covered by its hardened defaults (LZ77 compression is now off by default, among other changes) — see our OpenSSH 10.6 upgrade checklist for what else changed. And since a hardened server still needs a way back from disaster, make sure your automated VPS backups are actually running.

Written October 23, 2026. All sshd_config directives verified against the current OpenSSH manual (man7.org). Tested concepts apply to OpenSSH 9.x and 10.x on Ubuntu/Debian.

Further Reading & References

Leave a Comment