Home › The Stash

Linux for Defenders 05: Lock Down SSH — Keys Only, No Root, and fail2ban

LabOctober 20, 2026

IntermediateTestedTime ~40 minTool OpenSSH, fail2banNeeds a Linux VM you own + KaliCost freeExam Linux+ · Security+ · CySA+

0 / 7 steps

The goal

By the end, your Linux VM will only accept SSH logins with a key, never as root, and never with a password — and anything that keeps guessing usernames will be banned automatically. You will read the settings that actually apply, make and install a key, harden SSH with one small file, avoid the file-order trap that silently undoes it, and then trip fail2ban yourself from Kali and watch the ban land.

Your own machines only, and keep a way back in. Work on the target VM from the isolated attack range, logging in from your Kali VM. Before you reload SSH in step 03, keep one SSH session open and test from a second one — if the new settings lock you out, the open session (or the VM console) gets you back in.

Why this lab. SSH is the front door of almost every Linux server, and anything on the internet with port 22 open gets a steady stream of login guesses within minutes. Passwords can be guessed; a key cannot be guessed in any useful time. Turning passwords off and banning the guessers closes the most common way into a Linux box.

Steven’s SSH key has no passphrase and is called id_rsa_old_DO_NOT_USE. He uses it daily. — Nyx

Before you start. You need the target VM with SSH running and a normal user who can sudo (the sudo lab made alex), plus Kali. Install fail2ban on the target with sudo apt install fail2ban. The examples use target IP 172.17.0.2; swap in yours.

Steps

01Read the settings that actually apply

Do not read the config file and guess. Ask the SSH server itself what it will do, after every file and default is combined.

$ sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries|x11forwarding) '
maxauthtries 6
permitrootlogin without-password
passwordauthentication yes
kbdinteractiveauthentication no
x11forwarding yes

That is a stock Ubuntu 24.04. Passwords are allowed. Root cannot log in with a password, but can with a key (without-password is the old name for prohibit-password). Each connection gets six tries. X11 forwarding, which lets remote graphical programs reach your desktop, is on.

Why this matters. sshd -T prints the effective configuration: the main file, every drop-in, and the built-in defaults, combined. It is the only honest answer to “are passwords off on this box?”, and you will use it again in steps 03 and 04.

02Make a key on Kali and install it

On Kali, create a key pair, copy the public half to your user on the target, and log in with it.

$ ssh-keygen -t ed25519 -C "you@kali"
Generating public/private ed25519 key pair.
Your identification has been saved in /home/kali/.ssh/id_ed25519
Your public key has been saved in /home/kali/.ssh/id_ed25519.pub
$ ssh-copy-id alex@172.17.0.2
Now try logging into the machine, with:   "ssh 'alex@172.17.0.2'"
and check to make sure that only the key(s) you wanted were added.
$ ssh alex@172.17.0.2 'whoami; ls -l ~/.ssh/authorized_keys'
alex
-rw------- 1 alex alex 90 Oct 20 19:11 /home/alex/.ssh/authorized_keys

When ssh-keygen asks for a passphrase, set one: it encrypts the private key, so a stolen copy of the file is useless on its own. ssh-copy-id uses your password one last time to append the public key to ~/.ssh/authorized_keys on the target. The final login uses the key, not the password.

Why this matters. Do this before step 03. Once passwords are off, a user without a key in authorized_keys cannot log in at all. The private key never leaves Kali; only the .pub half goes to the server.

03Harden SSH with one drop-in file

On the target, put your settings in their own file instead of editing the main config. Check it, reload, and ask the server again.

$ sudo tee /etc/ssh/sshd_config.d/10-hardening.conf >/dev/null <<'EOF'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
MaxAuthTries 3
X11Forwarding no
EOF
$ sudo sshd -t && echo "config OK"
config OK
$ sudo systemctl reload ssh
$ sudo sshd -T | grep -E '^(permitrootlogin|passwordauthentication|kbdinteractiveauthentication|maxauthtries|x11forwarding) '
maxauthtries 3
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
x11forwarding no

