SSH hardening#
Why, and why this early#
SSH is the very first service exposed on this server: it’s what the connection at the end of the previous chapter went through. It’s also, by a wide margin, the most scanned target on the Internet: a freshly deployed server receives automated SSH connection attempts within minutes, well before chrony, nginx, or the firewall are even configured.
This chapter is deliberately placed right after first boot, not at the end of the install. In many tutorials, hardening comes last, once “everything works”, that’s backwards from a security standpoint: the window between first boot (SSH in default configuration) and the end of a full install can span hours, even days for a project spread across multiple sessions. Every minute spent with unhardened SSH on a public IP is wasted exposure. Hardening SSH is therefore the very first thing to do after first boot, before installing a single application package.
This guide doesn’t claim to be exhaustive on security. It covers the measures judged relevant for this specific project, not a complete reference framework. Genuinely thorough hardening depends on the context (network exposure, regulatory constraints, criticality) and deserves a dedicated effort, see the Lynis chapter, later on, for an audit method that goes beyond what’s shown here.
How#
Pre-authentication legal banner#
cat > /etc/issue.net <<'EOF'
*** WARNING, AUTHORIZED ACCESS ONLY ***
This system is restricted to authorized administrators.
All connections are logged and audited.
Disconnect IMMEDIATELY if you are not an authorized user.
EOF
cp /etc/issue.net /etc/issuesshd configuration#
cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
# Pre-authentication legal banner and verbose logging
Banner /etc/issue.net
LogLevel VERBOSE
# Modern algorithms only, hybrid post-quantum first
KexAlgorithms mlkem768x25519-sha256,curve25519-sha256@libssh.org,curve25519-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
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
# Session restrictions
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 4
ClientAliveInterval 300
ClientAliveCountMax 2
AllowUsers user
# Disable unused features
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
GSSAPIAuthentication no
HostbasedAuthentication no
UseDNS no
EOF
sshd -t # validate BEFORE reload
systemctl reload ssh # preserves active sessionsThe four algorithm lists (KexAlgorithms, HostKeyAlgorithms, Ciphers,
MACs) contain only modern choices: no CBC-mode suites, no SHA-1-based
MAC, both historically vulnerable to known attacks on SSH. The -etm
suffix (encrypt-then-MAC) is preferred over the older ordering, a
construction recognized as more robust against padding-oracle-style
attacks. Each list’s order is also a preference order: the first
algorithm mutually supported by the client is the one actually
negotiated.
The first algorithm in this list, mlkem768x25519-sha256, is a
post-quantum hybrid key exchange (native since OpenSSH 9.9, used by
default since OpenSSH 10.0): the combination mechanism and its rationale
are detailed in the Post-quantum chapter, which
covers the same hybridization on the TLS and SSH sides. ANSSI recommends
this hybridization approach and confirms its native availability in
OpenSSH in its technical note Transition post-quantique de SSHv2
(ANSSI-FT-116, February 2026).
On the session-restriction side, the values are deliberately stricter
than OpenSSH’s defaults: LoginGraceTime 30 (instead of the 120s
default) shrinks the window during which an unauthenticated connection
stays open, limiting the impact of many slow connections deliberately
kept open. MaxAuthTries 3 (instead of 6) limits the number of
authentication attempts per connection before it’s closed. MaxSessions 4 caps the number of multiplexed sessions on a single network
connection. ClientAliveInterval 300 with ClientAliveCountMax 2 sends
an application-level probe every 5 minutes and closes the connection
after 2 consecutive failures (10 minutes with no response): enough to
reclaim a dead or stuck connection’s resources without penalizing normal
interactive use. UseDNS no disables reverse-DNS resolution of the
client’s IP at connection time: it provides no actionable security
guarantee (an attacking IP usually also controls its own reverse DNS)
and adds connection latency for nothing.
PasswordAuthentication nois the default recommended here, for a server exposed on the Internet without upstream network restriction on port 22. Some architectures compensate differently (SSH reachable only from a private admin LAN through the firewall, see the nftables chapter, later in this guide) and can then afford to keep password auth as a recovery fallback. Without an equivalent network compensation,nois the safer default, provided a working key and an out-of-band recovery channel (serial console or HDMI) are available before cutting off password authentication.
Pitfalls to avoid#
Always validate
sshd -tbeforereload, never after. An invalid SSH configuration applied directly can irreversibly break remote access.sshd -tvalidates syntax without applying anything;systemctl reload ssh(as opposed torestart) preserves already-open sessions, which provides a safety net even against an error-tdidn’t catch, always keep an active SSH session open while testing a new configuration.
- Waiting until the install is “finished” to harden SSH. This is exactly the anti-pattern this chapter aims to break (see above). Harden now, not at the end.
- Cutting off password authentication without verifying the public key first. Explicitly test a key-based connection before disabling passwords, in a separate session, without closing the current one.
Verify#
# Effective SSH config (not just the written file)
sshd -T | grep -iE "permitrootlogin|pubkeyauth|passwordauth|allowusers"
# From a DIFFERENT session (without closing this one):
ssh -o PreferredAuthentications=publickey user@serverAlways keep a second access channel available (serial console, HDMI, or physical access) during any SSH configuration change, that’s the safety net that turns a configuration mistake into a simple fix rather than an incident requiring physically pulling the SD card.
The rest of system hardening (sysctl, audit, permissions) is covered in
the next chapter. The firewall,
fail2ban, and the full Lynis audit come later in this guide, once the
services they protect are actually installed.