SSH is the front door to every Linux server. It is also the most commonly brute-forced service on the internet. Despite this, most production SSH configurations haven’t changed meaningfully in a decade: password authentication disabled (maybe), an RSA key pair generated once and copied everywhere, and fail2ban running with default settings.
That is not hardening. That is the bare minimum from 2015.
In this article, we’ll build a modern SSH security posture layer by layer, starting from basic brute-force protection and ending with certificate-based authentication that eliminates static keys entirely.
Layer 1: fail2ban — Blocking Brute-Force Attacks
fail2ban monitors log files for repeated authentication failures and dynamically creates firewall rules to ban offending IPs. It’s the simplest defense against automated SSH brute-force attacks.
Installation and Configuration
| |
Create a local configuration file (never edit the defaults directly — they get overwritten on updates):
| |
Key parameters:
| Parameter | Purpose | Recommended Value |
|---|---|---|
maxretry | Failed attempts before ban | 3 |
bantime | How long the IP stays banned | 24h for SSH |
findtime | Window to accumulate failures | 10m |
ignoreip | IPs that are never banned | Your management subnet |
banaction | Firewall backend | nftables-multiport |
Enable and start the service:
| |
Layer 2: IP Allowlisting
If you know which IPs need SSH access, restrict it at both the sshd and firewall level.
sshd-Level Restriction
The AllowUsers and Match directives in sshd_config can restrict access by source IP:
| |
This ensures that even if an attacker has valid credentials, they can only authenticate from the specified network.
Firewall-Level Restriction (nftables)
The firewall should be the primary enforcement point:
| |
Or with ufw for simpler setups:
| |
Cloud Provider Security Groups
On AWS, GCP, or Azure, the security group / firewall rule should be your first line of defense. Never leave port 22 open to 0.0.0.0/0 in a security group — restrict it to your VPN egress IPs or bastion host.
Layer 3: Modern Cryptographic Algorithms
The cryptographic defaults in older OpenSSH versions are dangerously permissive. A hardened configuration should explicitly specify which algorithms are acceptable.
Ed25519 Keys
Ed25519 is the recommended key type in 2026. It’s based on Curve25519, produces compact 256-bit keys, is faster to generate and verify than RSA, and offers fewer opportunities for side-channel attacks:
| |
Stop generating RSA keys. If you need backward compatibility with ancient systems, use RSA-4096 as a fallback — never RSA-2048.
Post-Quantum Key Exchange
OpenSSH 9.x defaults to sntrup761x25519-sha512@openssh.com as the key exchange algorithm. This is a hybrid that combines X25519 (classical elliptic curve) with NTRU Prime (post-quantum lattice-based). If either algorithm is broken, the other still provides security:
# Verify your default KEX on a connection
ssh -vv server.example.com 2>&1 | grep "kex: algorithm:"
# Expected: sntrup761x25519-sha512@openssh.com
Disabling Weak Algorithms
Explicitly restrict ciphers, MACs, and key exchange algorithms in sshd_config:
| |
| Algorithm Category | Allowed | Explicitly Removed |
|---|---|---|
| Key Exchange | sntrup761x25519-sha512, curve25519-sha256 | diffie-hellman-*, ecdh-sha2-* |
| Ciphers | chacha20-poly1305, aes256-gcm, aes128-gcm | aes*-cbc, 3des-cbc, aes*-ctr |
| MACs | hmac-sha2-512-etm, hmac-sha2-256-etm | hmac-sha1*, hmac-md5*, umac-* |
| Host Keys | Ed25519 | RSA, ECDSA, DSA |
Layer 4: sshd_config Hardening
Beyond cryptographic settings, several sshd options directly reduce attack surface:
| |
After any change, always validate the configuration before restarting:
| |
sshd_config will prevent the daemon from starting, locking you out of the server. Always keep an existing SSH session open while testing changes, and always run sshd -t before restarting.Layer 5: SSH Certificate-Based Authentication
This is where modern SSH security diverges from the traditional approach. Instead of distributing public keys to every server’s authorized_keys file, you use an SSH Certificate Authority (CA) to sign short-lived certificates.
The Problem with authorized_keys
The traditional SSH key model has fundamental operational problems:
| Problem | Impact |
|---|---|
| Keys are distributed to every server | Revoking a key means touching every authorized_keys file |
| Keys never expire | A compromised key works forever until manually removed |
| No identity metadata | The server sees a key, not a person — no audit trail |
| Key sprawl | Engineers accumulate keys across dozens of servers with no inventory |
How SSH Certificates Work
An SSH CA signs a user’s public key, producing a certificate with embedded metadata: identity, validity period, allowed principals, and extensions. The server trusts the CA, not individual keys.
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Engineer │────▶│ Vault (CA) │────▶│ SSH Server │
│ │ │ │ │ │
│ 1. Auth │ │ 2. Sign key │ │ 3. Verify │
│ via OIDC │ │ (60min │ │ cert vs │
│ │ │ TTL) │ │ trusted CA │
└──────────┘ └──────────────┘ └──────────────┘
Using HashiCorp Vault as SSH CA
Vault’s SSH secrets engine acts as a certificate authority, signing user keys on demand with configurable TTLs.
1. Enable the SSH secrets engine in Vault:
| |
2. Generate or configure the CA key pair:
| |
3. Retrieve the CA public key and deploy it to servers:
| |
4. Configure sshd to trust the CA:
| |
5. Create a Vault role defining certificate parameters:
| |
6. Engineers sign their key and connect:
| |
The certificate expires in 1 hour. No authorized_keys file is touched. The server’s auth log shows the certificate’s identity metadata, not just a key fingerprint.
Layer 6: VPN-Gated SSH Access
The most effective way to reduce SSH attack surface is to remove it from the public internet entirely. Place SSH behind a VPN so that only authenticated VPN users can even reach port 22.
Architecture with Pritunl or WireGuard
┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ Engineer │────▶│ VPN Gateway │────▶│ SSH Server │
│ │ │ (Pritunl / │ │ (port 22 │
│ WireGuard │ │ WireGuard) │ │ bound to │
│ tunnel │ │ │ │ 10.8.0.0/24) │
└──────────┘ └──────────────┘ └──────────────┘
Configure sshd to only listen on the VPN interface:
| |
With this setup, port 22 is unreachable from the public internet. Port scans see nothing. Brute-force attempts are structurally impossible because the TCP connection never completes. fail2ban becomes a safety net rather than a primary defense.
Pritunl supports integration with OIDC providers (including Authentik), so VPN access can be gated behind the same identity layer as your other services — with MFA, group policies, and centralized audit logging.
Layer 7: Port Knocking
Port knocking adds a layer of obscurity by keeping port 22 completely closed until a specific sequence of connection attempts is made to other ports. It’s not a security mechanism in isolation, but it eliminates noise from automated scanners.
Setup with knockd
| |
| |
Client-side usage:
| |
Complete Hardened sshd_config
Here’s a practical production configuration combining all the layers discussed above:
| |
After deploying, validate and restart:
| |
Layered Defense Summary
Each layer addresses a different threat vector. No single layer is sufficient on its own:
| Layer | Protects Against | Limitation |
|---|---|---|
| fail2ban | Brute-force attacks | Reactive; doesn’t help with valid credentials |
| IP allowlisting | Unauthorized source IPs | Doesn’t help if attacker is on allowed network |
| Modern algorithms | Cryptographic attacks, quantum harvest | Requires client compatibility |
| sshd hardening | Misconfiguration exploitation | Doesn’t prevent authorized user abuse |
| SSH certificates (Vault) | Key sprawl, stale access, missing audit trail | Requires Vault infrastructure |
| VPN-gated access | Public internet exposure | VPN becomes single point of entry |
| Port knocking | Automated scanning, log noise | Obscurity, not security |
The goal is to ensure that an attacker must defeat multiple independent layers to gain SSH access. Compromising one layer — a stolen VPN credential, a misconfigured firewall rule, a leaked certificate — should not be sufficient on its own.
Conclusion
SSH hardening in 2026 is not about picking the one right control. It’s about layering defenses so that each compensates for the weaknesses of the others. fail2ban handles brute-force noise. Firewall rules and VPN access eliminate public exposure. Modern cryptographic algorithms protect the channel. Certificate-based authentication via Vault eliminates the operational nightmare of static key management.
If you’re still distributing authorized_keys files and calling it secure, you’re a decade behind. Move to certificate-based authentication. Put SSH behind a VPN. Harden the daemon configuration. Treat every layer as necessary and none as sufficient.
References:
