Last January, I sat down to review a server’s auth logs and felt a familiar knot in my stomach.
Over 50,000 failed SSH login attempts — in a single month. Bots methodically hammering port 22 with common credentials, dictionary wordlists, and leaked password databases. Just waiting for one mistake.
That audit changed how I think about SSH security. Not as a checkbox, but as a discipline. What follows are the nine hardening techniques I’ve since applied across dozens of production servers. Not theoretical guidelines — actual configurations with real, measurable outcomes.
1. Disable Root Login
Start here. Always.
In my logs, 98% of automated attacks target the root user directly. It’s the one account guaranteed to exist on every Linux system, with no lockout by default and full system access if compromised. Allowing root SSH is handing attackers a target they already know the name of.
# /etc/ssh/sshd_config
PermitRootLogin no
The alternative is a standard user with sudo privileges. This creates an audit trail, forces attackers to compromise two layers instead of one, and gives you visibility into who did what and when.
If automation requires elevated access, use the prohibit-password option instead of fully enabling root login:
PermitRootLogin prohibit-password
Before disabling root login, verify your sudo user works in a separate, live session. Getting locked out of a VPS at 2 AM is a formative — and entirely avoidable — experience.
2. Disable Password Authentication
After this single change, my failed authentication attempts dropped by 99.7%.
Password-based login is vulnerable to brute force, dictionary attacks, credential stuffing, and the universal human tendency to reuse weak passwords. SSH keys eliminate the guessing game entirely — there’s no credential to steal if authentication never involves one.
# /etc/ssh/sshd_config
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
Generate a modern key using Ed25519, which is faster, more compact, and more secure than RSA:
ssh-keygen -t ed25519 -a 100 -C "you@example.com"
Deploy it to your server:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
Always verify the key-based login works in a second terminal before disabling password auth. A sensible key management strategy to consider:
| Context | Approach |
|---|---|
| Work servers | Password-protected key |
| Personal servers | Dedicated key, separate from work |
| CI/CD pipelines | Restricted key with forced commands |
3. Move SSH Off Port 22
This one generates debate. Yes, it’s security through obscurity. No, obscurity alone is not security. But the numbers are hard to argue with:
- Port 22 → 50,000+ attacks per month
- Custom port → ~12 attacks per month
- Reduction → 99.98%
Most internet-facing attack bots scan port 22 exclusively. Moving to an obscure port above 1024 eliminates virtually all of that noise, making your real logs easier to read and reducing the actual attack surface against your rate-limiting rules.
Port 2849
Update your firewall before restarting SSH:
sudo ufw allow 2849/tcp
sudo ufw delete allow 22/tcp
sudo ufw reload
And again — test from a second open session first.
4. Rate-Limit Authentication Attempts
Even with keys required, it’s worth making brute-force attempts prohibitively slow. These directives transform an hours-long attack into a months-long one:
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 30
MaxStartups 10:30:60
MaxStartups 10:30:60 is particularly effective: it starts randomly dropping new unauthenticated connections once 10 are open, reaching 100% drop rate at 60. This throttles bots that open many connections in parallel.
5. Whitelist SSH-Capable Users
Access control should be explicit, not implicit. Only users with a legitimate operational need should be able to reach SSH at all.
AllowGroups sshusers admins
sudo groupadd sshusers
sudo usermod -aG sshusers alice
A tiered group structure keeps things clean:
sshusers— standard interactive accessadmins— privileged access, potentially with 2FA (see tip #9)DenyUsers— explicitly block compromised or deprecated accounts
6. Disable Features You Don’t Use
Every enabled SSH feature is an attack surface. Most servers don’t need X11 forwarding, TCP tunneling, or agent forwarding — and leaving them enabled is silent risk.
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
IgnoreRhosts yes
HostbasedAuthentication no
When exceptions are genuinely needed, scope them narrowly using Match blocks:
Match User developer
AllowTcpForwarding yes
PermitOpen localhost:5432 localhost:3306
Default locked. Explicit exceptions only.
7. Enforce Modern Cryptography
OpenSSH’s out-of-the-box defaults still permit legacy algorithms with known weaknesses. Explicitly specify only what you trust:
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512
PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512
While you’re at it, remove the legacy DSA host key entirely:
sudo rm /etc/ssh/ssh_host_dsa_key*
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
Clients that previously connected will get a host key warning once. That’s expected and not a problem.
8. Enable Verbose Logging and Automatic Banning
Good logging is your passive early-warning system. Noisy auth logs are the first sign something is wrong.
LogLevel VERBOSE
Logs land in:
- Ubuntu/Debian →
/var/log/auth.log - RHEL/Rocky Linux →
/var/log/secure
Pair this with fail2ban, which reads those logs and automatically bans repeat offenders at the firewall level:
sudo apt install fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 2849
maxretry = 3
bantime = 3600
Three failed attempts, one-hour ban, automatic. No manual intervention required.
9. Add Two-Factor Authentication for Privileged Access
For production systems, bastion hosts, or any internet-facing server — require a second factor on top of your SSH key. Even a compromised key is useless without the TOTP code.
AuthenticationMethods publickey,keyboard-interactive
sudo apt install libpam-google-authenticator
google-authenticator
Apply 2FA selectively using a Match block. Service accounts don’t need it; administrators do:
Match Group admins
AuthenticationMethods publickey,keyboard-interactive
Complete sshd_config — Production Baseline
Port 2849
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
UsePAM yes
AllowGroups sshusers admins
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 30
MaxStartups 10:30:60
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com
KexAlgorithms curve25519-sha256
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512
PubkeyAcceptedAlgorithms ssh-ed25519,rsa-sha2-512
LogLevel VERBOSE
Match Group admins
AuthenticationMethods publickey,keyboard-interactive
Validate and apply:
sudo sshd -t # syntax check — always do this first
sudo systemctl restart sshd
ssh -vvv user@server -p 2849 # verify from a second terminal
Closing Thoughts
SSH hardening isn’t about achieving an impenetrable system. It’s about raising the cost of an attack until your server is no longer worth the effort — and directing attackers toward softer targets.
None of these changes are difficult in isolation. The key is applying them deliberately, verifying each one before closing your current session, and layering them together. Each technique compounds the others.
Start with key authentication and root login today. Add rate limiting and fail2ban this week. The rest can follow incrementally.
Security compounds. So does negligence.