If you put a server on the public internet with port 22 open and password login enabled, you are going to get knocked on. Not maybe — guaranteed. The bots find you within minutes, and they never sleep. The good news: locking SSH down properly takes about ten minutes and roughly six lines of config.
🔐 Lock Down SSH in 10 Minutes: A Hardening Checklist
This is the same config I put on every Linux box I run — from a $5 VPS to the Proxmox cluster in my office. By the end of this post you’ll have key-only authentication, no root login, a non-default port, a whitelisted user group, fail2ban, and a workflow that won’t lock you out of your own server.
📋 Prerequisites
- A Linux server you can already SSH into (Debian/Ubuntu in the examples — same idea on RHEL/Alma/Rocky).
- A non-root user with
sudorights. If you’ve been logging in as root, create one first (adduser christopher && usermod -aG sudo christopher). - A second terminal ready before you touch sshd. This is the rule that has saved me more times than I can count.
🗝️ Step 1: Get on Keys, Get Off Passwords
Passwords are the single biggest reason servers get popped. Even a 16-character password is brute-force-able if a bot has a year and you’re not rate-limiting. SSH keys aren’t — a 256-bit ed25519 key is, for practical purposes, uncrackable.
Generate one on your laptop (not the server), then copy the public half up:
# Generate a modern ed25519 key (faster, smaller, stronger than RSA)
ssh-keygen -t ed25519 -C "christopher@laptop"
# Copy the public key to your server (still using password — for now)
ssh-copy-id -p 22 [email protected]
# Test that key-based login works BEFORE you disable passwords
ssh [email protected]If ssh-copy-id isn’t available, append the contents of ~/.ssh/id_ed25519.pub to ~/.ssh/authorized_keys on the server manually. Don’t skip the test login — verify keys work before you disable passwords below.
🛡️ Step 2: The Six-Line Hardening File
On modern OpenSSH you don’t edit /etc/ssh/sshd_config directly — you drop a file in /etc/ssh/sshd_config.d/ and it gets included automagically. That way distro upgrades don’t trample your changes.
# /etc/ssh/sshd_config.d/99-hardening.conf
# Drop this file in the .d/ directory — it overrides the main sshd_config.
# 1. No passwords. Keys only.
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
# 2. No root over SSH. Log in as a normal user and sudo.
PermitRootLogin no
# 3. Move off port 22 — kills 99% of drive-by bots.
Port 2222
# 4. Whitelist who can SSH in (group is easier to manage than user list).
AllowGroups ssh-users
# 5. Idle session timeout (server kicks dead sessions after ~15 min).
ClientAliveInterval 300
ClientAliveCountMax 2
# 6. Limit retries before disconnect.
MaxAuthTries 3
LoginGraceTime 30What each setting does:
- PasswordAuthentication no — bots can’t try
admin/adminanymore. They’ll still knock, but they’ll fail instantly. - PermitRootLogin no — even if someone steals your root password, they can’t use it for SSH. They’d need a key in
/root/.ssh/authorized_keys, which shouldn’t exist. - Port 2222 — this isn’t real security (it’s “security through obscurity”), but it cuts log noise by ~99% because the drive-by scanners only check 22. Pick anything between 1024 and 65535 that isn’t already used.
- AllowGroups ssh-users — only users in this group can SSH in at all. Now you can
adduser someonefor sudo without giving them remote access. - ClientAliveInterval / CountMax — closes dead sessions so you don’t have ghost connections eating slots.
- MaxAuthTries 3 — three failed auths and the connection drops. Pairs well with fail2ban below.
🧪 Step 3: Apply It Without Locking Yourself Out
This is where people brick their own server. Do these in order:
# Validate the config BEFORE you restart — a typo here can lock you out.
sudo sshd -t
# Add yourself to the whitelist group.
sudo groupadd ssh-users 2>/dev/null
sudo usermod -aG ssh-users $USER
# Open the new port in the firewall BEFORE reloading sshd.
sudo ufw allow 2222/tcp
sudo ufw delete allow 22/tcp # only after you've tested 2222
# Reload sshd — this keeps your current session alive.
sudo systemctl reload sshNow — do not close your existing SSH session. Open a brand-new terminal and try to connect on the new port:
# In a SECOND terminal — keep your existing SSH session open.
# If you can log in on the new port, you're safe to close the old one.
ssh -p 2222 [email protected]If that works, great — close the old session. If it doesn’t, you still have the old session to fix whatever broke. The number-one cause is forgetting to open the new port in the firewall before reloading sshd.
🚧 Step 4: fail2ban for Everything Else
Keys + non-default port + AllowGroups stops the vast majority of attacks at the door. fail2ban handles the rest: anybody who hits MaxAuthTries three times gets their IP firewalled for an hour. Free, painless, no false positives if your config is right.
# Install fail2ban for everything bots still try (script kiddies, password sprays).
sudo apt install -y fail2ban
# Drop a jail config for sshd. Use the new port you set above.
sudo tee /etc/fail2ban/jail.d/sshd.local >/dev/null <<'EOF'
[sshd]
enabled = true
port = 2222
maxretry = 3
findtime = 10m
bantime = 1h
EOF
sudo systemctl enable --now fail2ban
sudo fail2ban-client status sshdRun sudo fail2ban-client status sshd any time to see who’s currently banned. On a fresh server with port 22 closed, this list stays empty for weeks. On port 22 it’d be ten IPs deep within an hour.
💻 Bonus: A Sane Client Config
Now that your server is on a non-default port, you don’t want to type ssh -p 2222 christopher@host a dozen times a day. Drop an entry in ~/.ssh/config on your laptop:
# ~/.ssh/config on your laptop — so you don't have to type -p 2222 every time.
Host pegasus
HostName pegasus.example.com
Port 2222
User christopher
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 60Now ssh pegasus uses the right user, key, and port automatically. Add one block per server you care about.
✅ The Checklist
- Generate an ed25519 key on your laptop and copy it to the server.
- Verify key-based login works before changing anything else.
- Drop
99-hardening.confinto/etc/ssh/sshd_config.d/. sshd -tto validate, open the new port in the firewall, thensystemctl reload ssh.- Test the new port from a second terminal before closing your existing session.
- Install fail2ban with a jail pointed at the new port.
- Add a matching block to
~/.ssh/configon every client you use.
You’re done! Your server is now harder to break into than 99% of what’s on the public internet, and the noise in /var/log/auth.log just dropped to almost nothing.
Next post in this series I’ll cover the things you do after the basics: SSH certificates (so you can revoke keys across a fleet without touching every server), 2FA via TOTP, and how to wire all of this into Ansible so a fresh VPS is hardened in one playbook run. If you’ve got a specific SSH question — or a story about the time you locked yourself out — I’d love to hear it.
Thanks!