Bitdoze Logo

How to Secure an SSH Server in Linux: Hardening Guide

Secure your Linux SSH server with this hardening guide. Covers key-based auth, sshd_config best practices, fail2ban, firewall rules, and OpenSSH security.

DragosDragos36 min read
How to Secure an SSH Server in Linux: Hardening Guide

Secure Shell (SSH) is how most of us manage remote Linux servers, and it’s the first thing attackers probe when your VPS comes online. If you’re running a Linux server, SSH hardening isn’t optional. Unsecured SSH leads to unauthorized access, data breaches, and compromised infrastructure.

This guide takes you from a default sshd_config to a production-hardened setup. Every recommendation is based on current OpenSSH behavior (10.x as of 2025), not outdated blog posts that still reference protocol 1 or deprecated directives.

If you need a server to practice on, Hetzner Cloud offers affordable VPS instances starting at ~€4/month, good enough for testing everything in this guide.

regreSSHion (CVE-2024-6387)

If you’re running OpenSSH 8.5p1 through 9.7p1, you had a critical unauthenticated remote code execution vulnerability (regreSSHion, discovered by Qualys). It was fixed in OpenSSH 9.8 (July 2024). Update your SSH server immediately before doing anything else: sudo apt update && sudo apt install openssh-server (or sudo dnf update openssh-server on RHEL/Fedora).

Understanding SSH

SSH is a cryptographic protocol for secure remote access over an unsecured network. It replaced Telnet and rsh in the mid-1990s and has become the standard for remote server management on Linux, BSD, and macOS.

SSH operates on a client-server model:

  1. The client initiates a connection to the SSH server
  2. The server sends its public key to the client
  3. The client verifies the server’s identity (or prompts you to accept it)
  4. A secure encrypted channel is established
  5. User authentication takes place (public key, password, etc.)
  6. Upon successful authentication, the session begins
Component Description
SSH Server The program (sshd) running on the remote machine that listens for incoming connections
SSH Client The program (ssh) used to connect to an SSH server
Encryption Algorithms used to secure the communication channel (key exchange, ciphers, MACs)

Modern OpenSSH (7.6+, released 2017) supports only SSH protocol version 2. Protocol 1 was removed entirely, there is no Protocol directive to configure. You’re already using protocol 2.

If you manage Linux servers regularly, you’ll want a solid grasp of essential Linux commands alongside SSH.

Basic SSH Server Security Measures

These are the quick wins, the things every operator should do on a new server before anything else.

Disable Root Login

Allowing direct root login via SSH gives attackers half the battle. They only need to crack one password or find one key to own the entire system.

Edit /etc/ssh/sshd_config (or use a drop-in file, more on that later):

PermitRootLogin no

Test the config before restarting:

sudo sshd -t
sudo systemctl restart sshd

Verify it took effect:

sudo sshd -T | grep permitrootlogin

Expected output: permitrootlogin no

Keep an active session open

Before restarting sshd after any config change, always keep one SSH session open. Test the new config from a second terminal. If you lock yourself out, the open session is your lifeline. For remote servers, know where your provider’s web console is (Hetzner, DigitalOcean, and Vultr all offer one).

Use Key-Based Authentication (Not Passwords)

Key-based authentication is the single most important SSH security improvement you can make. Keys can’t be brute-forced the way passwords can.

Generate an Ed25519 key pair on your local machine:

ssh-keygen -t ed25519 -a 100

Since OpenSSH 9.5 (Oct 2023), ssh-keygen generates Ed25519 keys by default. You can omit -t ed25519 on modern systems. For legacy systems that don’t support Ed25519, use ssh-keygen -t rsa -b 4096.

Copy the public key to your server:

ssh-copy-id user@server_ip

Test the login. You should connect without a password prompt:

ssh user@server_ip

For more on generating and using SSH keys with specific services, see this guide on linking GitHub with SSH keys.

Disable Password Authentication

Once key-based auth is working, disable passwords entirely. Set these in your sshd_config or drop-in file:

PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes

Verify:

sudo sshd -T | grep -i passwordauth

Expected output: passwordauthentication no

Change the Default SSH Port (Optional)

Changing the SSH port is optional. It reduces automated scan noise but doesn’t stop targeted attacks. The measures above (key auth, no root login) matter far more. There are real operational costs: firewall rules, scripts, team muscle memory all need updating. I’d skip it for single-server setups and consider it only if you’re seeing excessive log spam.

If you do change the port, update your firewall rules before restarting sshd:

# In /etc/ssh/sshd_config
Port 2222
# Update firewall first
sudo ufw allow 2222/tcp
sudo ufw deny 22/tcp   # optional: close the old port
sudo sshd -t && sudo systemctl restart sshd

Test from a new terminal before closing your current session:

ssh -p 2222 user@server_ip

Advanced SSH Hardening Techniques

Beyond the quick wins, these settings tighten your SSH server against more sophisticated attacks.

Limit User Access with AllowUsers / AllowGroups

Restrict which users can log in via SSH:

# Only these users can SSH in
AllowUsers admin deploy

# Or restrict by group
AllowGroup sshusers

If AllowUsers is set, all other users are denied by default. You can use wildcards: AllowUsers admin* deploy*.

Practical Match blocks let you apply rules per-user or per-address:

Match User git
    ForceCommand internal-sftp
    AllowTcpForwarding no

Match Address 192.168.1.0/24
    PasswordAuthentication yes

Verify:

sudo sshd -T | grep -i allowusers

Implement Two-Factor Authentication (2FA)

2FA adds a TOTP code on top of key-based authentication. An attacker would need both your private key AND your phone.

ChallengeResponseAuthentication is deprecated since OpenSSH 8.7 (2021). Use KbdInteractiveAuthentication instead. It’s the same directive under a new name.

1. Install Google Authenticator:

sudo apt-get update
sudo apt-get install libpam-google-authenticator

2. Configure PAM. Edit /etc/pam.d/sshd and add:

auth required pam_google_authenticator.so

3. Update SSH config. In /etc/ssh/sshd_config (or a drop-in like sshd_config.d/90-2fa.conf):

KbdInteractiveAuthentication yes
UsePAM yes
AuthenticationMethods publickey,keyboard-interactive

The AuthenticationMethods publickey,keyboard-interactive line requires both a key AND a 2FA code. Stronger than allowing either one alone.

4. Set up the authenticator for each user:

google-authenticator

Follow the prompts to scan the QR code and save the emergency scratch codes.

5. Restart and test:

sudo sshd -t && sudo systemctl restart sshd

Test from another terminal. You should be prompted for your key passphrase and then the TOTP code. If you hit issues, see troubleshooting SSH authentication failures.

Configure Idle Timeout Correctly

Idle timeout disconnects inactive sessions, useful if someone walks away from a terminal.

Breaking change in OpenSSH 8.2

The original version of this article recommended ClientAliveCountMax 0. Since OpenSSH 8.2 (Feb 2020), this disables connection killing entirely. It does NOT cause immediate termination. Use ClientAliveCountMax 1 instead.

Correct configuration for a 5-minute timeout:

ClientAliveInterval 300
ClientAliveCountMax 1

This sends a keepalive probe every 300 seconds. If the client doesn’t respond to one probe, the connection is terminated (total ~5 minutes).

Desired Timeout ClientAliveInterval ClientAliveCountMax
5 minutes 300 1
10 minutes 600 1
15 minutes 900 1
30 minutes 1800 1

Enforce Authentication Limits

Two directives that limit brute-force effectiveness per connection:

MaxAuthTries 3
LoginGraceTime 30s
  • MaxAuthTries 3 (default: 6), limits auth attempts per connection
  • LoginGraceTime 30s (default: 120s), the window a client has to authenticate after connecting. OpenSSH 9.9+ adds random jitter (up to 4s) to this value.

Both go in the drop-in config shown in the next section.

sshd_config Hardening Best Practices

This is the section you’ll bookmark and return to. A single drop-in config file that covers all the hardening directives in one place.

Use sshd_config.d/ for Clean Configuration

