Security 📖 20 min read
📅 Published: 🔄 Updated:

Locking Down SSH: Keys, Configs, and Fail2Ban

Tested on: Ubuntu 22.04, Rocky Linux 9, OpenSSH 8.9+

Here's a hardened sshd_config. Copy it. Restart sshd. Read the explanations below if you want to understand what each line does.

⚠️ Test your new SSH config in a second terminal before closing your current session. If you lock yourself out, you need console access to fix it.

The Hardened Config

Drop this into /etc/ssh/sshd_config (back up the original first with cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak):

/etc/ssh/sshd_config
Port 47892
PermitRootLogin no
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
UsePAM yes
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
MaxAuthTries 3
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2

Then: sudo systemctl restart sshd. That's it. Your server is now rejecting passwords, root logins, and most automated attacks.

The rest of this page explains what each directive does and walks through key setup if you haven't done that yet.

🔒 The auth.log reality: On a fresh VPS with default SSH on port 22, I pulled the auth.log after 48 hours. 11,437 failed login attempts. Almost all targeting "root", "admin", "ubuntu", "test". Source IPs from across China, Russia, Brazil, Vietnam — all automated. A few examples from the log:
/var/log/auth.log (48 hours on a stock VPS)
Failed password for root from 218.92.0.107 port 53210 ssh2
Failed password for invalid user admin from 185.224.128.43 port 42180 ssh2
Failed password for root from 103.145.13.41 port 55764 ssh2
Failed password for invalid user test from 45.136.14.72 port 38920 ssh2
...repeated 11,433 more times
That's why you harden SSH. Not because you're paranoid — because the internet is a shooting range and your server is standing in it.

What Each Line Does

One line at a time:

  • Port 47892 — Moves SSH off port 22. Kills 99% of automated scanners. Not real security, but your logs get a lot quieter.
  • PermitRootLogin no — Nobody logs in as root over SSH. Use a normal user and sudo.
  • PasswordAuthentication no — Keys only. The single most important line in this file.
  • PermitEmptyPasswords no — Belt and suspenders. Should already be the default.
  • ChallengeResponseAuthentication no — Closes a backdoor that lets password auth sneak back in through PAM.
  • PubkeyAuthentication yes — Enables key-based login. Usually on by default, but be explicit.
  • AuthenticationMethods publickey — Only allow public key. No keyboard-interactive, no GSSAPI, nothing else.
  • UsePAM yes — Keep PAM on for account/session management, just not for authentication.
  • X11Forwarding no — Unless you're forwarding GUI apps (you're probably not), turn it off.
  • AllowTcpForwarding no — Prevents your server from being used as a tunnel. Enable selectively if you need it.
  • AllowAgentForwarding no — Same idea. Don't let agent forwarding be abused by a compromised server.
  • MaxAuthTries 3 — Three failed attempts, then disconnect. Slows down brute-force scripts.
  • LoginGraceTime 20 — 20 seconds to authenticate or get dropped. Default is 120, which is generous to attackers.
  • ClientAliveInterval 300 — Ping idle clients every 5 minutes.
  • ClientAliveCountMax 2 — Two missed pings and you're disconnected. Cleans up ghost sessions.

Now, if you don't already have SSH keys set up, here's how.

Generating an SSH Key

Run this on your local machine, not the server:

Your local machine
# Modern systems - use Ed25519 (faster and more secure)
ssh-keygen -t ed25519 -C "anurag@mymachine"

# Older systems that don't support Ed25519
ssh-keygen -t rsa -b 4096 -C "anurag@mymachine"

Set a passphrase when prompted. If your laptop gets stolen, the passphrase is the only thing between the thief and your servers. I keep mine in a password manager.

You get two files:

  • ~/.ssh/id_ed25519 — Your private key. Never share this. Never copy it to servers.
  • ~/.ssh/id_ed25519.pub — Your public key. This goes on servers you want to access.

Copying Your Key to the Server

Easiest method (while password auth is still enabled):

Your local machine
ssh-copy-id [email protected]

Appends your public key to ~/.ssh/authorized_keys on the server.

Manual method if ssh-copy-id isn't available:

Your local machine
# Display your public key
cat ~/.ssh/id_ed25519.pub

# Copy the output, then on the server:
mkdir -p ~/.ssh
echo "paste-your-public-key-here" >> ~/.ssh/authorized_keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

💡 Keys not working? If key auth isn't working, check permissions first: ~/.ssh should be 700, authorized_keys should be 600. SSH is picky about this.

SSH refuses to use keys if permissions are too open. The chmod commands aren't optional.

Test Before You Disable Passwords

Do not skip this. Open a new terminal and confirm key-based login works:

Your local machine
ssh [email protected]

If it asks for your key passphrase (not your server password), you're good. If it asks for the server password, something is broken — fix it before touching sshd_config.

Apply the Config

