Last updated: 28 October 2026. Sources are listed at the end.

This SSH hardening checklist is for anyone who runs Ubuntu 24.04 servers. It gives you a config you can copy, check and roll out without locking yourself out. Every setting below was validated against OpenSSH 9.6p1, the version Ubuntu 24.04 ships.

Ubuntu 24.04 changed two things that trip people up. First, settings in the drop-in directory override the main file. Second, sshd now starts through systemd socket activation, so changing the port works differently. We cover both, then give you one tested drop-in file, a crypto section, a safe rollout order and a printable table.

The short answer

  • Put your settings in /etc/ssh/sshd_config.d/10-hardening.conf. For each option, the first value sshd reads wins.
  • Allow keys only: PasswordAuthentication no, KbdInteractiveAuthentication no, PermitRootLogin no.
  • Limit who can log in with AllowGroups, and turn off forwarding you do not use.
  • Use Ed25519 keys with a passphrase, or FIDO2 keys (ed25519-sk) for admins.
  • Run sshd -t before every reload, and keep your current session open while you test.
  • Do not expose port 22 to the whole internet if you can avoid it. Use a VPN, a bastion or a firewall allowlist instead.

How Ubuntu 24.04 reads sshd config

The default /etc/ssh/sshd_config on 24.04 has this near the top:

Include /etc/ssh/sshd_config.d/*.conf

Because the Include comes before everything else, your drop-in files are read first. For most options, sshd keeps the first value it sees and ignores later ones. As a result, a line in a drop-in beats the same line further down the main file. Files are read in name order, so 10-hardening.conf beats 50-cloud-init.conf.

This affects cloud images. Some providers ship /etc/ssh/sshd_config.d/50-cloud-init.conf with PasswordAuthentication yes. We tested the precedence by adding that line in a 50- file and then in a 05- file:

# 10-hardening.conf says "no", 50-cloud-init.conf says "yes"
$ sshd -T -f sshd_config -C user=deploy,host=x,addr=192.0.2.10 | grep ^passwordauthentication
passwordauthentication no

# same "yes" line renamed to 05-test.conf
$ sshd -T -f sshd_config -C user=deploy,host=x,addr=192.0.2.10 | grep ^passwordauthentication
passwordauthentication yes

So check what else lives in that directory with ls /etc/ssh/sshd_config.d/. Also note that the stock main file has X11Forwarding yes uncommented, so you need to override it explicitly.

Socket activation and changing the port

Since Ubuntu 22.10, sshd uses systemd socket activation by default. ssh.socket holds port 22 open, and systemd starts ssh.service when the first connection arrives. On 24.04, a systemd generator (sshd-socket-generator) reads Port and ListenAddress from your sshd config and builds the socket settings from them.

Therefore, editing Port and running systemctl restart ssh is not enough. The comment in the 24.04 default sshd_config gives the official steps:

sudo systemctl daemon-reload
sudo systemctl restart ssh.socket

If you would rather go back to the classic always-running daemon, Canonical’s announcement lists these commands:

sudo systemctl disable --now ssh.socket
sudo rm -f /etc/systemd/system/ssh.service.d/00-socket.conf
sudo rm -f /etc/systemd/system/ssh.socket.d/addresses.conf
sudo systemctl daemon-reload
sudo systemctl enable --now ssh.service

We did not change the port or socket units in our lab, because that container shares system services. The commands above come from Ubuntu’s own config comment and announcement. In addition, remember to open the new port in your firewall before you switch. A non-standard port cuts log noise, but it does not stop a targeted attacker.

SSH hardening checklist: the tested drop-in file

This file is the core of the setup. Save it as /etc/ssh/sshd_config.d/10-hardening.conf. Before you do, create the group and add your admin users to it (see our guide to Linux user and group management):

sudo groupadd sshusers
sudo usermod -aG sshusers deploy

Here is the file. We tested it on Ubuntu 24.04 with OpenSSH 9.6p1 in our lab.

# /etc/ssh/sshd_config.d/10-hardening.conf
# Tested with OpenSSH 9.6p1 on Ubuntu 24.04. First value wins.

# Who may log in, and how
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey
AllowGroups sshusers

# Slow down guessing and drop dead sessions
MaxAuthTries 3
LoginGraceTime 30
MaxStartups 10:30:60
ClientAliveInterval 300
ClientAliveCountMax 2

# Turn off features most servers never use
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no

# Log the key fingerprint used for each login
LogLevel VERBOSE

A few notes on the choices:

  • AuthenticationMethods publickey is a second lock. Even if a later file turns passwords back on, sshd still demands a key.
  • AllowGroups is easier to manage than AllowUsers. If you prefer users, you can pin them to source addresses, for example AllowUsers deploy@192.0.2.0/24 backup. If you set both options, a user must match both.
  • ClientAliveInterval 300 with ClientAliveCountMax 2 drops an unresponsive session after about 10 minutes.
  • Leave UsePAM yes as Ubuntu ships it. PAM still handles account checks and session setup even when passwords are off.
  • AllowTcpForwarding no breaks tunnels and some IDE remote features. If a team needs them, enable forwarding only for that group with a Match Group block in its own drop-in.

We validated the syntax, then dumped the settings sshd would actually apply for a user called deploy:

$ sudo sshd -t -f /tmp/ssh-hardening/sshd_config && echo "syntax OK"
syntax OK
$ sudo sshd -T -f /tmp/ssh-hardening/sshd_config -C user=deploy,host=x,addr=192.0.2.10
...
permitrootlogin no
passwordauthentication no
kbdinteractiveauthentication no
x11forwarding no
allowtcpforwarding no
allowagentforwarding no
loglevel VERBOSE
allowgroups sshusers
authenticationmethods publickey
...

On a real server you drop the -f flag, because sshd reads /etc/ssh/sshd_config by default. A typo makes sshd -t stop with a clear error:

/tmp/ssh-hardening/bad.conf: line 1: Bad configuration option: PermitRootLogn
/tmp/ssh-hardening/bad.conf: terminating, 1 bad configuration options

We also started a throwaway sshd on a spare local port with this config. Key login for deploy worked. Password login returned Permission denied (publickey). Then root was refused, and a -L port forward was blocked. The log lines with LogLevel VERBOSE show which key was used:

Accepted publickey for deploy from 127.0.0.1 port 44658 ssh2: ED25519 SHA256:kFzQH7RzIJrWr8nwaN5JprscB2nieEqhBszuxpQZyE0
User root from 127.0.0.1 not allowed because none of user's groups are listed in AllowGroups
refused local port forward: originator 127.0.0.1 port 58222, target 127.0.0.1 port 22022

Modern crypto on OpenSSH 9.6

The defaults in 9.6 are already decent. For example, the first key exchange is sntrup761x25519-sha512@openssh.com, a hybrid of a post-quantum algorithm and X25519. It has been the default since OpenSSH 9.0. Our lab client negotiated it without any extra config:

debug1: kex: algorithm: sntrup761x25519-sha512@openssh.com
debug1: kex: host key algorithm: ssh-ed25519
debug1: kex: server->client cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none

Note that ssh -Q kex, ssh -Q cipher and ssh -Q mac list everything the build supports, including 3des-cbc and hmac-md5. That is not what the server offers. To see what sshd offers, use sshd -T | grep -E '^(kexalgorithms|ciphers|macs)'. Also, 9.6 added the “strict KEX” fix for the Terrapin attack (CVE-2023-48795), and it only protects you when both sides support it.

To trim the older options, add a second drop-in. This one follows the ordering in the ssh-audit project’s Ubuntu 24.04 policy, and it drops the ECDSA host key:

# /etc/ssh/sshd_config.d/20-crypto.conf
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
# Offer only the Ed25519 and RSA host keys (drops ECDSA)
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key

Keep the curve25519 entries for now. Older clients without the hybrid algorithm still need them.

Check it with ssh-audit

ssh-audit is maintained by Joe Testa at github.com/jtesta/ssh-audit. Ubuntu’s repo has version 3.1.0, which is old. We installed the current release (3.9.0) from PyPI in a virtual environment instead:

python3 -m venv ~/ssh-audit-venv
~/ssh-audit-venv/bin/pip install ssh-audit
~/ssh-audit-venv/bin/ssh-audit your-server.example.com

The project also publishes a snap (sudo snap install ssh-audit) and a Docker image. We ran it against our lab sshd and then against the built-in policy:

$ ssh-audit --skip-rate-test -P "Hardened Ubuntu Server 24.04 LTS (version 2)" -p 22022 127.0.0.1
Host:   127.0.0.1:22022
Policy: Hardened Ubuntu Server 24.04 LTS (version 2)
Result: ✔ Passed

The policy only passed once we used a 4096-bit RSA host key. With the stock 3072-bit key it failed on key size. Regenerating host keys changes their fingerprints, so every client will see a warning. Plan that change and publish the new fingerprints first. Without --skip-rate-test, ssh-audit also warns about the DHEat connection-flood issue (CVE-2002-20001). Its guide suggests rate limiting new connections at the firewall.

Keys: Ed25519, passphrases and FIDO2

Generate Ed25519 keys on the admin’s laptop, never on the server. We ran this in the lab:

$ ssh-keygen -t ed25519 -a 100 -C "deploy@laptop-2026"
Generating public/private ed25519 key pair.
...
The key fingerprint is:
SHA256:kFzQH7RzIJrWr8nwaN5JprscB2nieEqhBszuxpQZyE0 deploy@laptop-2026

Always set a passphrase. The -a 100 flag adds more key-derivation rounds, so a stolen key file is slower to crack. Then use ssh-agent so you type the passphrase once per session.

For admins, a FIDO2 security key is stronger still. The private key never leaves the hardware, and each login needs a touch. OpenSSH 9.6 supports sk-ssh-ed25519@openssh.com, which we confirmed with ssh -Q key. The command is below, but we did not run it, because our lab has no hardware key:

ssh-keygen -t ed25519-sk -O resident -O verify-required -C "admin-yubikey"

A safe rollout order

Follow this order on each server so that a mistake cannot lock you out:

  1. Keep your current SSH session open for the whole change. Existing sessions survive a reload.
  2. Confirm your key works and your user is in sshusers before you disable passwords.
  3. Write the drop-in, then run sudo sshd -t. Fix every error it prints.
  4. Run sudo systemctl reload ssh. On 24.04, the unit’s reload step runs sshd -t again before sending HUP.
  5. Open a second terminal and log in fresh. Only close the first session once that works.
  6. Have console access ready (cloud serial console, iDRAC or iLO) in case something still goes wrong.

Optional: TOTP as a second factor

You can add a time-based code on top of the key with libpam-google-authenticator. Ubuntu’s tutorial installs the package, adds auth required pam_google_authenticator.so to /etc/pam.d/sshd, and enables challenge-response login. In OpenSSH 9.6, ChallengeResponseAuthentication is a deprecated alias for KbdInteractiveAuthentication. To require a key and then a code, the sshd side looks like this:

KbdInteractiveAuthentication yes
AuthenticationMethods publickey,keyboard-interactive

We checked only that these two lines pass sshd -t. We did not test the PAM module or a full login. Test it on a non-production server first, and keep the emergency scratch codes somewhere safe.

Do not expose port 22 if you can avoid it

The most effective item on any SSH hardening checklist is limiting who can reach the port. If only your admins can connect, password guessing from the internet never reaches sshd. You have three common options:

  • A VPN such as WireGuard, with sshd reachable only from the VPN subnet.
  • A bastion host that admins reach first, using ssh -J bastion user@server.
  • An identity-aware access proxy such as Teleport or Tailscale SSH. Read our note on Tailscale SSH flaws and what to do before you rely on one blindly.

If port 22 must face the internet, at least allow only known admin IPs with ufw. We did not apply firewall rules in the lab container. However, this is standard ufw syntax:

sudo ufw allow from 203.0.113.10 to any port 22 proto tcp
sudo ufw deny 22/tcp
sudo ufw enable

Add the allow rule before you enable ufw, or you will cut yourself off.

Monitoring and host key checks

With LogLevel VERBOSE, every login records the key fingerprint, so you can tell which laptop was used. Watch journalctl -u ssh for failures. Then set up fail2ban as described in our post on detecting SSH brute force attacks with fail2ban.

On the client side, never accept a changed host key without checking why it changed. Our write-up on Tor relays and SSH host key warnings shows what that warning can mean. To get the server’s fingerprint for comparison, run ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on the console.

India angle

CERT-In’s Directions of 28 April 2022 require organisations to keep logs of their ICT systems for a rolling 180 days. Those logs must be kept within Indian jurisdiction. They also require reporting certain incidents within 6 hours. VERBOSE SSH logs, shipped off the server, help with both. Check your log retention against those Directions.

Printable SSH hardening checklist

Print this SSH hardening checklist and tick it off for each server.

SettingRecommended valueWhy
Drop-in file name10-hardening.confRead before 50-cloud-init.conf; first value wins
PermitRootLoginnoAdmins log in as themselves and use sudo
PasswordAuthenticationnoRemoves password guessing completely
KbdInteractiveAuthenticationnoCloses the PAM password prompt path
PubkeyAuthenticationyesKeys are the only way in
AuthenticationMethodspublickeyBackstop if another file re-enables passwords
PermitEmptyPasswordsnoNever allow blank passwords
AllowGroupssshusersOnly named people can log in
MaxAuthTries3Fewer guesses per connection
LoginGraceTime30Drops idle half-open logins faster
ClientAliveInterval / CountMax300 / 2Ends dead sessions after about 10 minutes
X11ForwardingnoUbuntu ships it as yes
AllowAgentForwardingnoStops agent hijack on a compromised server
AllowTcpForwardingno (or per group)Blocks unplanned tunnels
LogLevelVERBOSELogs the key fingerprint per login
UsePAMyes (default)Account and session checks still run
Crypto20-crypto.conf aboveDrops weak MACs and the ECDSA host key
User keysEd25519 with passphrase, or ed25519-skStrong keys; hardware for admins
Firewallallow admin IPs or VPN onlyPort 22 not open to everyone
  • ☐ sshd -t passes with no output
  • ☐ Fresh login tested in a second terminal
  • ☐ ssh-audit run, with findings reviewed
  • ☐ fail2ban or equivalent watching the logs

This post covers SSH only. For the rest of the server, work through our Linux server hardening checklist, or book a Linux server hardening audit if you want us to check it for you.

Sources

Running Linux servers in production? Our Linux server hardening & security audit checks configuration, patching, SSH and logging. You can also use our free Linux server hardening checklist, or contact us.

Post author avatar

Bhaskar Soni

Bhaskar Soni is a Security Engineer and the founder of Youngster Company, based in Ahmedabad, Gujarat. He works hands-on with Linux server hardening, vulnerability assessment and penetration testing (VAPT), ISO 27001 compliance and OSINT investigations. He writes practical, tested guides for IT professionals and Indian businesses, and every article on this site is written or reviewed by him.

Leave a Reply