Modern distributions (Ubuntu 22.04+, Debian 12+, RHEL 9+) support /etc/ssh/sshd_config.d/*.conf for configuration snippets. This is the preferred way to harden without touching the main config file.

Create a drop-in file with all your hardening settings:

sudo cat > /etc/ssh/sshd_config.d/90-hardening.conf << 'EOF'
# Authentication
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30s

# Idle timeout (5 min)
ClientAliveInterval 300
ClientAliveCountMax 1

# Disable forwarding if not needed
AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no

# Minimum RSA key size
RequiredRSASize 3072

# Crypto hardening (verify with: ssh -Q kex/cipher/mac)
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr
MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com
EOF

Test and restart:

sudo sshd -t && sudo systemctl restart sshd

Always test before restarting

Run sudo sshd -t before sudo systemctl restart sshd. This catches syntax errors that would prevent sshd from starting and lock you out. Keep one SSH session open while you test from another.

Restrict Key Exchange, Ciphers, and MACs

Explicit algorithm lists remove weak defaults. The list above is based on sshaudit.com recommendations. Before setting these, verify what your OpenSSH build supports:

ssh -Q kex        # Key exchange algorithms
ssh -Q cipher     # Ciphers
ssh -Q mac        # MAC algorithms
ssh -Q key        # Key types
ssh -Q sig        # Signature algorithms

If a client can’t connect after you restrict algorithms, check both sides. The client needs to support at least one algorithm in your server’s list. For SSH tunneling use cases, see SSH port forwarding and tunneling for context on what forwarding controls affect.

Set a Minimum RSA Key Size (RequiredRSASize)

RequiredRSASize 3072 (available since OpenSSH 9.1) rejects RSA keys shorter than 3072 bits. This protects against weak keys from older clients or legacy automation scripts.

If you’re still using Ed25519 keys (recommended), this directive has no effect on your primary keys, but it catches weak RSA keys from other sources.

Disable Unnecessary Forwarding

If your server doesn’t need SSH tunneling, agent forwarding, or X11, disable them:

AllowTcpForwarding no
AllowAgentForwarding no
X11Forwarding no

Or use the single option: DisableForwarding yes. This disables all forwarding features at once. Only do this if you don’t need SSH port forwarding and tunneling.

Test Your Config Before Restarting

This can’t be stressed enough:

# Test syntax (catches errors before they lock you out)
sudo sshd -t

# Dump effective config (shows values after all Match blocks and includes)
sudo sshd -T | grep -E 'permitrootlogin|passwordauth|pubkeyauth|maxauthtries'

sshd -T shows the actual running configuration, not just what’s in the file. This is how you verify that Match blocks and drop-in files are applying correctly.

Firewall Configuration for SSH

Network-level access control is your outermost defense layer.

Using ufw (Ubuntu/Debian)

ufw is the recommended firewall for Ubuntu and Debian:

sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp    # or your custom port
sudo ufw enable
sudo ufw status verbose

If you changed your SSH port, replace 22 with your custom port number.

Using firewalld (RHEL/Fedora)

firewalld is the recommended firewall for RHEL, Fedora, and CentOS:

sudo firewall-cmd --permanent --add-service=ssh
sudo firewall-cmd --reload
sudo firewall-cmd --list-all

Using iptables (Advanced)

iptables gives you fine-grained control but requires more care:

# Allow established connections
iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Allow SSH (adjust port if necessary)
iptables -A INPUT -p tcp --dport 22 -j ACCEPT

# Rate-limit new connections (max 4 per minute)
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --set
iptables -A INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 60 --hitcount 4 -j DROP

# Drop everything else
iptables -A INPUT -j DROP

Docker and Firewall Bypass Warning

Docker bypasses your firewall

Docker manipulates iptables directly, potentially exposing container ports even if ufw or firewalld blocks them. If you run Docker on the same server as SSH, read the dedicated guide on Docker bypassing firewall rules.

After configuring your firewall, verify the SSH port is reachable: check remote port connectivity using nc or nmap.

For server-level intrusion prevention beyond firewalls, consider CrowdSec as a complement to your firewall rules.

Brute-Force Protection: fail2ban & PerSourcePenalties

You need layers of brute-force defense. OpenSSH 9.8+ has a built-in system, and fail2ban adds more flexibility.

Built-in Brute-Force Protection with PerSourcePenalties (OpenSSH 9.8+)

OpenSSH 9.8 (July 2024) introduced PerSourcePenalties, a built-in penalty system that tracks and delays misbehaving source IPs. It’s on by default.

Default configuration:

PerSourcePenalties crash:90s authfail:30s noauth:10s
PerSourcePenaltyExemptList 192.168.0.0/16
PerSourceNetBlockSize 32:128

How it works: IPs that trigger authentication failures get progressively longer delays before sshd responds. Crashes trigger a 90-second penalty, auth failures 30 seconds.

PerSourcePenalties is on by default in OpenSSH 9.8+. If your server is behind a NAT gateway or reverse proxy, all clients may appear to come from the same IP. Tune PerSourcePenaltyExemptList to exempt your NAT range, or you’ll penalize all clients when one misbehaves.

Setting Up fail2ban for SSH

fail2ban adds value beyond PerSourcePenalties: longer ban durations, email alerts, custom actions, and multi-service protection (not just SSH).

Create /etc/fail2ban/jail.local (don’t edit jail.conf directly — it gets overwritten on updates):

[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 3
bantime = 3600
findtime = 600
  • backend = systemd uses journald instead of tailing /var/log/auth.log — more reliable on modern distros
  • maxretry = 3 — three failed attempts triggers a ban
  • bantime = 3600 — ban lasts 1 hour
  • findtime = 600 — the window for counting failures (10 minutes)

Start fail2ban:

sudo systemctl restart fail2ban

Verify it’s running:

sudo fail2ban-client status sshd

Monitoring & Logging SSH Access

Configure SSH Logging

Set verbose logging to capture key algorithm information and detailed auth events:

LogLevel VERBOSE
SyslogFacility AUTH

View SSH logs with journalctl:

sudo journalctl -u sshd -f                    # Live tail
sudo journalctl -u sshd --since "1 hour ago"  # Recent activity
sudo sshd -T | grep -E 'loglevel|syslog'      # Check effective config

Monitor SSH Access Attempts

Practical commands for watching activity:

sudo lastb                 # Failed login attempts
who                        # Currently logged-in users
sudo journalctl -u sshd | grep "Failed password" | tail -20   # Recent failed passwords

For deeper monitoring, tools like OSSEC (host-based intrusion detection) or server monitoring dashboards give you alerting and trend analysis.

Keeping OpenSSH Updated & Patched

Why CVE-2024-6387 (regreSSHion) Matters

In July 2024, Qualys disclosed regreSSHion — a critical unauthenticated RCE in sshd affecting OpenSSH 8.5p1 through 9.7p1. A signal handler race condition allowed an attacker to execute arbitrary code as root without any credentials.

This was the most serious SSH vulnerability in years. It was fixed in OpenSSH 9.8.

Two more vulnerabilities were fixed in OpenSSH 9.9p2 (February 2025):

  • CVE-2025-26465: MITM impersonation when VerifyHostKeyDNS is enabled (off by default)
  • CVE-2025-26466: Memory/CPU DoS via SSH2_MSG_PING packets (mitigated by PerSourcePenalties)

These are real-world reasons to keep OpenSSH updated. Not theoretical.

Post-Quantum Key Exchange: What Changed in OpenSSH 10.0

Post-quantum key exchange is automatic in OpenSSH 10.0+. No configuration needed. If you explicitly set KexAlgorithms, include mlkem768x25519-sha256 to retain post-quantum protection.

OpenSSH 10.0 (April 2025) uses mlkem768x25519-sha256 — a hybrid of ML-KEM (post-quantum) and X25519 (classical) — as the default key exchange algorithm. This protects against “harvest now, decrypt later” attacks where adversaries record encrypted traffic to decrypt it once quantum computers are available.

Earlier versions (9.0+) used sntrup761x25519-sha512@openssh.com as the default post-quantum KEX. Both provide quantum resistance — the 10.0 algorithm is just the standardized version.

How to Check and Update Your OpenSSH Version

ssh -V       # Client version
sshd -V      # Server version (available since OpenSSH 9.2)

Update on Ubuntu/Debian:

sudo apt update
sudo apt install openssh-server

Update on RHEL/Fedora:

sudo dnf update openssh-server

After updating, verify and restart:

ssh -V
sudo sshd -t && sudo systemctl restart sshd

Set up automatic security updates to catch SSH patches without manual intervention:

# Ubuntu/Debian
sudo apt install unattended-upgrades
sudo dpkg-reconfigure unattended-upgrades

SSH Key Management Best Practices

Secure Key Generation

Ed25519 is the recommended key type. It’s been the default since OpenSSH 9.5 (Oct 2023) — fast, small keys, and no known weaknesses.

ssh-keygen -t ed25519 -a 100     # Primary (recommended)
ssh-keygen -t rsa -b 4096        # Legacy fallback for older systems

Key type comparison:

Key Type Recommended Size Status in OpenSSH 10.x
Ed25519 256 bits (fixed) Default, recommended
RSA 4096 bits Supported, use sha2-512
ECDSA 256-521 bits Supported, avoid if possible
DSA Removed in OpenSSH 10.0

DSA keys removed in OpenSSH 10.0

OpenSSH 10.0 (April 2025) completely removed DSA support. DSA was disabled at compile time in 9.8 (July 2024). If you still have DSA keys, migrate to Ed25519 immediately.

Key Storage and Protection

  • Store private keys on your local machine only — never on the server
  • Set restrictive permissions: chmod 700 ~/.ssh; chmod 600 ~/.ssh/id_ed25519
  • Always use a passphrase when generating keys
  • Use ssh-agent for convenience:
eval $(ssh-agent)
ssh-add ~/.ssh/id_ed25519

For high-security environments, consider hardware security keys (YubiKey, etc.) that store the private key in tamper-resistant hardware.

Regular Key Rotation

Rotate SSH keys every 6–12 months, or immediately if a key may have been compromised:

  1. Generate a new key pair
  2. Add the new public key to ~/.ssh/authorized_keys on the server
  3. Test the new key
  4. Remove the old public key from the server
  5. Delete the old private key from your local machine

For teams managing many servers, SSH certificates offer centralized key management with automatic expiration. This is the scalable alternative to distributing authorized_keys files everywhere. See SSH ProxyJump and jump host configuration for managing access to servers behind bastion hosts.

Testing Your SSH Security

Run ssh-audit

ssh-audit is a Python tool that checks your SSH server’s configuration and flags weak algorithms, insecure settings, and known issues.

# Install
sudo apt install ssh-audit     # or: pipx install ssh-audit

# Run against your server
ssh-audit localhost
ssh-audit your_server_ip

The output shows color-coded findings — red for critical, yellow for warnings, green for good. Match the recommendations against the algorithm lists in this article. See sshaudit.com for detailed hardening guides that align with the KexAlgorithms/Ciphers/MACs settings recommended here.

Verify Your Configuration

# Dump effective config (shows actual values after all includes and Match blocks)
sudo sshd -T | grep -E 'permitrootlogin|passwordauth|pubkeyauth|maxauthtries'

# Verbose connection test (shows what algorithms are negotiated)
ssh -v user@localhost

# List supported algorithms
ssh -Q kex
ssh -Q cipher
ssh -Q mac

Penetration Testing (Optional)

For a deeper check:

# Port scan
nmap -sV -p22 your_server_ip

# Test weak algorithm negotiation
ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 user@your_server_ip
# This should FAIL — if it succeeds, your server still accepts weak algorithms

Tools like Hydra can test brute-force resistance, but only run these against servers you own or have authorization to test. See checking remote port connectivity for verifying your firewall rules.

Common SSH Security Mistakes & Troubleshooting

Locked Out After Config Change

Wrong AllowUsers, bad port, or firewall misconfiguration — it happens. Fix:

  • Use your VPS provider’s web console (Hetzner, Vultr, DigitalOcean all offer one)
  • Log in through the console, fix the config, restart sshd
  • Prevention: always keep one session open while testing new config
How do I recover if I'm locked out of SSH?

Use your VPS provider’s web console or IPMI access. Log in, fix /etc/ssh/sshd_config (or remove the offending drop-in from sshd_config.d/), and restart sshd with sudo systemctl restart sshd. If you can’t access the console, most providers let you boot into a rescue mode to edit files on the disk.

Key Ignored Due to Wrong Permissions

Permission denied (publickey) is almost always a permissions issue on the server:

chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Also verify PubkeyAuthentication yes is set in sshd_config.

Connection Refused After Port Change

Forgot to update firewall rules. Check:

sudo ufw status          # Ubuntu/Debian
sudo firewall-cmd --list-all   # RHEL/Fedora
sudo iptables -L -n      # Manual iptables

Algorithm Mismatch Errors

If a client can’t connect after you restricted algorithms, check what the client supports:

ssh -Q kex    # Run on the client

Add a compatible algorithm to the server’s KexAlgorithms list, or update the client’s OpenSSH.

Conclusion

SSH hardening is a layered defense. No single setting makes you secure — but combined, these measures make your server a hard target:

  1. Key-based auth only — no passwords, no root login
  2. Restrict access — AllowUsers, firewall rules, MaxAuthTries
  3. Harden the config — drop-in file with restricted algorithms, idle timeouts, no forwarding
  4. Brute-force protection — PerSourcePenalties + fail2ban
  5. Monitor and update — LogLevel VERBOSE, journalctl, automatic security updates
  6. Test regularly — ssh-audit, sshd -T, verbose connection tests

If you’re running multiple servers, CrowdSec for server-level intrusion prevention adds community-driven threat intelligence on top of everything above.

For more on SSH workflows, see SSH port forwarding and tunneling and securing your control panel.

Secure Your Server with CrowdSec

Table of Contents