SSH Hardening on a Linux VPS: Complete sshd_config Security Guide
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:
- You enable agent forwarding and SSH into Server A.
- OpenSSH creates a socket at
/tmp/ssh-XXXX/agent.YYYYon Server A. - An attacker with root on Server A reads the
SSH_AUTH_SOCKenvironment variable from your session. - The attacker connects to that socket:
SSH_AUTH_SOCK=/tmp/ssh-XXXX/agent.YYYY ssh user@server-b. - 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:
- Use your VPS provider's web console (KVM/VNC) to log in directly.
- Fix
/etc/ssh/sshd_config.d/00-hardening.conf. - Run
sshd -tto validate. - 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 mainsshd_config) AllowUserswith 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:
- Password auth disabled:
ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no youruser@serverreturns "Permission denied" - Root login disabled:
ssh root@serverreturns "Permission denied" - Key auth works:
ssh youruser@servergives you a shell - AllowUsers active:
sudo sshd -T | grep allowusersshows your user - Session limits set:
sudo sshd -T | grep maxauthtriesshows 3 - Forwarding disabled:
sudo sshd -T | grep allowagentforwardingshows no - Strong ciphers only:
ssh-audit server-ipshows no [fail] entries - Logs working:
sudo journalctl -u sshd -n 20shows 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