All five changed. sshd -t checks the syntax before you reload, so a typo cannot take SSH down. reload applies the new settings to new connections and leaves your open session alone — that is your safety net. Now open a second session from Kali to prove you can still get in before you close the first.

Why a drop-in. The main file, /etc/ssh/sshd_config, belongs to the package and can be replaced on upgrade. A file of your own in sshd_config.d/ survives upgrades, says in one place what you changed, and is easy to remove.

04Dodge the file-order trap

Many cloud and VM images ship their own drop-in, often 50-cloud-init.conf, that turns passwords back on. See what happens if your file sorts after it.

$ ls /etc/ssh/sshd_config.d/
10-hardening.conf
50-cloud-init.conf
$ cat /etc/ssh/sshd_config.d/50-cloud-init.conf
PasswordAuthentication yes
$ sudo mv /etc/ssh/sshd_config.d/10-hardening.conf /etc/ssh/sshd_config.d/60-hardening.conf
$ sudo sshd -T | grep '^passwordauthentication '
passwordauthentication yes
$ sudo mv /etc/ssh/sshd_config.d/60-hardening.conf /etc/ssh/sshd_config.d/10-hardening.conf
$ sudo sshd -T | grep '^passwordauthentication '
passwordauthentication no

Named 60-, your file loses and passwords are back on, with no error anywhere. Named 10-, it wins. SSH reads the drop-ins in name order, and for most settings the first value it sees is the one it keeps — the opposite of what most people expect. (This test VM had no cloud-init file, so we created one to show the trap; check whether yours does with the ls above.)

Why this matters. This is how “we disabled password logins” turns out to be untrue in an audit. Low number for your file, then trust sshd -T, not the file you edited.

05Prove what is refused

From Kali, try the three doors: a password login, a root login, and your key.

$ ssh -o PubkeyAuthentication=no alex@172.17.0.2
alex@172.17.0.2: Permission denied (publickey).
$ ssh root@172.17.0.2
root@172.17.0.2: Permission denied (publickey).
$ ssh alex@172.17.0.2 whoami
alex

The first line forces a password attempt and is refused before you are even asked for one: (publickey) is the server listing the only method it now accepts. Root is refused outright. Your key still works.

The attacker’s view. A password guesser now gets nothing to guess at — the server never offers a password prompt. The defender’s move: this one setting does more against brute force than any password policy. fail2ban, next, deals with the noise that keeps coming anyway.

06Switch on fail2ban for SSH

fail2ban watches the SSH log and bans any address that fails too often. Set your own limits in jail.local, restart it, and check the jail.

$ sudo tee /etc/fail2ban/jail.local >/dev/null <<'EOF'
[sshd]
enabled  = true
maxretry = 3
findtime = 10m
bantime  = 10m
EOF
$ sudo systemctl restart fail2ban
$ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- Journal matches:	_SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

Three failures within ten minutes earns a ten-minute ban. On Ubuntu 24.04 the package already enables the SSH jail and reads the systemd journal, so jail.local only tightens the numbers. Put your changes in jail.local, never jail.conf, which an upgrade can overwrite.

Why this matters. With passwords off, guessing cannot succeed, but it still fills your logs and costs CPU. Banning repeat offenders keeps the log readable, so a real problem stands out.

07Trip it from Kali, then unban

Play the guesser. From Kali, try four common usernames, then check the jail on the target.

