LinuxXXVI · SSHsshd config
sshd configuration - hardening the server
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
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):
Matchblocks 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(orFailed 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:
| Keyword | Needs |
|---|---|
PerSourceMaxStartups | OpenSSH 9.5+ |
PerSourceNetBlockSize | OpenSSH 9.5+ |
PerSourcePenalties | OpenSSH 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
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
Q1. What is the right setting to disable password authentication in sshd_config?
Q2. A wrong sshd_config can lock you out of a remote host.
Q3. Which of the following should a production sshd_config include? Select all that apply.
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?
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.