> ## Content Index
> Fetch the complete content index at: https://blog.sudowl.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# SSH Hardening Checklist for Ubuntu Servers
- URL: https://blog.sudowl.com/ssh-hardening-ubuntu-checklist/
- Published: 2026-04-17T10:00:00.000Z
- Updated: 2026-10-11T08:47:37.000Z
- Description: Lock down SSH on Ubuntu 24.04 without locking yourself out: keys only, no root, the cloud-init drop-in trap, socket activation, verification.
- Author: Lukas
- Tags: Linux, Security, Ubuntu

An SSH port on a public IP gets login attempts within minutes of the box coming up. That's not a threat in itself — it's background noise — but it means password logins and root logins are a bet you don't need to make. This is the checklist I go through on every new Ubuntu server. It's short, and almost all of it boils down to: keys only, no root, and actually verify the config is what you think it is.

**TL;DR:** Key auth working first, then one drop-in file, test, reload.

```bash
# on your machine
ssh-keygen -t ed25519 -C "you@laptop"
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@server

# on the server
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf    # contents below
sudo sshd -t && sudo systemctl reload ssh
sudo sshd -T | grep -Ei 'passwordauth|permitroot|kbdinteractive'

```

Keep your current session open the whole time. Test with a *new* terminal before closing anything.

## Step 0: a sudo user with a key

Don't harden anything until you can log in as a non-root user with a key and that user can `sudo`. If you're still on root with a password, set that up first — [user management with adduser, sudo and SSH keys](/linux-user-management-adduser-sudo-ssh-keys/) walks through it. Ed25519 keys are short, fast, and supported by every OpenSSH version you'll meet. No reason to generate RSA in 2026 unless some ancient client forces you.

Check the key login actually works, from a second terminal:

```bash
ssh -i ~/.ssh/id_ed25519 admin@server 'sudo -v && echo ok'

```

## The drop-in file

Ubuntu's `/etc/ssh/sshd_config` includes `/etc/ssh/sshd_config.d/*.conf` at the very top. I don't edit the main file at all — package updates like to ask about it — and put everything in a drop-in:

```text
# /etc/ssh/sshd_config.d/00-hardening.conf
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
AuthenticationMethods publickey
PermitEmptyPasswords no

MaxAuthTries 3
LoginGraceTime 30
MaxStartups 10:30:60

X11Forwarding no
AllowAgentForwarding no

ClientAliveInterval 300
ClientAliveCountMax 2

AllowUsers admin

```

What matters here:

- **`PermitRootLogin no`** — log in as yourself, `sudo` when needed. If you have automation that genuinely needs root over SSH, `prohibit-password` allows root with a key only. I'd still rather not.
- **`PasswordAuthentication no`** \+ **`KbdInteractiveAuthentication no`** — both, not just the first one. Keyboard-interactive can also end up asking for a password through PAM. (`ChallengeResponseAuthentication` is the old, deprecated name for the second one.)
- **`AuthenticationMethods publickey`** — belt and braces: only public key, nothing else, no matter what other settings say.
- **`MaxAuthTries 3`** — fewer guesses per connection. Careful if you have many keys in your agent: SSH tries them one after another and you may get `Too many authentication failures` before it reaches the right one. Fix that on the client with `IdentitiesOnly yes` and an `IdentityFile` per host in `~/.ssh/config`.
- **`MaxStartups 10:30:60`** — throttles concurrent unauthenticated connections. Starts dropping 30% of new ones above 10, all above 60.
- **`ClientAliveInterval` / `CountMax`** — dead sessions get cleaned up after about 10 minutes of no response.
- **`AllowUsers`** — only these accounts can log in over SSH. Everyone else is rejected, even with a valid key. Space-separated, and `user@10.0.0.*` patterns work for source restrictions.

I leave `AllowTcpForwarding` alone because I use SSH tunnels to reach internal dashboards. If you don't, set it to `no` too.

## Gotcha: the cloud-init file wins

This is the one that makes people think SSH hardening "doesn't work". For most `sshd_config` keywords, **the first value sshd reads wins**, not the last. Drop-in files are read in alphabetical order. Cloud images on Ubuntu often ship a file like `/etc/ssh/sshd_config.d/50-cloud-init.conf` containing:

```text
PasswordAuthentication yes

```

