SSH Hardening on a Linux VPS: Complete sshd_config Security Guide

12 min read·Matthieu|

Lock down SSH on your Debian 12 or Ubuntu 24.04 VPS. Ed25519 key generation, sshd_config hardening, ProxyJump bastion setup, cipher hardening, and ssh-audit verification. Every change tested before you move on.

SSH is the front door to your server. Automated bots start hammering port 22 within minutes of a VPS going live. This guide walks through every sshd_config change needed to lock down SSH on Debian 12 (OpenSSH 9.2) and Ubuntu 24.04 (OpenSSH 9.6), with a verification step after each change so you know it actually worked.

Prerequisites

You need:

  • A VPS running Debian 12 or Ubuntu 24.04 (fresh or existing)
  • A non-root user with sudo access
  • A second terminal or SSH session open to the server (you will need this to test changes without locking yourself out)

This guide is part of the Linux VPS Security: Threats, Layers, and Hardening Guide series. After hardening SSH here, set up automated brute-force protection with .

How do I generate a secure SSH key for my VPS?

Generate an Ed25519 key on your local machine (your laptop or workstation, not the server). Ed25519 produces a 256-bit key that offers 128 bits of security, equivalent to RSA-3072, while being faster to generate and verify. The signatures are deterministic, so they don't depend on a random number generator at signing time. This eliminates an entire class of implementation attacks that affect RSA.

ssh-keygen -t ed25519 -C "yourname@yourmachine"

You will see output like this:

Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/yourname/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/yourname/.ssh/id_ed25519
Your public key has been saved in /home/yourname/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:xR5xGk3TOs4mEfW8sBv7g7LkE2PLxYae2TqfGxpfM3Q yourname@yourmachine

Set a passphrase. If someone steals your private key file, the passphrase is the only thing between them and your servers.

Ed25519 vs RSA: which key should I use?

Ed25519 RSA-4096
Key size 256 bits 4096 bits
Security strength ~128 bits ~140 bits
Key generation Instant 1-5 seconds
Signature verification Faster Slower
Private key file size 464 bytes ~3.3 KB
RNG dependency at signing No (deterministic) Yes
Compatibility OpenSSH 6.5+ (2014) Universal

Use Ed25519 unless you need to connect to systems running OpenSSH older than 6.5, which is rare in 2026.

Copy your public key to the server

From your local machine:

ssh-copy-id -i ~/.ssh/id_ed25519.pub youruser@your-server-ip

If ssh-copy-id is not available (some macOS setups), copy manually:

cat ~/.ssh/id_ed25519.pub | ssh youruser@your-server-ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

Verify: Log in from a new terminal using key authentication:

ssh -i ~/.ssh/id_ed25519 youruser@your-server-ip

If you get a shell without entering your server password (only your key passphrase), key authentication is working.

Understanding the Include directive

