Skip to Content

SSH Security

SSH is the core tool for remote Linux server management and one of the most common targets for attackers. This article explains how to comprehensively harden SSH security on Ubuntu 26.04.

Key Authentication

Generating an SSH Key Pair

Generate a key pair on the client (your local computer):

# Generate an Ed25519 key (recommended, more secure and faster) ssh-keygen -t ed25519 -C "your_email@example.com" # If you need compatibility with older systems, use RSA 4096-bit ssh-keygen -t rsa -b 4096 -C "your_email@example.com" # You will be prompted: # Enter file in which to save the key (/home/user/.ssh/id_ed25519): # Enter passphrase (empty for no passphrase): # It is strongly recommended to set a passphrase

Deploying the Public Key to the Server

# Method 1: Using ssh-copy-id (recommended) ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server # Method 2: Manual copy cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys" # Method 3: Manual operation # On the server: mkdir -p ~/.ssh chmod 700 ~/.ssh nano ~/.ssh/authorized_keys # Paste the public key content chmod 600 ~/.ssh/authorized_keys

Testing Key Login

# Log in using the key ssh -i ~/.ssh/id_ed25519 user@server # If configured as the default key, log in directly ssh user@server

SSH Agent Key Management

# Start SSH Agent eval "$(ssh-agent -s)" # Add a key to the Agent (enter the passphrase once to cache it) ssh-add ~/.ssh/id_ed25519 # List loaded keys ssh-add -l # Remove all cached keys ssh-add -D
Warning

Before modifying SSH configuration, make sure key authentication has been tested successfully and keep your current SSH session open. Only close the old connection after verifying you can log in normally from a new terminal — otherwise a misconfiguration could permanently lock you out of the server.

Hardening SSH Server Configuration

Edit the SSH server configuration file:

sudo nano /etc/ssh/sshd_config

Disabling Password Login

Only disable password login after confirming key login works:

# Disable password authentication PasswordAuthentication no # Disable empty passwords PermitEmptyPasswords no # Disable keyboard-interactive authentication KbdInteractiveAuthentication no

Disabling root Login

# Completely prohibit root from logging in via SSH PermitRootLogin no # Or only allow root to log in with keys (not recommended) # PermitRootLogin prohibit-password

Changing the Default Port

# Use a non-standard port (reduces automated scanning) Port 2222

After changing the port, update your firewall rules:

sudo ufw allow 2222/tcp sudo ufw delete allow 22/tcp

Restricting Login Users

# Only allow specific users to log in AllowUsers admin deployer # Only allow specific groups to log in AllowGroups sshusers # Deny specific users DenyUsers guest test

Other Security Settings

# Limit authentication attempts MaxAuthTries 3 # Limit concurrent unauthenticated connections MaxStartups 3:50:10 # Session timeout (disconnect after 300 seconds of inactivity) ClientAliveInterval 300 ClientAliveCountMax 2 # Disable X11 forwarding (servers usually don't need it) X11Forwarding no # Disable Agent forwarding (unless actually needed) AllowAgentForwarding no # Disable TCP forwarding (unless actually needed) AllowTcpForwarding no # Use strong encryption algorithms # Note: OpenSSH 10.2 (26.04) already prefers the post-quantum KEX mlkem768x25519-sha256 by default. # Avoid overriding KexAlgorithms — hardcoding a list freezes the algorithm set # and may end up dropping newly added post-quantum defaults in the future. Keep the secure defaults. # # Only override for troubleshooting or temporary interop with old clients, and always put the post-quantum algorithm first: # KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512@openssh.com,curve25519-sha256@libssh.org,curve25519-sha256 Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com # Display a login banner Banner /etc/ssh/banner.txt # Login grace time LoginGraceTime 30

Applying the Configuration

# Check configuration syntax sudo sshd -t # Restart SSH service (on Ubuntu the unit is ssh.service; sshd is just an alias) sudo systemctl restart ssh # Note: on 26.04 ssh listens via socket activation by default, # so after changing the port you must reload the socket unit: sudo systemctl daemon-reload sudo systemctl restart ssh.socket # Important: Test the connection in a new terminal window -- do not close the current one! ssh -p 2222 user@server

fail2ban Protection

fail2ban monitors log files and automatically bans IP addresses with multiple failed login attempts.

Installation and Configuration

