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:

ContextApproach
Work serversPassword-protected key
Personal serversDedicated key, separate from work
CI/CD pipelinesRestricted 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 access
  • admins — 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.