Securing SSH: 8 Concrete Measures Beyond the Password

A practical guide to hardening the SSH configuration of your Linux servers. ED25519 keys, 2FA, fail2ban, port knocking, auditing and advanced logging.

SSH is the main entry point to any Linux server. It is the protocol through which administration commands, deployments and often file transfers all flow. Yet most system administrators simply change the default port and set a strong password. That is not enough.

Brute-force attacks, credential stuffing and protocol vulnerabilities are real, everyday threats. A server exposed on the Internet receives, on average, several thousand SSH connection attempts per day. In this article, I detail 8 concrete, battle-tested measures to harden your SSH configuration, well beyond the simple password.

Each measure comes with its exact configuration and practical advice drawn from my production experience. All changes are made in the /etc/ssh/sshd_config file unless stated otherwise.

1. Disable password authentication

The first measure, and probably the most effective, is to completely remove password authentication. A password, however complex, remains vulnerable to dictionary and brute-force attacks. SSH keys offer an incomparably higher level of security: a 256-bit ED25519 key is equivalent to a password of several dozen random characters.

Before disabling passwords, make sure you have configured at least one public key on the server. Test the key-based connection in a separate session before cutting off password access. This is a classic mistake that can lock you out of your own server.

# /etc/ssh/sshd_config

# Disable password authentication
PasswordAuthentication no

# Enable public-key authentication
PubkeyAuthentication yes

# Disable weak authentication methods
ChallengeResponseAuthentication no
UsePAM no

# Restart the service
sudo systemctl restart sshd
Warning: always keep an SSH session open while you edit the configuration. If you make a mistake, you can fix it without losing access. Test the new configuration with sshd -t before restarting the service.

2. Restrict authorized users

By default, SSH allows any system user with a valid shell to log in. This is an unnecessarily large attack surface. The AllowUsers directive lets you explicitly define a whitelist of accounts allowed to connect. This is the principle of least privilege applied to SSH.

You can combine several approaches: restrict by user, by group, or exclude specific accounts. The AllowGroups directive is particularly handy for managing access at team scale. Create a dedicated group such as ssh-users and add only the relevant accounts to it.

# Allow only certain users
AllowUsers deployer admin morgann

# Or restrict by group (more maintainable)
AllowGroups ssh-users

# Explicitly forbid root (on top of AllowUsers)
DenyUsers root
PermitRootLogin no

# Create the group and add a user to it
sudo groupadd ssh-users
sudo usermod -aG ssh-users deployer
Best practice: favor AllowGroups over AllowUsers in multi-administrator environments. Adding or removing a user from the group is simpler and less risky than editing sshd_config every time the team changes.

3. Configure timeouts and login attempts

Timeout and attempt-limiting parameters are often left at their default values, which are too permissive. LoginGraceTime sets the maximum delay to authenticate after the TCP connection is established. MaxAuthTries limits the number of attempts per session. MaxSessions controls the number of multiplexed sessions on a single connection.

Lowering these values considerably slows down brute-force attacks and frees up server resources held by malicious connections more quickly. An attacker limited to 2 attempts per TCP connection with a 30-second grace period will be far less effective.

These parameters combine effectively with pourquoi Fail2ban ne suffit pas to create defense in depth. Even if the attacker bypasses fail2ban by changing IP address, the session limits remain active.

# Timeouts and session limits
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 3

# Automatically disconnect idle sessions
ClientAliveInterval 300
ClientAliveCountMax 2

# Disable forwarding if not needed
AllowTcpForwarding no
X11Forwarding no
AllowAgentForwarding no
Tip: ClientAliveInterval 300 combined with ClientAliveCountMax 2 automatically disconnects idle sessions after 10 minutes (300s x 2). This is essential to avoid ghost sessions that stay open indefinitely.

4. Enable 2FA with Google Authenticator

Two-factor authentication adds a fundamental layer of security: even if an attacker obtains your private key (stolen laptop, compromised workstation), they will also need the TOTP code generated by your authenticator app. This is defense in depth in its most concrete form.