If you name your file `99-hardening.conf`, the `50-` file is read first, its value sticks, and your `PasswordAuthentication no` is ignored. Silently. That's why mine is `00-hardening.conf`. Check what's there on your box:

```bash
ls /etc/ssh/sshd_config.d/
grep -r . /etc/ssh/sshd_config.d/

```

And never trust the file — trust `sshd -T`, which prints the effective config:

```bash
sudo sshd -T | grep -Ei 'passwordauthentication|permitrootlogin|kbdinteractive|allowusers|maxauthtries'

```

If that says `passwordauthentication yes`, something earlier is overriding you.

## Gotcha: socket activation on Ubuntu 24.04

Since Ubuntu 22.10, SSH is socket-activated by default: `ssh.socket` listens on the port and starts `ssh.service` on demand. For normal config changes `systemctl reload ssh` is fine. But **port and listen address** are now handled by the socket unit, generated from your `sshd_config` at boot / daemon-reload. If you change `Port` or `ListenAddress`:

```bash
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -tlpn | grep ssh

```

Skip that and sshd keeps listening on the old port while you wonder why the new one is dead. Debian 12 and 13 don't use socket activation by default — there it's just `systemctl restart ssh`.

About changing the port at all: it's not security. It cuts log noise a lot, which is nice, but anything targeted finds it in one scan. If you change it, open the new port in the firewall *first* — [UFW rules that don't lock you out](/ufw-firewall-rules-ubuntu/) has the safe order.

## Verify from the outside

Config looks right? Prove it. From your machine:

```bash
# should fail with: Permission denied (publickey).
ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password admin@server

# should fail — root is not allowed
ssh root@server

# should work
ssh admin@server

```

If the first command gives you a password prompt, password auth is still on. Go back to `sshd -T`.

## Gotcha: locking yourself out

The rules I don't break:

1. Keep the existing session open. A reload doesn't kill established sessions.
2. Always `sudo sshd -t` before reloading. It prints nothing if the config is valid, and an error with the line number if not.
3. Test the login in a second terminal before closing the first.
4. Know where the provider's web/VNC console is *before* you need it.

`AllowUsers` deserves a special mention: a typo in your own username there and you're out. Same for `AuthenticationMethods` with a method your client doesn't offer.

## Beyond the config file

These aren't sshd settings, but they belong on the checklist:

- **Rate-limit and ban.** Key-only auth makes brute force pointless, but the log noise stays. [fail2ban](/fail2ban-setup-ssh-nginx/) or CrowdSec cleans that up.
- **Don't expose SSH at all.** The strongest option: SSH only on a VPN interface. That's the setup in my [WireGuard post](/wireguard-vpn-server-ubuntu/), or use Tailscale if you don't want to manage keys yourself.
- **Firewall.** Allow 22 (or your port) from where you actually connect, if that's a fixed IP or range.
- **Updates.** OpenSSH gets security fixes. [Unattended upgrades](/ubuntu-automatic-updates-unattended-upgrades/) take care of that without you thinking about it.
- **Audit.** `sudo apt install ssh-audit` and run `ssh-audit localhost`. On current Ubuntu and Debian the default algorithms are already decent; it'll mostly point out legacy stuff that's still allowed for compatibility.

## Watch the logs

```bash
sudo journalctl -u ssh -f
sudo journalctl -u ssh --since today | grep -c 'Invalid user'

```

On a public IP you'll see a steady stream of `Invalid user` and `Connection closed by authenticating user` lines. That's fine. That's the internet. What you want to see is that none of them get anywhere — and with keys only, no root and an `AllowUsers` list, they won't.

This checklist is one step in my [initial server setup](/ubuntu-server-initial-setup-checklist/) — I do it right after creating the user and before installing anything else.

---

### Related posts

- [Restic Backups to S3 / MinIO — Encrypted, Deduplicated, Restore-Tested](https://blog.diengdoh.com/restic-backup-s3-minio-encrypted/?ref=blog.sudowl.com)
- [Netdata on Ubuntu — Live Server Metrics in 5 Minutes](https://blog.diengdoh.com/netdata-monitoring-ubuntu/?ref=blog.sudowl.com)
- [WireGuard VPN on Ubuntu — the Setup I Actually Use](https://blog.diengdoh.com/wireguard-vpn-server-ubuntu/?ref=blog.sudowl.com)