# Install sudo apt install fail2ban # Start and enable at boot sudo systemctl enable --now fail2ban

Creating a Local Configuration

Do not directly edit /etc/fail2ban/jail.conf; create a local configuration instead:

sudo nano /etc/fail2ban/jail.local
[DEFAULT] # Ban duration (seconds) bantime = 3600 # Detection time window (seconds) findtime = 600 # Maximum failed attempts maxretry = 3 # Ban action banaction = ufw # Ignored IPs (whitelist) ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24 # Email notifications # destemail = admin@example.com # sendername = Fail2Ban # mta = sendmail # action = %(action_mwl)s [sshd] enabled = true port = 2222 filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600

Managing fail2ban

# View status sudo fail2ban-client status # View SSH jail status sudo fail2ban-client status sshd # View banned IPs sudo fail2ban-client status sshd | grep "Banned IP" # Manually ban an IP sudo fail2ban-client set sshd banip 203.0.113.50 # Manually unban an IP sudo fail2ban-client set sshd unbanip 203.0.113.50 # View fail2ban logs sudo tail -f /var/log/fail2ban.log # Reload configuration sudo fail2ban-client reload

Advanced fail2ban Configuration

Protect other services:

# Nginx authentication protection [nginx-http-auth] enabled = true filter = nginx-http-auth port = http,https logpath = /var/log/nginx/error.log maxretry = 3 # Nginx malicious requests [nginx-botsearch] enabled = true filter = nginx-botsearch port = http,https logpath = /var/log/nginx/access.log maxretry = 2

SSH Client Configuration

Create an SSH config file on the client to simplify connections:

nano ~/.ssh/config
# Global settings Host * ServerAliveInterval 60 ServerAliveCountMax 3 AddKeysToAgent yes # Production server Host prod HostName 203.0.113.10 User admin Port 2222 IdentityFile ~/.ssh/id_ed25519_prod # Jump host Host bastion HostName 203.0.113.20 User jump Port 2222 IdentityFile ~/.ssh/id_ed25519 # Connect to an internal server via jump host Host internal HostName 10.0.1.50 User admin ProxyJump bastion IdentityFile ~/.ssh/id_ed25519

Simplified connections after configuration:

ssh prod ssh internal

Two-Factor Authentication

Add Google Authenticator two-factor authentication to SSH:

# Install sudo apt install libpam-google-authenticator # Each user runs the initialization google-authenticator # Follow the prompts to complete setup and scan the QR code # Configure PAM sudo nano /etc/pam.d/sshd # Add: # auth required pam_google_authenticator.so # Configure SSH sudo nano /etc/ssh/sshd_config # Set: # AuthenticationMethods publickey,keyboard-interactive # KbdInteractiveAuthentication yes sudo systemctl restart ssh

OpenSSH Post-Quantum Key Exchange

Ubuntu 26.04 ships OpenSSH 10.2, whose default key exchange prefers the post-quantum hybrid scheme mlkem768x25519-sha256 (ML-KEM-768 combined with X25519), protecting against “harvest-now-decrypt-later” attacks.

# Confirm the KEX algorithms supported by your OpenSSH (should include mlkem768x25519-sha256) ssh -Q kex # Inspect the KEX algorithm actually negotiated for a connection ssh -v user@server 2>&1 | grep "kex:"
Note

Post-quantum KEX is only used when both sides support it. When interoperating with older clients/servers (OpenSSH below 9.0) it automatically falls back to the classic curve25519-sha256 algorithm, with no manual configuration needed. Unless you have a real compatibility problem, do not override the KexAlgorithms default, to avoid accidentally removing post-quantum protection.

Monitoring and Auditing

# View successful login records last -n 20 # View failed login records lastb -n 20 # View SSH authentication logs journalctl -u ssh.service --since today | grep -E "Accepted|Failed" # Count failed login IPs journalctl -u ssh.service | grep "Failed" | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20 # View current SSH connections who w ss -tnp | grep :22

Security Checklist

Before applying changes, confirm the following:

  1. Key authentication has been configured and tested
  2. You have a backup access method (console access, another user, etc.)
  3. Firewall has been updated with the corresponding port rules
  4. fail2ban whitelist includes your commonly used IPs
  5. You have tested the connection from a new terminal before closing the old one
  6. SSH configuration syntax has been validated with sshd -t
Last updated on