# SSH hardening checklist for production servers

## TL;DR

Switch to key-only auth, disable password login and root login, restrict which users can connect, and add fail2ban for brute-force noise. Test a new session before closing your current one, because a bad sshd_config plus a closed session equals a lockout and a very bad afternoon.

```text
SSH hardening checklist for production servers
```

## Use this when

- You are setting up or auditing SSH on a production server
- Logs show brute-force attempts against sshd
- Compliance asks for an SSH hardening checklist with evidence
- You inherited a server and dont trust its sshd_config

## Not for this skill when

- You need VPN or WireGuard setup (different skill)
- You are debugging SSH connectivity in general, not hardening
- You need TLS certificates for web services
- You are provisioning the whole server from scratch

## Steps

### 1. Audit the current effective config

sshd_config files include drop-ins, so read the effective config, not just the file. Look for password auth, root login, and empty passwords.

```bash
sudo sshd -T | grep -Ei 'passwordauthentication|permitrootlogin|permitemptypasswords|pubkeyauthentication'
```

Expected: lines like `passwordauthentication yes` or `permitrootlogin prohibit-password`. This is your before snapshot; anything saying `yes` where you want `no` goes on the fix list.

### 2. Enforce key-only auth and lock down root

Write a drop-in config so your changes survive package updates to the main file. Key-only auth kills password brute-forcing outright; disabling root login shrinks the target.

```bash
printf 'PasswordAuthentication no\nPermitRootLogin no\nPermitEmptyPasswords no\nPubkeyAuthentication yes\n' | sudo tee /etc/ssh/sshd_config.d/hardening.conf
```

Expected: the drop-in file exists with those four lines. If you genuinely need root over SSH (you probably dont), use `prohibit-password` instead of `no`, and revisit that decision quarterly.

### 3. Restrict who can log in

Default-deny beats default-allow. List the humans and service accounts that need SSH; everyone else gets refused at the door.

```bash
printf 'AllowUsers deploy [your-username]\n' | sudo tee -a /etc/ssh/sshd_config.d/hardening.conf
```

Expected: the AllowUsers line appended. Double-check the usernames exist (`id [username]`) before you restart anything; a typo here locks out legitimate users.

### 4. Harden keys and add brute-force protection

Generate ed25519 keys (short, fast, modern) and install fail2ban so the background brute-force noise gets banned instead of just logged.

```bash
ssh-keygen -t ed25519 -f [HOME]/... -N "" && sudo apt-get install -y fail2ban && sudo systemctl enable --now fail2ban
```

Expected: a new keypair at that path, and fail2ban running (`sudo fail2ban-client status sshd` shows a jail). Copy the public key to the server's authorized_keys before you disable passwords.

### 5. Validate, restart, and test before closing your session

Check the config syntax, restart sshd, then open a NEW session while the old one is still alive. Only close the old session after the new one works.

```bash
sudo sshd -t && sudo systemctl restart sshd && ssh -i [HOME]/... [username]@YOUR_HOST 'echo SSH_OK'
```

Expected: `sshd -t` silent (valid), sshd restarts, and the new connection prints SSH_OK. If the new session fails, your old session is still open; fix the config and retry.

### Variant: sshd_config best practices

The full settings list people search for: Protocol 2 (default now), `X11Forwarding no`, `MaxAuthTries 3`, `LoginGraceTime 60`, `ClientAliveInterval 300`, `ClientAliveCountMax 2`, `AllowTcpForwarding` scoped or no. Apply via the same drop-in pattern and validate with `sshd -T`.

### Variant: disable password auth ssh

The minimal version of this skill: set `PasswordAuthentication no` plus `ChallengeResponseAuthentication no` (keyboard-interactive can otherwise still prompt), confirm your key works first, restart, done. This one change stops effectively all password brute-forcing.

### Variant: ssh brute force protection

If you cant change auth methods right now, fail2ban plus `MaxAuthTries 3` plus moving off port 22 cuts the noise dramatically. Moving ports is obscurity, not security, but it deletes 99 percent of automated attempts from your logs.

## Why this happens

SSH is the front door every bot on the internet knocks on, and password auth lets them knock millions of times. Key-only auth changes the game from "guessable secret" to "unforgeable credential," and everything else on the checklist (no root, limited users, fail2ban) is about shrinking what a compromised credential or a bug can reach.

## Edge cases and pitfalls

- Locking yourself out: always test a new session before closing the old one; keep a console (cloud provider web console) path as a backstop.
- Automation that uses passwords: CI deploy keys and scripts break when passwords go away; migrate them to keys first.
- `ChallengeResponseAuthentication` left on: keyboard-interactive can still prompt for passwords even with PasswordAuthentication off; disable both.
- AllowUsers typos: one wrong username and a legit user cant get in; verify with `id` first.
- fail2ban banning you: whitelist your office and CI IPs in the fail2ban config before enabling aggressive bans.
- Old clients: very old SSH clients may not speak ed25519; keep one RSA key around if you still manage legacy boxes.

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_EQM-clmB4tBcD_DmEn8Ijg