The libpam-google-authenticator PAM module integrates seamlessly with OpenSSH. The configuration requires both the SSH key and a TOTP code to connect. Setup time is about 5 minutes per server.

# Install the PAM module
sudo apt install libpam-google-authenticator

# Per-user configuration
google-authenticator -t -d -f -r 3 -R 30 -w 3

# /etc/pam.d/sshd - Add at the end of the file
auth required pam_google_authenticator.so

# /etc/ssh/sshd_config
UsePAM yes
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

# Restart SSH
sudo systemctl restart sshd
Important: generate and store the backup codes in a safe place (password manager, vault). Without these codes, losing your phone means losing access to the server. I have detailed the full procedure in my dedicated article on 2FA over SSH.

5. Use ED25519 keys instead of RSA

RSA was the standard for decades, but ED25519 surpasses it on every front. ED25519 keys are shorter (256 bits versus 4096 bits for a security-equivalent RSA key), signing is faster, and the algorithm is resistant to side-channel attacks. On top of that, ED25519 uses elliptic curves whose parameters are not suspect, unlike NIST P-256.

Migration is simple: generate a new ED25519 key pair, deploy the public key on your servers, then remove the old RSA keys. Take the opportunity to configure the server so that it only accepts modern algorithms and rejects the old, vulnerable encryption algorithms.

# Generate an ED25519 key with a comment
ssh-keygen -t ed25519 -C "morgann@workstation-2026" -a 100

# Copy the key to the server
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server

# Restrict algorithms on the server side in sshd_config
HostKeyAlgorithms ssh-ed25519,[email protected]
PubkeyAcceptedAlgorithms ssh-ed25519,[email protected]

# Remove the old RSA/DSA/ECDSA host keys
sudo rm /etc/ssh/ssh_host_rsa_key*
sudo rm /etc/ssh/ssh_host_dsa_key*
sudo rm /etc/ssh/ssh_host_ecdsa_key*

# Regenerate only the ED25519 host key
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
sudo systemctl restart sshd
Note: the -a 100 option during generation increases the number of KDF iterations to protect the private key on disk. This is an extra layer of protection if your private key file is compromised: brute-forcing the passphrase will be 100 times slower.

6. Port knocking and fail2ban

Port knocking is a security-through-obscurity technique that, combined with real security measures, adds an effective layer of protection. The principle is simple: the SSH port stays closed by default. It only opens when the client sends a precise sequence of connections to other ports. It is the digital equivalent of a coded knock on a door.

Fail2ban, for its part, monitors authentication logs and temporarily bans IP addresses that fail too often. It is the most widespread measure, and for good reason: it is simple to set up and immediately effective against automated brute force.

These two tools complement each other perfectly. Port knocking drastically reduces the noise in the logs, and fail2ban handles the attempts that get through anyway.

# Install fail2ban
sudo apt install fail2ban

# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600

# Port knocking with knockd
sudo apt install knockd

# /etc/knockd.conf
[openSSH]
    sequence    = 7000,8000,9000
    seq_timeout = 5
    command     = /sbin/iptables -I INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
    tcpflags    = syn

[closeSSH]
    sequence    = 9000,8000,7000
    seq_timeout = 5
    command     = /sbin/iptables -D INPUT -s %IP% -p tcp --dport 22 -j ACCEPT
    tcpflags    = syn

# Client side: knock before connecting
knock server 7000 8000 9000 && ssh user@server
Advice: fail2ban with bantime = 3600 and maxretry = 3 is a good starting point. For heavily exposed servers, consider progressive bans with bantime.increment = true, which doubles the ban duration on each repeat offense. I have written a complete guide on advanced fail2ban configuration.

7. Logging and alerts

A hardened SSH configuration is worthless if nobody watches the logs. By default, OpenSSH logs at a minimal verbosity level. Switching to VERBOSE lets you record the fingerprints of the keys used, detailed failed attempts and complete session information. This data is essential for post-incident analysis.

