Why SSH hardening?
Raspberry Pi OS has enabled SSH by default since November 2016, requiring a password at first login. The default configuration allows password-based login. That means anyone who can reach port 22 on your Pi can try to guess their way in.
That might sound abstract. But scanner traffic on port 22 is constant. Shodan's data shows millions of SSH servers scanning each other. Shodan and similar systems will index your network before you finish setting it up, if your IP is exposed.
This is not paranoia. It is statistics. And it is easy to do something about.
Check SSH status
Confirm that SSH is running and port 22 is listening:
sudo systemctl status ssh
You should see active (running). Check which port is listening:
sudo ss -tlnp | grep 22
Output should show something like 0.0.0.0:22 or :::22. That confirms SSH is accepting connections on all interfaces.
Also check which OpenSSH version you are running:
ssh -V
Raspberry Pi OS Bookworm (the current release) ships OpenSSH 9.2. This matters because a couple of directive names changed in version 8.7.
Key-based login
This is the most important step. Key-based login is more secure than passwords in every way. An Ed25519 key pair is small, fast, and resistant to brute-force.
Run this on your local machine, not on the Pi:
ssh-keygen -t ed25519 -C "pi-homelab-2025"
The -C flag is a comment. Write something that makes sense to you. You will be asked for a passphrase. Use one. It encrypts your private key, making it useless even if someone copies the file.
The default location is ~/.ssh/id_ed25519 (private) and ~/.ssh/id_ed25519.pub (public). Copy the public key to the Pi:
ssh-copy-id -i ~/.ssh/id_ed25519.pub pi@[your-pi-ip]
This adds your public key to ~/.ssh/authorized_keys on the Pi. Test that it works before disabling password login:
ssh -i ~/.ssh/id_ed25519 pi@[your-pi-ip]
If it connects without a password prompt (or asks for your passphrase), you are ready for the next step.
Open the SSH configuration file:
sudo nano /etc/ssh/sshd_config
Find and change (or add) these lines:
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no instead of KbdInteractiveAuthentication no. Both work as aliases on newer versions, but be precise.
Restart SSH to apply the changes:
sudo systemctl restart ssh
Test again from your other terminal. Now try connecting without a key. It should be rejected.
Change the SSH port (optional)
Moving SSH from port 22 to something like 2222 is not a security measure in itself. A scanner that looks carefully will find your SSH regardless of which port it sits on. But it cuts down on scan noise significantly. Most automated bots only scan port 22 and give up if nothing is there.
In /etc/ssh/sshd_config:
Port 2222
Update your UFW rule before restarting SSH:
sudo ufw allow 2222/tcp
sudo ufw delete allow 22/tcp # remove the old rule afterwards
Remember to specify the port when connecting:
ssh -p 2222 pi@[your-pi-ip]
Or add it to ~/.ssh/config on your local machine so you do not have to type it every time:
Host my-pi
HostName [your-pi-ip]
User pi
Port 2222
IdentityFile ~/.ssh/id_ed25519
After that, ssh my-pi is all you need.
Disable root login
The root user exists on every Linux system and has the same username everywhere. That makes it an obvious target. Disable direct root login over SSH:
PermitRootLogin no
On Raspberry Pi OS, root login is typically already disabled or set to prohibit-password. Set it explicitly to no.
Allow only specific users
Restrict who can log in via SSH at all. The default Pi username is pi:
AllowUsers pi
If you created a different user for your homelab, use that name instead. To allow multiple users, separate with spaces: AllowUsers pi admin.
This directive blocks all other usernames, even if they exist on the system. It is an extra barrier on top of key authentication.
fail2ban
fail2ban monitors log files and blocks IP addresses that make too many failed login attempts. Even with key-based login enabled, it is a good addition. It keeps logs clean and stops persistent scanners.
Install:
sudo apt update && sudo apt install -y fail2ban
fail2ban ships with a default /etc/fail2ban/jail.conf. Do not edit that file directly. Instead, create a local override:
sudo nano /etc/fail2ban/jail.local
Add this content:
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 3
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = %(sshd_log)s
backend = %(sshd_backend)s
port = ssh with port = 2222 (or whichever port you chose). The ssh value looks up port 22 in /etc/services and only matches that port.
Start and enable fail2ban:
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
Check that the SSH jail is active:
sudo fail2ban-client status sshd
Output shows the number of active bans and the list of banned IP addresses. A fresh installation will show zero bans. That is fine. It changes quickly if your IP is exposed.
Remove a ban manually if you accidentally lock yourself out:
sudo fail2ban-client set sshd unbanip [ip-address]
UFW integration
If you have UFW enabled (see the UFW guide), add a rate-limiting rule on the SSH port:
sudo ufw limit ssh
This blocks IP addresses that attempt more than 6 connections per 30 seconds. It is not a replacement for fail2ban, but it filters the roughest scan traffic at the packet filtering layer, before it even reaches the SSH daemon.
Or restrict SSH to your LAN subnet only:
sudo ufw allow from 192.168.1.0/24 to any port 22
Adjust the subnet to match your network. Run ip route to find it. This is the most restrictive option, but only practical if you never need SSH from outside your LAN.
Complete /etc/ssh/sshd_config example
Here is a full configuration with all recommended settings. Adjust port and username to match your setup:
# /etc/ssh/sshd_config
# SSH hardening for Raspberry Pi / Debian-based Linux
# Change port to reduce scan noise (optional)
Port 2222
# Addressing
AddressFamily any
ListenAddress 0.0.0.0
ListenAddress ::
# Authentication
PermitRootLogin no
MaxAuthTries 3
MaxSessions 3
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PasswordAuthentication no
KbdInteractiveAuthentication no
# Legacy alias (OpenSSH < 8.7): ChallengeResponseAuthentication no
UsePAM yes
# Restrict access to specific users
AllowUsers pi
# Timeouts
LoginGraceTime 20
ClientAliveInterval 300
ClientAliveCountMax 2
# Disable unnecessary features
X11Forwarding no
PrintMotd no
AcceptEnv LANG LC_*
# SFTP subsystem (remove if you do not use it)
Subsystem sftp /usr/lib/openssh/sftp-server
sudo systemctl restart ssh. An active SSH session is not interrupted by the restart. But always test from a new connection before closing your existing session.
Verify
Check your SSH configuration for weak algorithms and outdated settings with ssh-audit:
# Install ssh-audit (Python-based)
pip3 install ssh-audit
# Run against your Pi
ssh-audit [your-pi-ip]
Alternatively via pipx to avoid system-wide Python conflicts:
pipx install ssh-audit
ssh-audit [your-pi-ip]
ssh-audit reports on weak encryption algorithms, outdated key exchange and potential configuration issues. You do not need a perfect score, but critical findings should be addressed.
Also check from another machine on the LAN with nmap:
nmap -p 22 [your-pi-ip]
# Or on a custom port:
nmap -p 2222 [your-pi-ip]
A correctly configured Pi with UFW active will show open on the allowed port and filtered on port 22 if you changed the port.
Explicitly test that password login is disabled:
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no pi@[your-pi-ip]
You should get Permission denied (publickey) or similar. If you get a password prompt, PasswordAuthentication no is not taking effect.
What SSH hardening does not do
SSH hardening protects the login layer. It does not replace network segmentation. An attacker already on your LAN can still reach port 22 with your UFW LAN rules open.
It does not replace a VPN for remote access. If you want SSH available from outside in a secure way, WireGuard is the right answer. Not port forwarding.
fail2ban and UFW rate limiting protect against brute-force. They do not protect against an attacker who has your private key. Keep your private key on the machine it belongs to. Use a passphrase.
Keep your Pi updated. SSH hardening means nothing if there is an unpatched system behind it. Run regularly:
sudo apt update && sudo apt upgrade -y
Frequently asked questions
What happens if I lock myself out?
Keep an active SSH session open while you test new rules. A running session is not interrupted when you restart the SSH service. As a fallback, connect a keyboard and monitor directly to the Pi and log in locally.
Should I change the SSH port from 22?
The port change does not remove the attack surface, but it eliminates 99% of automated scan noise from bots hammering port 22. A quick win with minimal risk. Combine it with key-based login and the port number barely matters.
Is Fail2ban necessary if I already use SSH keys?
Strictly speaking, no. An attacker without your private key cannot get in regardless of how many tries they make. Fail2ban cuts log noise and protects other services like Nginx and Nextcloud running on the same server. It is a cheap extra layer.
Sources
- sshd_config(5) – OpenBSD manual pages: authoritative reference for all sshd_config directives and default values
- CIS Benchmark for Debian Linux – Center for Internet Security: concrete SSH hardening recommendations with the rationale behind each setting
- fail2ban documentation – configuration reference: complete reference for jail.local, filters and banaction configuration