Skip to main content
RunBook Academy

LinuxXXVI · SSHsshd config

sshd configuration - hardening the server

Intermediate⏱ ~14 minsshd

What you'll learn

  • Configure sshd with a production-grade ruleset
  • Disable weak authentication methods and ciphers
  • Restrict root login and configure AllowGroups
  • Rate-limit connections with sshd's own controls rather than TCP wrappers
  • Validate Match-scoped authentication policy with sshd -T -C
  • Set up logging and intrusion detection signals

Prerequisites

Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-09

Not yet marked complete on this device.

sshd configuration is the most consequential security configuration on a Linux server. A weak sshd_config exposes the host to brute force, weak crypto, and credential theft. This lesson covers a production-grade hardening.

Where the config lives

  • Server: /etc/ssh/sshd_config.
  • Match overrides (per user/group/host): Match blocks at the bottom of the file.
  • Default values are documented in man 5 sshd_config. Always start from a known-good defaults file (e.g. the distribution’s /etc/ssh/sshd_config).

A production hardening baseline

# /etc/ssh/sshd_config

# Network
Port 22
AddressFamily any
ListenAddress 0.0.0.0
ListenAddress ::

# Logging
SyslogFacility AUTH
LogLevel VERBOSE

# Authentication
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication no   # keys only; set to yes if you add MFA (see below)
UsePAM yes
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 10
LoginGraceTime 30

# Connection-rate limits (built into sshd)
# NOTE: the PerSource* keywords are deliberately NOT in this baseline.
# They are newer than some of the platforms this course targets, and an
# unknown keyword stops sshd from starting. Add them separately, after
# checking your version - see "Version-gated keywords" below.
MaxStartups 10:30:60

# Key exchange and ciphers
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com

# Host keys
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key

# Disable legacy
UseDNS no    # skip reverse DNS lookups for performance and privacy

# Timeouts
ClientAliveInterval 300
ClientAliveCountMax 2

# Banner
Banner /etc/issue.net

Restrict user access

AllowGroups ssh-users admins
AllowUsers admin operator

AllowGroups allows any user in the listed groups. AllowUsers allows specific users by name. DenyUsers and DenyGroups take precedence.

For root:

PermitRootLogin no

Or restrict root to specific key-based auth:

PermitRootLogin prohibit-password
Match User root
    AuthenticationMethods publickey

Disable weak algorithms

The list above (KexAlgorithms, Ciphers, MACs) restricts to modern algorithms. Verify with:

ssh -vvv user@server 2>&1 | grep -E 'kex: |cipher: |mac:'

The output shows the negotiated algorithms.

Match blocks for per-user overrides

Match Group sftp-only
    ForceCommand internal-sftp
    PasswordAuthentication no
    AllowTcpForwarding no
    X11Forwarding no

Match Address 10.0.0.0/24
    PasswordAuthentication yes
    KbdInteractiveAuthentication yes
    AuthenticationMethods publickey,keyboard-interactive:pam

Match blocks may override only the subset of keywords listed at the end of the Match entry in man 5 sshd_config — principally the authentication, forwarding, access-control and ForceCommand keywords. Global settings such as Ciphers, MACs, KexAlgorithms, Port, ListenAddress, AddressFamily and HostKey cannot appear inside a Match block at all. sshd does not ignore them; it refuses to start:

/etc/ssh/sshd_config line 42: Directive 'Ciphers' is not allowed within a Match block

That failure is loud, which is the good case. It also means you cannot weaken (or strengthen) crypto per source address — the algorithm lists are process-wide, decided before sshd knows which user or address it is talking to. If you need different crypto for different populations, you need a second sshd instance on a second port, not a Match block.

Run sshd -t after every Match-block edit, and check the per-principal result with sshd -T -C user=bob,host=h.example.com,addr=10.0.0.5.

KbdInteractiveAuthentication yes in that block is not decoration. The baseline above sets it to no, and keyboard-interactive:pam — the mechanism every SSH MFA deployment uses — cannot run while it is disabled. Naming a method in AuthenticationMethods does not enable it.

Logging and monitoring

sshd logs to syslog (AUTH facility). To see live events:

sudo journalctl -t sshd -t sshd-session -f

Important events to alert on:

  • Failed password (or Failed publickey) - brute force or misconfiguration.
  • Invalid user - scanning for valid usernames.
  • Connection closed by authenticating user - half-completed auth (network issues, scanner).
  • Received disconnect from <IP> - connection drops.