If you already copied the full config from the top of this page, you're done with this step. If you're making changes incrementally instead, here are the critical lines:

/etc/ssh/sshd_config
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
PubkeyAuthentication yes

Restart SSH to apply:

On the server
sudo systemctl restart sshd

Open a second terminal immediately and test login. Keep the first session alive until you've confirmed the new config works.

Disable Root Login

"root" is the first username every bot tries. Kill it. Make sure you have a sudo-capable user first:

On the server (as root)
adduser anurag
usermod -aG sudo anurag

Already in the config from above: PermitRootLogin no. Restart sshd, confirm you can still log in as your regular user.

Change the Port

Configuration file example
Configuration file example

People will tell you this is "security through obscurity" and therefore useless. They're half right — a port scan finds it in seconds. But the bots doing bulk password spraying across the entire IPv4 range only check port 22. Moving off it drops your log noise by 99%. That's worth the 30 seconds it takes.

The config above uses 47892. Pick any high port. Avoid 2222, 8022, 22222 — some scanners check those.

Open the port in your firewall before restarting sshd or you'll lock yourself out:

On the server
# For ufw
sudo ufw allow 47892/tcp

# For firewalld
sudo firewall-cmd --permanent --add-port=47892/tcp
sudo firewall-cmd --reload

Restart sshd, then connect with the new port:

Your local machine
ssh -p 47892 [email protected]

Save yourself from typing -p 47892 every time — add it to your local SSH config:

~/.SSH/config (on your local machine)
Host myserver
 HostName yourserver.com
 User anurag
 Port 47892

Now ssh myserver does the right thing.

Fail2Ban

Honest opinion: fail2ban is a band-aid. The real fix is key-only auth + non-standard port + firewall rules. fail2ban just makes the logs quieter. That said, I still install it everywhere because quieter logs are easier to read when something actually goes wrong.

Debian/Ubuntu
sudo apt update && sudo apt install fail2ban
CentOS/RHEL
sudo yum install epel-release
sudo yum install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban

Works out of the box for SSH. Customize it by creating a local config (the main config gets overwritten on updates):

/etc/fail2ban/jail.local
[DEFAULT]
# Ban for 1 hour
bantime = 3600

# An IP gets banned if it fails 5 times...
maxretry = 5

# ...within a 10-minute window
findtime = 600

# Ignore my home IP so I don't lock myself out
ignoreip = 127.0.0.1/8 ::1 YOUR.HOME.IP.HERE

[sshd]
enabled = true
port = ssh,47892
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
  • bantime = 3600 — Block for 1 hour. Some people go permanent. I don't — rotating IPs make that pointless.
  • maxretry = 3 — Three strikes for SSH. Legit users don't fat-finger three times in a row.
  • findtime = 600 — Counting window. 3 failures in 10 minutes = banned.
  • ignoreip — Your home/office IP goes here so you don't ban yourself when you mistype your passphrase.
  • port — Include your custom SSH port if you changed it.

Restart Fail2Ban:

Bash
sudo systemctl restart fail2ban

Checking Fail2Ban Status

Bash
# Check status of SSH jail
sudo fail2ban-client status sshd

# See currently banned IPs
sudo fail2ban-client get sshd banned

# Unban an IP (if you accidentally banned yourself)
sudo fail2ban-client unban YOUR.IP.HERE

Give it a few days, then check. You'll see hundreds of banned IPs. All automated junk that never had a chance anyway, since you disabled password auth. Like I said — band-aid. But a tidy one.

Extra Hardening

Stuff I do on servers that matter more than average:

Limit Who Can SSH

Whitelist who can SSH in:

/etc/SSH/sshd_config
AllowUsers anurag jane

Everyone else gets rejected before authentication even starts.

Use Two-Factor Authentication

Google Authenticator or Duo can require a key AND a TOTP code. That's a separate tutorial — search "SSH Google Authenticator PAM" if you want it.

Set Up Login Notifications

Simple script that emails me on every login:

/etc/SSH/sshrc
#!/bin/bash
IP=$(echo $SSH_CONNECTION | awk '{print $1}')
HOSTNAME=$(hostname)
USER=$USER
DATE=$(date)
echo "SSH login: $USER from $IP on $HOSTNAME at $DATE" | mail -s "SSH Alert: $HOSTNAME" [email protected]

Requires a working mail setup on the server. Worth it for the peace of mind — you'll know instantly if someone else gets in.

Verify It Worked

Run this and confirm the output matches what you expect:

On the server
sshd -T | grep -E 'permitrootlogin|passwordauthentication|port'

You should see:

Expected output
port 47892
passwordauthentication no
permitrootlogin no

If the output doesn't match, your config has a syntax error or there's an override in a Match block somewhere below your settings. sshd -T shows the effective config after all Match blocks are resolved — it's the only way to know what sshd is actually running with. Don't trust the file alone.

💬 Comments