SSH¶
SSH is the remote terminal, and on most Linux images it's the door an attacker tries first. The Mint benchmark, written for a workstation, simply says the server shouldn't be running (CIS 2.1.11); the Debian benchmark has a full section on hardening it (Debian CIS 5.1.1 to 5.1.24), and a contest README often requires it, so this page covers both cases. The item numbers below are the Debian ones. The server reads /etc/ssh/sshd_config and every file in /etc/ssh/sshd_config.d/. If the README doesn't require remote administration, stop and disable it; if it does, harden it.
Not required¶
sudo systemctl disable --now ssh
sudo ufw delete allow 22/tcp
Required: the settings¶
Edit /etc/ssh/sshd_config (sudo nano), one directive per line, no leading #. Files in sshd_config.d/ are read first and win, so check them for the same directives.
| Directive | Value | Why |
|---|---|---|
PermitRootLogin (5.1.20) |
no |
Root never logs in over the network; administrators log in as themselves and use sudo |
PasswordAuthentication |
no if the README's users have keys; otherwise yes with the lockout below |
Key-only login can't be brute-forced |
PermitEmptyPasswords (5.1.19) |
no |
A blank password can't be used remotely even if one exists |
MaxAuthTries (5.1.16) |
2 to 5 (use 3; the benchmark says 4 or fewer) | Attempts per connection before the server drops it |
LoginGraceTime (5.1.13) |
30 to 60 seconds (use 60) | An unauthenticated connection is dropped after this long, which limits how many an attacker can hold open |
ClientAliveInterval (5.1.7) |
60 to 600 seconds (use 300) with ClientAliveCountMax 3; STIG: 600 and 1 (UBTU-22-255030, 255035) |
Idle sessions are closed after interval × count with no response (the benchmark wants both above 0) |
DisableForwarding (5.1.8) |
yes |
One switch that turns off X11, TCP, agent, and tunnel forwarding together |
X11Forwarding |
no (STIG UBTU-22-255040, CAT I) |
No graphical forwarding through the tunnel |
X11UseLocalhost |
yes (STIG UBTU-22-255045) |
If X11 forwarding is ever on, only local clients can reach the display |
AllowTcpForwarding |
no |
No tunnelling other traffic through this server |
AllowAgentForwarding |
no |
A compromised server can't use the admin's forwarded keys |
PermitTunnel |
no |
No VPN-style tunnels |
HostbasedAuthentication (5.1.10) |
no |
No trust based on the connecting machine's name |
IgnoreRhosts (5.1.11) |
yes |
.rhosts files are ignored |
GSSAPIAuthentication (5.1.9) |
no |
Kerberos-style auth that isn't in use is one less code path |
MaxStartups (5.1.17) |
10:30:60 |
After 10 unauthenticated connections, new ones are dropped with rising probability until 60; blunts connection floods |
MaxSessions (5.1.18) |
10 or less | Sessions per connection |
Ciphers (5.1.6) |
remove 3des-cbc, aes128-cbc, aes192-cbc, aes256-cbc, chacha20-poly1305@openssh.com |
CBC ciphers are weak; chacha20 had the Terrapin bug (CVE-2023-48795). The STIG (UBTU-22-255050 to 255060) goes further with explicit allow-lists, below. |
KexAlgorithms (5.1.12, 5.1.23) |
remove diffie-hellman-group1-sha1, diffie-hellman-group14-sha1, diffie-hellman-group-exchange-sha1 |
SHA-1 key exchange |
MACs (5.1.15) |
remove the MD5, RIPEMD, SHA1-96, and umac-64 variants | Weak integrity checks |
ListenAddress (5.1.24, Level 2) |
the machine's management address | Only listens on one interface |
PermitUserEnvironment (5.1.21) |
no |
Users can't set environment variables at login, which could redirect commands |
Protocol |
2 (on old images; modern OpenSSH ignores it) |
SSH 1 is broken |
UsePAM (5.1.22) |
yes |
Login goes through the faillock and quality rules |
Banner (5.1.5) |
/etc/issue.net |
A warning banner before login |
AllowUsers or AllowGroups (5.1.4) |
The README's administrators | Only listed accounts can even try |
LogLevel (5.1.14) |
VERBOSE or INFO |
Logs the key fingerprint used, which the auth log otherwise omits |
Apply¶
Create a drop-in file. A name starting 00- is read before the main file and other drop-ins, so its values win:
sudo nano /etc/ssh/sshd_config.d/00-hardening.conf
Start with the four that matter most, one directive per line, no leading #:
PermitRootLogin no
PermitEmptyPasswords no
MaxAuthTries 3
AllowUsers alice bob
Then work down the table and add the rest, using the README's administrators on the AllowUsers line. For the three algorithm lines, a leading - means "remove these from the default list" rather than replacing the whole list, which is safer than typing an allow-list by hand:
Ciphers -3des-cbc,aes128-cbc,aes192-cbc,aes256-cbc,chacha20-poly1305@openssh.com
For a STIG baseline, use its FIPS allow-lists instead (no leading -): Ciphers aes256-ctr,aes256-gcm@openssh.com,aes192-ctr,aes128-ctr,aes128-gcm@openssh.com, MACs hmac-sha2-512,hmac-sha2-512-etm@openssh.com,hmac-sha2-256,hmac-sha2-256-etm@openssh.com, KexAlgorithms ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256. Both forms satisfy the CIS check; the allow-list is stricter and can refuse an old SSH client.
Before restarting, test the file, because a typo here locks you out:
sudo sshd -t
sudo systemctl restart ssh
sudo chmod 600 /etc/ssh/sshd_config /etc/ssh/sshd_config.d/*.conf
sshd -t prints nothing when the config is good and names the file and line when it isn't. The config file itself is root-only, mode 600 (5.1.1). Also fix the same directives in the main /etc/ssh/sshd_config where they're set the wrong way; some readers look only there.
Host keys¶
The server's private keys in /etc/ssh/ssh_host_*_key must be readable by root only (mode 600; Debian CIS 5.1.2) and the public halves 644 (5.1.3). World-readable host keys let anyone impersonate the server.
sudo chmod 600 /etc/ssh/ssh_host_*_key
sudo chmod 644 /etc/ssh/ssh_host_*_key.pub
Authorized keys¶
sudo find /home /root -name authorized_keys -exec sh -c 'echo "== $1"; cat "$1"' _ {} \;
A key in an account's ~/.ssh/authorized_keys is a password that never expires. The README says nothing about keys, so a key on an account is either the organization's (ask what the README says about remote access) or an attacker's. Remove lines you can't account for, and root's authorized_keys should be empty.
Verify¶
sudo sshd -T | grep -iE '^(permitrootlogin|passwordauthentication|permitemptypasswords|maxauthtries|logingracetime|clientaliveinterval|clientalivecountmax|disableforwarding|x11forwarding|allowtcpforwarding|permittunnel|permituserenvironment|hostbasedauthentication|ignorerhosts|gssapiauthentication|maxstartups|maxsessions|usepam|banner|allowusers)'
sudo sshd -T | grep -iE '^(ciphers|kexalgorithms|macs)' | grep -iE 'cbc|sha1|md5|umac-64' # nothing
stat -c '%a %n' /etc/ssh/ssh_host_*_key
sshd -T prints the effective configuration after all files are merged, which is the only reliable way to check.
Example¶
sshd -T shows permitrootlogin yes, maxauthtries 6, permitemptypasswords yes, x11forwarding yes. /etc/ssh/sshd_config.d/ contains 99-cloud.conf with PasswordAuthentication yes, which is fine because the README's users have no keys. Write the hardening drop-in, restart, re-run sshd -T. ls -l /etc/ssh/ssh_host_rsa_key shows -rw-r--r--; chmod 600. /root/.ssh/authorized_keys has one key labelled backdoor@kali; delete the file.
Try it¶
- Set
PermitRootLogin noand tryssh root@hostfrom another VM. ThenMaxAuthTries 3and type four wrong passwords. - Break the drop-in on purpose and run
sshd -tto see what the error looks like.
Build it¶
Your hardened 00-hardening.conf, kept as a file and copied in, followed by sshd -t and a restart. Then sshcheck.sh: sshd -T filtered to the directives on this page with the expected value beside each.