Beyond passive logging, set up active alerts. A simple script that watches successful connections and sends a notification lets you detect an intrusion immediately. Combined with a log centralization tool such as rsyslog or journald, you get full visibility into SSH activity.

# /etc/ssh/sshd_config - Advanced logging
LogLevel VERBOSE
SyslogFacility AUTH

# Alert script on successful login
# /etc/ssh/notify-login.sh
#!/bin/bash
SUBJECT="SSH alert: login on $(hostname)"
BODY="User: $PAM_USER\nSource IP: $PAM_RHOST\nDate: $(date)"
echo -e "$BODY" | mail -s "$SUBJECT" [email protected]

# /etc/pam.d/sshd - Trigger the script
session optional pam_exec.so /etc/ssh/notify-login.sh

# Monitor connections in real time
journalctl -u sshd -f --output=short-precise

# Count failed attempts over the last 24h
journalctl -u sshd --since "24 hours ago" | grep "Failed password" | wc -l
Tip: the VERBOSE level records the fingerprint of every key used to connect. This is invaluable for identifying which key was used in the event of an incident, especially if several people share the same service account.

8. Audit the configuration regularly

Security is not a state, it is a process. A configuration that is hardened today can become vulnerable tomorrow following an update, a policy change or the discovery of a new flaw. Regularly auditing your SSH configuration is essential to maintain a consistent level of security.

The ssh-audit tool analyzes your SSH server's configuration and identifies weak algorithms, obsolete parameters and missing best practices. Lynis, for its part, performs a global security audit of the system, including SSH. Integrate these tools into your maintenance routines or, better still, into your CI/CD pipeline.

Schedule an audit at least monthly. Automate it with cron and send the reports by email. The ssh-audit results can be fed into your monitoring tool to trigger alerts if the security score degrades.

# Install ssh-audit
pip install ssh-audit

# Audit the local server
ssh-audit localhost

# Audit a remote server
ssh-audit server.example.com

# Audit with Lynis (SSH section)
sudo apt install lynis
sudo lynis audit system --tests-from-group ssh

# Automate the monthly audit via cron
# /etc/cron.monthly/ssh-audit.sh
#!/bin/bash
REPORT="/var/log/ssh-audit-$(date +%Y%m).txt"
ssh-audit localhost > "$REPORT" 2>&1
mail -s "SSH audit report - $(hostname)" [email protected] < "$REPORT"

# Check the configuration syntax
sudo sshd -t

# List the active algorithms
ssh -Q cipher
ssh -Q mac
ssh -Q kex
Reminder: after every OpenSSH update, run ssh-audit again to verify that no new algorithms have been enabled by default. Security updates fix flaws but can also re-enable options that you had deliberately disabled.

Summary and next steps

Here is a recap of the 8 measures to apply to harden your SSH configuration:

  • Disable passwords: key-based authentication only
  • Restrict users: AllowGroups with a dedicated group
  • Limit attempts: MaxAuthTries, timeouts and automatic disconnection
  • Enable 2FA: SSH key + mandatory TOTP code
  • Use ED25519: drop RSA and obsolete algorithms
  • Deploy fail2ban: automatic banning of attackers
  • Log in VERBOSE: full traceability of connections
  • Audit regularly: ssh-audit and lynis as a monthly routine

Applied together, these measures drastically reduce your server's attack surface. None of them is complex to implement, and each adds an extra layer of protection. Security is built in depth: it is not a single measure that protects you, it is their combination.

To go further, I recommend checking out the official ssh-audit website, which maintains an up-to-date list of recommended algorithms, as well as the OpenSSH man pages for a complete reference of all available directives.

Did you enjoy this article?

Comments

Morgann Riu

Cybersecurity and Linux administration expert. I help companies secure and optimize their critical infrastructures.

Back to the blog

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.