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#

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/issue

sshd 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 sessions

The 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 no is 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, no is 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 -t before reload, never after. An invalid SSH configuration applied directly can irreversibly break remote access. sshd -t validates syntax without applying anything; systemctl reload ssh (as opposed to restart) preserves already-open sessions, which provides a safety net even against an error -t didn’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@server

Always 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.