$ for u in admin oracle test ubuntu; do ssh -o BatchMode=yes $u@172.17.0.2 true; done
admin@172.17.0.2: Permission denied (publickey).
oracle@172.17.0.2: Permission denied (publickey).
test@172.17.0.2: Permission denied (publickey).
ubuntu@172.17.0.2: Permission denied (publickey).
$ sudo fail2ban-client status sshd | tail -3
   |- Currently banned:	1
   |- Total banned:	1
   `- Banned IP list:	172.17.0.1
$ ssh alex@172.17.0.2
ssh: connect to host 172.17.0.2 port 22: Connection refused

Kali’s address is banned, and now even your valid key gets Connection refused: a ban blocks the address, not the user. The SSH log shows what fail2ban counted, and the ban itself is an nftables rule:

$ sudo journalctl -u ssh -n 6 --no-pager -o short
Oct 20 19:12:17 labvm sshd[2970]: Connection closed by invalid user admin 172.17.0.1 port 33972 [preauth]
Oct 20 19:12:17 labvm sshd[2972]: Invalid user oracle from 172.17.0.1 port 33978
Oct 20 19:12:17 labvm sshd[2972]: Connection closed by invalid user oracle 172.17.0.1 port 33978 [preauth]
Oct 20 19:12:17 labvm sshd[2974]: Invalid user test from 172.17.0.1 port 33980
Oct 20 19:12:17 labvm sshd[2974]: Connection closed by invalid user test 172.17.0.1 port 33980 [preauth]
Oct 20 19:12:17 labvm sshd[2976]: Connection closed by authenticating user ubuntu 172.17.0.1 port 33988 [preauth]
$ sudo nft list table inet f2b-table
table inet f2b-table {
	set addr-set-sshd {
		type ipv4_addr
		elements = { 172.17.0.1 }
	}

	chain f2b-chain {
		type filter hook input priority filter - 1; policy accept;
		tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
	}
}
$ sudo fail2ban-client set sshd unbanip 172.17.0.1
1

The log separates Invalid user (the name does not exist) from authenticating user ubuntu (it does — Ubuntu images ship that account). The unban returns 1 for one address removed, and your key works again.

The defender’s move. Username guessing is MITRE ATT&CK T1110 (brute force). A real log full of Invalid user from one address is that pattern; the Splunk SSH lab hunts it at scale. If you ban yourself on a real server, unban from the console or another address — and consider an ignoreip line in jail.local for your admin network.

What you learned

  • sshd -T — the effective SSH settings; trust it over the file you edited
  • keys first, then passwords off — ssh-keygen, ssh-copy-id, then a drop-in with PasswordAuthentication no and PermitRootLogin no
  • sshd -t, then reload, with a session kept open — change SSH without locking yourself out
  • first value wins — a low-numbered drop-in beats 50-cloud-init.conf; a high-numbered one silently loses
  • fail2ban — settings in jail.local, fail2ban-client status sshd, and unbanip
  • Invalid user from one address — username guessing (T1110), read straight from the SSH log

On Red Hat (RHEL, Rocky, Fedora). Checked on Rocky Linux 9: the service is sshd, not ssh (sudo systemctl reload sshd), and the image ships 50-redhat.conf in the same drop-in folder, so the same low-number rule applies. Defaults matched Ubuntu: passwords on, root by key only. SSH logs go to /var/log/secure as well as the journal. fail2ban is not in the base repositories; it comes from EPEL (sudo dnf install epel-release, then sudo dnf install fail2ban). If you move SSH to another port, SELinux has to be told too (semanage port).

Put it on your resume. Hardened OpenSSH on Linux (key-only authentication, no root login, reduced auth tries) with a verified drop-in configuration, and deployed fail2ban to automatically ban hosts after repeated failed logins. Change it to match what you actually did.

Tried it? Tell us

Nova built this lab from Steven’s ideas, and every command above was run on a real machine before it went up. If a step behaved differently for you, or you know a better way, tell us. Ideas for the next lab are welcome too — no promises on timing.

NextLinux for Defenders 06 — Packages and updates you can trust→

Go deeper

Original walkthrough. Every command and output block above was run on Ubuntu 24.04 (OpenSSH 9.6p1, fail2ban 1.0.2) with the client on a second machine on the same lab network, and the Red Hat note checked on Rocky Linux 9.8. Home-directory paths and timestamps will differ on yours. If something does not match your machine, tell me and I’ll fix it.