Before editing sshd_config, know this: both Debian 12 and Ubuntu 24.04 ship with Include /etc/ssh/sshd_config.d/*.conf at the top of /etc/ssh/sshd_config. OpenSSH uses the first value it finds for most directives. Any .conf file in sshd_config.d/ takes priority over settings in the main file.

Check what is already set:

ls -la /etc/ssh/sshd_config.d/

On Ubuntu 24.04, you will typically find 50-cloud-init.conf if cloud-init is active. Read any existing files before making changes:

cat /etc/ssh/sshd_config.d/*.conf 2>/dev/null

We will put our hardening in a single file that loads first:

sudo touch /etc/ssh/sshd_config.d/00-hardening.conf
sudo chmod 600 /etc/ssh/sshd_config.d/00-hardening.conf

The 00- prefix ensures our file is read before other config snippets. The chmod 600 restricts read access to root only, since this file controls who can log in.

All sshd_config changes in this guide go into /etc/ssh/sshd_config.d/00-hardening.conf unless stated otherwise.

How do I disable SSH password authentication?

Disabling password authentication forces key-based login only. Brute-force attacks become pointless because there is no password to guess.

Open the hardening config:

sudo nano /etc/ssh/sshd_config.d/00-hardening.conf

Add:

PasswordAuthentication no
KbdInteractiveAuthentication no

KbdInteractiveAuthentication replaces the deprecated ChallengeResponseAuthentication in OpenSSH 9.x. Disable both to close all password-based login paths.

Before restarting sshd, always validate the config and keep your current session open.

sudo sshd -t

If there is no output, the config is valid. Any errors will print to the terminal.

Now restart sshd:

sudo systemctl restart sshd

Verify from a second terminal (do not close your current session):

ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no youruser@your-server-ip

You should see:

youruser@your-server-ip: Permission denied (publickey).

That confirms password authentication is off. Sharp eyes: notice it says (publickey) as the only allowed method. This is exactly what we want.

Now confirm key login still works from that same second terminal:

ssh youruser@your-server-ip

If both tests pass, password authentication is disabled and key login works. If key login fails, go back to your still-open first session and fix the config before you get locked out.

How do I disable SSH root login on Ubuntu and Debian?

Direct root login over SSH should be off. Even with key-only auth, a compromised key for root gives full system access with no audit trail of who logged in. Use a regular user with sudo instead.

Add to /etc/ssh/sshd_config.d/00-hardening.conf:

PermitRootLogin no

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify from a second terminal:

ssh root@your-server-ip

Expected output:

root@your-server-ip: Permission denied (publickey).

Verify the setting took effect with sshd -T (uppercase T dumps the running config):

sudo sshd -T | grep -i permitrootlogin
permitrootlogin no

How do I restrict SSH access to specific users and groups?

AllowUsers and AllowGroups limit SSH login to an explicit list. Anyone not on the list is rejected, even if they have valid keys. This is a safety net against accidental key deployment or new users created by packages.

Add to /etc/ssh/sshd_config.d/00-hardening.conf:

AllowUsers youruser

Replace youruser with your actual username. To allow multiple users:

AllowUsers youruser deployer

Alternatively, use group-based access (better for teams):

sudo groupadd sshusers
sudo usermod -aG sshusers youruser

Then in the config:

AllowGroups sshusers

Processing order

OpenSSH evaluates access in this order: DenyUsers, AllowUsers, DenyGroups, AllowGroups. A deny at any stage blocks the user. If you use AllowUsers, only listed users can log in. If you use AllowGroups, only members of listed groups can log in. You can combine them, but AllowUsers is checked first.

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify from a second terminal:

ssh youruser@your-server-ip

Confirm your user can still log in. Then verify that a non-listed user would be rejected:

sudo sshd -T | grep -i allowusers
allowusers youruser

What SSH session limits should I set?

These directives limit authentication attempts, connection timeouts, and unauthenticated connections. They slow down brute-force attacks and clean up stale sessions.

Add to /etc/ssh/sshd_config.d/00-hardening.conf:

MaxAuthTries 3
LoginGraceTime 20
MaxStartups 10:30:60
MaxSessions 3
ClientAliveInterval 300
ClientAliveCountMax 2

Here is what each setting does:

Directive Value Effect
MaxAuthTries 3 Disconnects after 3 failed auth attempts per connection
LoginGraceTime 20 Gives 20 seconds to authenticate before disconnecting
MaxStartups 10:30:60 After 10 unauthenticated connections, randomly drops 30% of new ones. At 60, drops all new connections.
MaxSessions 3 Max 3 multiplexed sessions per connection
ClientAliveInterval 300 Sends a keepalive packet every 300 seconds (5 minutes)
ClientAliveCountMax 2 Disconnects after 2 missed keepalive responses

ClientAliveInterval math: The actual idle timeout is ClientAliveInterval x ClientAliveCountMax. With these values: 300 x 2 = 600 seconds = 10 minutes. An idle session disconnects after 10 minutes of no response.

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify:

sudo sshd -T | grep -E "maxauthtries|logingracetime|maxstartups|maxsessions|clientaliveinterval|clientalivecountmax"
maxauthtries 3
logingracetime 20
maxstartups 10:30:60
maxsessions 3
clientaliveinterval 300
clientalivecountmax 2

Disable unnecessary forwarding features

Forwarding features extend SSH's attack surface. Disable everything you don't actively use.

Add to /etc/ssh/sshd_config.d/00-hardening.conf:

AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify:

sudo sshd -T | grep -E "allowagentforwarding|allowtcpforwarding|x11forwarding|permittunnel"
allowagentforwarding no
allowtcpforwarding no
x11forwarding no
permittunnel no

Why should I disable SSH agent forwarding?

Agent forwarding lets a remote server use your local SSH keys to authenticate to other servers. This sounds convenient for jumping between machines. The problem: anyone with root on that remote server can hijack your agent socket and use your keys.

The attack scenario:

  1. You enable agent forwarding and SSH into Server A.
  2. OpenSSH creates a socket at /tmp/ssh-XXXX/agent.YYYY on Server A.
  3. An attacker with root on Server A reads the SSH_AUTH_SOCK environment variable from your session.
  4. The attacker connects to that socket: SSH_AUTH_SOCK=/tmp/ssh-XXXX/agent.YYYY ssh user@server-b.
  5. Server B sees a valid authentication using your key. The attacker is in.

The attacker never touched your private key. They only needed root on the intermediate server while your session was active.

Use ProxyJump instead (next section). It provides the same multi-hop access without exposing your agent socket to any intermediate server.

How do I configure SSH ProxyJump for bastion host access?

ProxyJump routes your SSH connection through a jump host (bastion) without agent forwarding. Your keys never leave your local machine. The connection is encrypted end-to-end: the bastion only sees encrypted traffic passing through.

Command-line usage

ssh -J jumpuser@bastion.example.com targetuser@10.0.1.50

The -J flag tells SSH to connect to the bastion first, then tunnel through to the target. You can chain multiple jumps with commas:

ssh -J jump1@bastion1,jump2@bastion2 targetuser@10.0.1.50

SSH config file setup

For regular use, add entries to ~/.ssh/config on your local machine:

Host bastion
    HostName bastion.example.com
    User jumpuser
    IdentityFile ~/.ssh/id_ed25519

Host internal-app
    HostName 10.0.1.50
    User appuser
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519

Host internal-db
    HostName 10.0.2.100
    User dbadmin
    ProxyJump bastion
    IdentityFile ~/.ssh/id_ed25519

Now connect to internal servers directly:

ssh internal-app

SSH handles the bastion hop automatically.

Hardening the bastion server

On the bastion host, restrict what jump users can do. Add to the bastion's sshd_config:

Match User jumpuser
    PermitTTY no
    X11Forwarding no
    PermitTunnel no
    ForceCommand /usr/sbin/nologin
    AllowTcpForwarding yes

This allows TCP forwarding (needed for ProxyJump) but prevents the jump user from getting a shell, running commands, or using X11. If the bastion is compromised, the attacker gets a forwarding-only account with no shell access.

Which SSH ciphers and key exchange algorithms are secure?

Default cipher lists in OpenSSH include algorithms kept for backward compatibility. Removing weak ones reduces your attack surface. These recommendations target OpenSSH 9.2+ (Debian 12) and 9.6+ (Ubuntu 24.04).

First, regenerate host keys using Ed25519 only and remove any DSA or ECDSA keys:

sudo rm -f /etc/ssh/ssh_host_*key*
sudo ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
sudo ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""

We keep an RSA host key for clients that don't support Ed25519 (rare, but it avoids lockouts).

Next, remove weak Diffie-Hellman moduli (groups smaller than 3072 bits):

sudo awk '$5 >= 3071' /etc/ssh/moduli > /tmp/moduli.safe
sudo mv /tmp/moduli.safe /etc/ssh/moduli
sudo chown root:root /etc/ssh/moduli
sudo chmod 644 /etc/ssh/moduli

Add to /etc/ssh/sshd_config.d/00-hardening.conf:

HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256
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,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
Category Algorithms Notes
Key exchange sntrup761x25519, curve25519, DH group16/18 sntrup761 is post-quantum hybrid. curve25519 is the current standard.
Ciphers ChaCha20-Poly1305, AES-256-GCM, AES-128-GCM, AES-CTR ChaCha20 first: faster in software without AES-NI. GCM is authenticated encryption.
MACs HMAC-SHA2 ETM, UMAC-128 ETM ETM (Encrypt-then-MAC) only. Non-ETM modes are weaker.
Host keys Ed25519, RSA (SHA-2) No DSA (broken). No ECDSA (NIST curve trust concerns).

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify from a second terminal that you can still connect. If your SSH client doesn't support any of these algorithms (very old client), you will get a "no matching cipher" error. In that case, update your client or temporarily add back the needed algorithm.

Configure a login banner

Hide your SSH version information and show a legal warning banner:

sudo nano /etc/ssh/banner.txt
Authorized access only. All activity is monitored and logged.

Keep it short. Long banners containing system info help attackers fingerprint your OS.

Add to /etc/ssh/sshd_config.d/00-hardening.conf:

Banner /etc/ssh/banner.txt
DebianBanner no

DebianBanner no removes the Debian/Ubuntu version string from the SSH protocol banner. Version disclosure helps attackers target known vulnerabilities for your specific OpenSSH build.

Validate and restart:

sudo sshd -t && sudo systemctl restart sshd

Verify from your local machine:

ssh -v youruser@your-server-ip 2>&1 | grep "banner"

Complete hardened sshd_config reference

Here is the full /etc/ssh/sshd_config.d/00-hardening.conf with all settings from this guide:

# Authentication
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
AuthenticationMethods publickey

# Access control
AllowUsers youruser

# Session limits
MaxAuthTries 3
LoginGraceTime 20
MaxStartups 10:30:60
MaxSessions 3
ClientAliveInterval 300
ClientAliveCountMax 2

# Forwarding restrictions
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PermitTunnel no

# Cryptography
HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256
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,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com

# Banner
Banner /etc/ssh/banner.txt
DebianBanner no

# Logging
LogLevel VERBOSE

LogLevel VERBOSE records additional details including key fingerprints used for authentication. Useful for auditing who logged in with which key.

After writing this file, always validate before restarting:

sudo sshd -t && sudo systemctl restart sshd

Check the full effective config:

sudo sshd -T

This dumps every active setting, including defaults. Pipe through grep to check specific values:

sudo sshd -T | grep -E "permitrootlogin|passwordauthentication|allowusers"
permitrootlogin no
passwordauthentication no
allowusers youruser

How do I verify my SSH hardening with ssh-audit?

ssh-audit scans your server and grades each algorithm as good, warning, or fail. Run it after hardening to confirm everything passes.

Install on your local machine or a separate server (not the target):

pip3 install ssh-audit

Or with apt on Debian/Ubuntu:

sudo apt update && sudo apt install -y ssh-audit

Scan your server:

ssh-audit your-server-ip

With the hardening from this guide, you should see no [fail] entries. The output starts with the detected OpenSSH version, then lists each algorithm category:

# general
(gen) banner: SSH-2.0-OpenSSH_9.6
(gen) software: OpenSSH 9.6
(gen) compression: enabled (zlib@openssh.com)

# key exchange algorithms
(kex) sntrup761x25519-sha512@openssh.com  -- [info] available since OpenSSH 8.5
(kex) curve25519-sha256                   -- [info] available since OpenSSH 7.4
...

# encryption algorithms (ciphers)
(enc) chacha20-poly1305@openssh.com       -- [info] available since OpenSSH 6.5
(enc) aes256-gcm@openssh.com              -- [info] available since OpenSSH 6.2
...

If you see any [fail] entries, check that your 00-hardening.conf is loaded (remember the Include order) and that no other file in sshd_config.d/ is overriding your settings.

ssh-audit also has built-in hardening guides:

ssh-audit --list-hardening-guides

Troubleshooting

I locked myself out

If you can't SSH in after a config change:

  1. Use your VPS provider's web console (KVM/VNC) to log in directly.
  2. Fix /etc/ssh/sshd_config.d/00-hardening.conf.
  3. Run sshd -t to validate.
  4. Restart: systemctl restart sshd.

This is why we say: never close your working session before testing from a second terminal.

sshd won't start after config change

sudo sshd -t

This prints the exact line and file with the error. Common issues:

  • Typo in an algorithm name (no spaces allowed in the comma-separated lists)
  • Duplicate directives in multiple files (check sshd_config.d/ and the main sshd_config)
  • AllowUsers with a username that doesn't exist (sshd starts, but nobody can log in)

Connection refused after cipher hardening

Your local SSH client might not support the ciphers you configured. Check which ciphers your client supports:

ssh -Q cipher

Compare against the server's list. Add back any cipher your client needs, or update your client.

Check SSH logs

sudo journalctl -u sshd -f

Watch the logs in real time while attempting a connection from another terminal. Authentication failures, config errors, and connection drops all appear here.

Verify which config file sets a directive

sudo sshd -T | grep passwordauthentication

If the value doesn't match what you set, another file in sshd_config.d/ is overriding yours. Files load in alphabetical order. Our 00-hardening.conf loads first, so it wins for most directives. But check for cloud-init or other management tools that write their own configs.

Verification checklist

Run through this after completing all hardening steps:

  1. Password auth disabled: ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no youruser@server returns "Permission denied"
  2. Root login disabled: ssh root@server returns "Permission denied"
  3. Key auth works: ssh youruser@server gives you a shell
  4. AllowUsers active: sudo sshd -T | grep allowusers shows your user
  5. Session limits set: sudo sshd -T | grep maxauthtries shows 3
  6. Forwarding disabled: sudo sshd -T | grep allowagentforwarding shows no
  7. Strong ciphers only: ssh-audit server-ip shows no [fail] entries
  8. Logs working: sudo journalctl -u sshd -n 20 shows recent auth entries

After SSH is hardened, set up to automatically ban IPs that fail authentication. For network-level SSH access control, see .


Copyright 2026 Virtua.Cloud. All rights reserved. This content is original work by the Virtua.Cloud team. Reproduction, republication, or redistribution without written permission is prohibited.

Ready to try it yourself?

Deploy your own server in seconds. Linux, Windows, or FreeBSD.

See VPS Plans
SSH Hardening on a Linux VPS: sshd_config Security Guide