Centralise the log to your SIEM. A host’s local log is forgotten on incident; the SIEM persists.

Rate limiting

sshd does rate-limit. The controls are built in and are all in man 5 sshd_config:

# /etc/ssh/sshd_config — available on every version this course targets
MaxStartups 10:30:60          # start dropping unauthenticated conns at 10,
                              # 30% chance, refuse all at 60
LoginGraceTime 30             # seconds to complete authentication
MaxAuthTries 3                # attempts per connection

Version-gated keywords

These three are worth having and are not safe to paste blindly:

KeywordNeeds
PerSourceMaxStartupsOpenSSH 9.5+
PerSourceNetBlockSizeOpenSSH 9.5+
PerSourcePenaltiesOpenSSH 9.8+

RHEL 9 ships OpenSSH 8.7p1 and Ubuntu 24.04 ships 9.6p1, so none of the three is safe on RHEL 9 and PerSourcePenalties is not safe on either.

An unknown keyword is fatal: sshd refuses to start, and if you find that out by reloading over SSH on a remote host, you have locked yourself out of it. Check first, and let sshd validate the file before anything restarts:

ssh -V                      # what you actually have
sshd -t && echo "config parses"

Add them in a drop-in rather than the main file, so backing the change out is deleting one file:

# The unit is ssh.service on Debian-family and sshd.service on RHEL-family;
# Debian ships sshd.service as an alias, so resolve it rather than guessing.
SSHD_UNIT=$(systemctl list-unit-files --no-legend 'ssh.service' 'sshd.service' \
  | awk 'NR==1{print $1}')

# only on OpenSSH >= 9.5, and drop the PerSourcePenalties line below 9.8
sudo tee /etc/ssh/sshd_config.d/60-per-source.conf >/dev/null <<'EOF'
PerSourceMaxStartups 3
PerSourceNetBlockSize 32:128
PerSourcePenalties yes
EOF
sudo sshd -t && sudo systemctl reload "$SSHD_UNIT"

Note that the drop-in directory is only read if the main sshd_config contains an Include /etc/ssh/sshd_config.d/*.conf line — Ubuntu and RHEL 9 both ship one, but confirm it with grep -i ^include /etc/ssh/sshd_config before relying on it.

MaxStartups caps concurrent connections that have not yet authenticated — the exact resource a brute-force flood consumes. PerSourcePenalties makes sshd track sources whose connections end badly (authentication failure, no auth, grace exceeded) and refuse them for a growing interval. Check what is in effect with sshd -T | grep -iE 'maxstartups|persource'.

To restrict SSH to one network, use the packet filter:

# Network-level restriction: this is what hosts.allow used to do
sudo nft add rule inet filter input tcp dport 22 ip saddr != 10.0.0.0/24 drop

fail2ban remains useful as a log-driven ban layer on top:

sudo apt install fail2ban
sudo systemctl enable --now fail2ban

fail2ban reads sshd logs and bans IPs with too many failures. It reacts after the fact; MaxStartups and PerSourcePenalties act during the flood. Use both.

Test before reload

sudo sshd -t                 # test config syntax
sudo sshd -T | less          # dump the effective global config
sudo systemctl reload sshd   # apply

sshd -t checks syntax and global consistency only. For anything set inside a Match block, resolve the effective policy for a specific connection:

sudo sshd -T -C addr=10.0.0.5,user=alice,host=jump.example.com

Always test from another terminal before disconnecting your own session. A wrong sshd_config can lock you out.

Banner /etc/issue.net

A legal notice warning that access is monitored. Not a security control; a legal one.

Knowledge check

Knowledge check · 5 questions

  1. Q1. What is the right setting to disable password authentication in sshd_config?

  2. Q2. A wrong sshd_config can lock you out of a remote host.

  3. Q3. Which of the following should a production sshd_config include? Select all that apply.

  4. Q4. You add a hosts.allow default-deny to restrict SSH to 10.0.0.0/24, run sshd -t, reload, and see no errors. What is the state of the host?

  5. Q5. A Match Address block for the management subnet sets AuthenticationMethods publickey,keyboard-interactive:pam while the global baseline keeps KbdInteractiveAuthentication no. sshd -t exits 0 and the reload succeeds. What happens next?

Passing score: 75%. Answers are checked in this browser.