
Harden SSH on Ubuntu with Keys, UFW, and Fail2Ban
Remote SSH is often the only practical way to administer a cloud server. That makes it both essential and a constant target for automated login attempts. A sensible baseline is layered: authenticate with a cryptographic key, limit network exposure with UFW, and use Fail2Ban to slow repeated failures.
This tutorial applies to current Ubuntu Server releases, including Ubuntu 24.04 LTS. It deliberately keeps port 22: changing the port may reduce log noise, but it is not an authentication control. The goal is a configuration you can test, understand, and recover from.
Before You Change Anything
The biggest risk during SSH hardening is locking yourself out. Keep your current SSH session open until a second terminal has successfully connected with the new configuration. If your provider offers a web console or serial console, confirm that it works before starting.
First update the package index and install the required components:
sudo apt update
sudo apt install openssh-server ufw fail2ban
Confirm the SSH service and note the current port:
sudo systemctl status ssh --no-pager
sudo sshd -T | grep '^port '
sshd -T prints the effective server configuration. It is more reliable than assuming that a line in one file wins, because Ubuntu loads both /etc/ssh/sshd_config and snippets from /etc/ssh/sshd_config.d/.
Create a Dedicated Administrative User
Do not use the root account for routine administration. If you do not already have a non-root account with sudo access, create one while your existing session is still open:
sudo adduser deploy
sudo usermod -aG sudo deploy
Replace deploy with your preferred username. Test local privilege escalation before depending on the account:
su - deploy
sudo -v
exit
The account password remains useful for sudo even after remote password authentication is disabled.
Generate and Install an Ed25519 Key
Run the following command on your own computer, not on the server:
ssh-keygen -t ed25519 -a 64 -C "deploy@production"
Accept a sensible file path and set a strong passphrase. The private key stays on your computer; the server receives only the public key. On Linux, macOS, or Windows with OpenSSH, copy it with:
ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@SERVER_IP
If ssh-copy-id is unavailable, securely append the contents of the .pub file to /home/deploy/.ssh/authorized_keys on the server. Then enforce the ownership and permissions OpenSSH expects:
sudo chown -R deploy:deploy /home/deploy/.ssh
sudo chmod 700 /home/deploy/.ssh
sudo chmod 600 /home/deploy/.ssh/authorized_keys
Open a second terminal and test the key explicitly:
ssh -i ~/.ssh/id_ed25519 deploy@SERVER_IP
Do not continue until this succeeds. For troubleshooting, add -vvv to the client command and inspect the server journal with sudo journalctl -u ssh -n 100 --no-pager.
Disable Risky SSH Authentication Paths
Ubuntu supports modular snippets under /etc/ssh/sshd_config.d/. OpenSSH uses the first value it reads for most directives, so use an early filename and always inspect the effective result.
Create /etc/ssh/sshd_config.d/00-local-hardening.conf:
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PermitEmptyPasswords no
You can create it safely with an editor:
sudoedit /etc/ssh/sshd_config.d/00-local-hardening.conf
These settings allow public-key login, reject password and keyboard-interactive login, and block direct root access. Before applying them, validate the syntax:
sudo sshd -t
No output means the syntax is valid. Next, verify that the intended values actually won:
sudo sshd -T | grep -E \
'^(pubkeyauthentication|passwordauthentication|kbdinteractiveauthentication|permitrootlogin) '
Expected output should show yes, no, no, and no respectively. If it does not, inspect the include order and other snippets:
grep -RInE '^(Include|PubkeyAuthentication|PasswordAuthentication|KbdInteractiveAuthentication|PermitRootLogin)' \
/etc/ssh/sshd_config /etc/ssh/sshd_config.d
Apply the configuration without terminating established sessions:
sudo systemctl reload ssh
Now open a third terminal and connect normally. Also verify that a password-only attempt is rejected:
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@SERVER_IP
Keep your original session open until both tests behave as expected.
Configure UFW Without Cutting Off SSH
UFW is Ubuntu's default host firewall frontend. Always allow SSH before enabling it:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw limit OpenSSH
sudo ufw enable
sudo ufw status verbose
ufw limit OpenSSH allows the SSH application profile and adds basic rate limiting. If SSH listens on a custom port, use the actual TCP port instead:
sudo ufw limit 2222/tcp
Open only the ports the machine serves. A typical public web server might also need:
sudo ufw allow 'Nginx Full'
Do not copy that rule onto a server that does not run Nginx. Firewall policy should describe the real workload, not a generic checklist.
For an internal server with a fixed management network, an even tighter rule is possible:
sudo ufw delete limit OpenSSH
sudo ufw allow from 203.0.113.0/24 to any port 22 proto tcp
Replace the documentation subnet with your real trusted CIDR. Avoid source-IP restrictions if administrators use changing residential or mobile addresses and have no recovery console.
Add Fail2Ban for Repeated Failures
Fail2Ban watches authentication failures and temporarily blocks sources that cross a threshold. It reduces repeated attempts, but it does not make weak credentials safe; key-only authentication remains the primary control.
Never edit the packaged jail.conf directly. Create /etc/fail2ban/jail.d/sshd.local:
[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 5
findtime = 10m
bantime = 1h
If SSH uses a nonstandard port, replace port = ssh with that number. Validate and start the service:
sudo fail2ban-client -t
sudo systemctl enable --now fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
The jail status should show the active filter and ban counters. To inspect recent activity:
sudo journalctl -u fail2ban --since '1 hour ago' --no-pager
If you accidentally ban your own address and still have console access, remove it with:
sudo fail2ban-client set sshd unbanip YOUR_IP
Do not intentionally generate many failed logins against a production host just to make the counter move. Configuration validation, service status, and a controlled test from an address you can recover are enough.
Troubleshooting Common Failures
The public key is rejected
Check the username, key path, and file permissions. Then review the authentication journal:
sudo namei -l /home/deploy/.ssh/authorized_keys
sudo journalctl -u ssh -n 100 --no-pager
On the client, ssh -vvv reveals which keys were offered. If an agent is presenting many keys, force the intended identity:
ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 deploy@SERVER_IP
Password login still works
Do not guess which file is responsible. Inspect the effective configuration:
sudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication) '
Search all loaded files for an earlier value, correct the ordering, run sudo sshd -t, and reload the service again.
Fail2Ban shows no sshd jail
Run sudo fail2ban-client -t and read the service journal. Confirm the jail filename ends in .local or .conf, enabled = true is present, and the SSH service is writing events to the systemd journal.
UFW blocks a required service
Use numbered rules so you can remove only the incorrect entry:
sudo ufw status numbered
sudo ufw delete RULE_NUMBER
Make changes through a provider console if the SSH path is already unavailable.
Production Checklist
- A non-root sudo user can log in using an encrypted private key.
- A separate terminal proves key login works after the SSH reload.
sudo sshd -treturns no error.sudo sshd -Tconfirms passwords, keyboard-interactive login, and root login are disabled.- UFW allows the actual SSH port before the firewall is enabled.
- Only required application ports are open.
- The Fail2Ban
sshdjail is active and its configuration test passes. - The provider console or another recovery path has been tested.
- Private keys are backed up securely, never copied to the server, and protected with passphrases.
- Ubuntu security updates are applied on a maintained schedule.
Official References
- Ubuntu Server: OpenSSH server
- Ubuntu Server: Firewall and UFW
- Fail2Ban official repository and documentation links
Key-only SSH, a default-deny firewall, and measured banning solve different parts of the remote-access problem. Used together—and tested before the original session is closed—they provide a strong, maintainable baseline without relying on obscurity.