crefft Source 2026.10.4

SSH Configuration Guide

crefft > Infrastructure > Services > SSH

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

Quick Reference

TaskCommand
Test config syntaxsshd -t
Show effective configsshd -T
Show config for one connectionsshd -T -C user=deploy,addr=10.0.0.5,host=h
Reload configsystemctl reload sshd
Check auth logjournalctl -u ssh -f
Generate keyssh-keygen -t ed25519
Copy keyssh-copy-id user@host

Table of Contents

  1. Server Configuration
  2. Authentication
  3. Access Control
  4. Cryptographic Settings
  5. Client Configuration
  6. Key Management
  7. fail2ban
  8. Monitoring

Server Configuration

Hardened sshd_config

Projects MUST apply these security settings:

# /etc/ssh/sshd_config.d/00-site-hardening.conf

# Protocol
Protocol 2

# Port: this value MUST match the fail2ban jails, firewall rules and client
# config. Changing it requires the coordinated procedure in "Changing the
# Listening Port".
Port 22

# Listen address (bind to specific interface if needed)
# ListenAddress 192.168.1.1
# ListenAddress 100.64.0.1  # Tailscale only

# Authentication
PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes

# Access control
AllowUsers deploy admin
# Or by group:
# AllowGroups ssh-users admins

# Security
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
AllowStreamLocalForwarding no
PermitTunnel no
GatewayPorts no
PermitUserEnvironment no
PermitUserRC no
StrictModes yes

# Session
ClientAliveInterval 300
ClientAliveCountMax 2
MaxAuthTries 3
MaxSessions 2
LoginGraceTime 60

# Logging
LogLevel VERBOSE
SyslogFacility AUTH

# Banners
Banner /etc/ssh/banner
PrintMotd no
PrintLastLog yes

Drop-in Precedence

Drop-in files MUST be named so that site policy is read before any vendor or cloud-init snippet. Projects MUST NOT rely on a 99- prefix to override earlier files.

Why: sshd_config is not last-wins. sshd_config(5) states that “for each keyword, the first obtained value will be used”, and Include splices matching files in lexical order at the point of the Include directive. Debian, Ubuntu, and RHEL place Include /etc/ssh/sshd_config.d/*.conf at the top of /etc/ssh/sshd_config, so 50-cloud-init.conf is read before 99-hardening.conf and wins every directive both files set.

# Don't: 50-cloud-init.conf is read first, so this file changes nothing
# /etc/ssh/sshd_config.d/99-hardening.conf
PasswordAuthentication no

# Do: sorts before vendor snippets, so its values are the first obtained
# /etc/ssh/sshd_config.d/00-site-hardening.conf
PasswordAuthentication no

Inventory the include tree and prove the effective value before deployment:

# Every file that contributes settings, in the order sshd reads them
grep -rn '^[[:space:]]*Include' /etc/ssh/sshd_config
ls -1 /etc/ssh/sshd_config.d/

# The value sshd actually uses, not the value the last file requested
sshd -T | grep -iE '^(passwordauthentication|permitrootlogin|pubkeyauthentication)'

Why: sshd -T prints the effective configuration after all includes are resolved. It is the only check that distinguishes a policy that is applied from one that is merely present in a file.

Validating and Rolling Out Changes

Projects MUST follow this order when changing authentication, allowlists, the listening port, or host keys. Every step MUST complete before the original administrative session is closed.

# 1. Back up the whole configuration, including drop-ins
install -d -m 700 /root/ssh-backup
cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.d /root/ssh-backup/

# 2. Check syntax (exits non-zero and prints the offending line on failure)
sshd -t

# 3. Check the effective global policy
sshd -T | grep -iE '^(port|permitrootlogin|passwordauthentication|allowusers|allowgroups)'

# 4. Check every Match block that must apply, one connection profile at a time
sshd -T -C user=deploy,addr=10.0.0.5,host=deploy.example.com | grep -i forcecommand
sshd -T -C user=admin,addr=10.0.0.6,host=admin.example.com | grep -i permittty

# 5. Confirm the account you will log in with survives the new policy
grep -c '^[^#]' ~admin/.ssh/authorized_keys   # key present
sshd -T | grep -i '^allowusers'               # account listed

# 6. Arm a timed rollback that fires if verification fails
cat > /usr/local/sbin/sshd-rollback <<'EOF'
#!/bin/sh
cp -a /root/ssh-backup/sshd_config /root/ssh-backup/sshd_config.d /etc/ssh/
systemctl reload sshd
EOF
chmod 700 /usr/local/sbin/sshd-rollback
systemd-run --unit=sshd-rollback --on-active=10min /usr/local/sbin/sshd-rollback

# 7. Reload (not restart - keeps existing connections)
systemctl reload sshd

# 8. Confirm the daemon is listening where you expect
ss -tlnp | grep sshd

# 9. From a SECOND terminal, open a new connection and prove it authenticates
ssh -o BatchMode=yes admin@server.example.com true && echo "login OK"

# 10. Only after step 9 succeeds, cancel the rollback and close the first session
systemctl stop sshd-rollback.timer

Why: sshd -t proves only that the file parses. It cannot prove that the intended administrator still matches AllowUsers, still has a usable key after PasswordAuthentication no, or that the daemon is reachable on a changed port. Ubuntu’s OpenSSH documentation warns that losing the SSH server can mean losing all access to the host, so the surviving session and the timed rollback are the recovery path.

If step 9 fails, restore from /root/ssh-backup in the still-open session and reload. If both sessions are lost, recovery requires console, serial, or hypervisor access.

# Don't: syntax check then reload, with no proof anyone can still log in
sshd -t && systemctl reload sshd

Configuration Files

PathPurpose
/etc/ssh/sshd_configMain server config
/etc/ssh/sshd_config.d/*.confDrop-in policy files, read in lexical order (Debian 12+)
/etc/ssh/ssh_host_*_keyHost private keys
/etc/ssh/ssh_host_*_key.pubHost public keys
/etc/ssh/bannerPre-login banner

Authentication

Key-Only Authentication

Projects MUST disable password authentication:

# /etc/ssh/sshd_config.d/00-site-hardening.conf
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no

Authorized Keys

# User's authorized keys
~/.ssh/authorized_keys

# Permissions (critical!)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

Key Options

Every authorized_keys record — options, key type, key material, and comment — MUST occupy one physical line. Restricted keys MUST be built from restrict, re-enabling only the capabilities that key needs.

# Basic key
ssh-ed25519 AAAA... user@host

# Restrict to specific source IP
from="192.168.1.0/24,10.0.0.5",restrict ssh-ed25519 AAAA... user@host

# Force specific command (deploy key)
restrict,command="/usr/local/bin/deploy.sh" ssh-ed25519 AAAA... deploy-key

# Read-only SFTP: -R denies uploads, renames, deletes and permission changes
restrict,command="internal-sftp -R" ssh-ed25519 AAAA... sftp-only

# Combined restrictions
from="10.0.0.0/8",restrict,command="/usr/bin/rsync --server --sender ." ssh-ed25519 AAAA... backup

Why: sshd(8) specifies that each line of the file contains one key, with the optional options field on the same line as the key. There is no line continuation syntax. restrict (OpenSSH 7.2 and later) disables port, agent, and X11 forwarding, PTY allocation and ~/.ssh/rc execution, and automatically includes any restriction added by later releases; an explicit no-* list silently omits whatever it does not name.

Why (-R): internal-sftp and sftp-server allow writes wherever filesystem permissions permit them. sftp-server(8) documents -R as the option that places the server in read-only mode and denies operations that change the state of the filesystem. A forced command without -R is not a read-only service, whatever the key comment says.

# Don't: a backslash does not continue an authorized_keys record
command="internal-sftp",no-port-forwarding,no-agent-forwarding \
  ssh-ed25519 AAAA... sftp-only

The wrapped form is two records, not one. Line 1 is discarded as malformed and line 2 is accepted as a plain, unrestricted key: it grants a shell, forwarding, and a PTY to anyone holding that private key.

Verify authorized keys after every edit:

# No record may end with a backslash
grep -n '\\$' ~/.ssh/authorized_keys && echo "BROKEN: wrapped record"

# Every key sshd will accept, one line per record
ssh-keygen -l -f ~/.ssh/authorized_keys

# Prove the read-only SFTP key: the download succeeds, the upload is refused
printf 'get readme.txt\nput local.txt\n' | sftp -b - sftp-only@server.example.com
# sftp> get readme.txt
# sftp> put local.txt
# dest open "/local.txt": Permission denied

Certificate-Based Authentication

For larger deployments, projects SHOULD use SSH certificates. Projects MUST use one CA for user keys and a separate CA for host keys, and MUST complete the installation steps on both sides; a signed key that is never installed and configured changes nothing.

Why: TrustedUserCAKeys must name the file the user CA public key was actually copied to, HostCertificate must name the signed host certificate because sshd loads no certificate by default, and clients must trust the host CA through an @cert-authority line in known_hosts. Signing alone leaves the estate on raw-key trust while appearing to be certificate-based.

User Certificates

# 1. On an offline CA host: create the user CA, protected by a passphrase
ssh-keygen -t ed25519 -f user_ca -C "user CA"

# 2. Sign a user key; principals MUST be the account names the holder may use
ssh-keygen -s user_ca -I alice@example.com -n alice -V +30d alice_ed25519.pub

# 3. Check what you signed before shipping it
ssh-keygen -L -f alice_ed25519-cert.pub

# 4. Install the CA public key on every server, at the configured path
install -o root -g root -m 644 user_ca.pub /etc/ssh/trusted_user_ca.pub
# /etc/ssh/sshd_config.d/00-site-ca.conf
TrustedUserCAKeys /etc/ssh/trusted_user_ca.pub

The client offers alice_ed25519-cert.pub automatically when it sits beside the private key alice_ed25519.

Host Certificates

# 1. On the CA host: create the host CA
ssh-keygen -t ed25519 -f host_ca -C "host CA"

# 2. Sign the server's public host key; principals MUST cover every name and
#    address clients use to reach it
ssh-keygen -s host_ca -I server.example.com -h \
  -n server.example.com,10.0.0.10 -V +52w ssh_host_ed25519_key.pub

# 3. Install the certificate beside its private key on the server
install -o root -g root -m 644 ssh_host_ed25519_key-cert.pub /etc/ssh/
# /etc/ssh/sshd_config.d/00-site-hostkeys.conf
HostKey /etc/ssh/ssh_host_ed25519_key
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
# On every client: trust the host CA for the estate, on one physical line
# ~/.ssh/known_hosts
@cert-authority *.example.com ssh-ed25519 AAAA... host CA

Validate before and after reloading:

# Silent means the certificate pairs with its private key; "No matching private
# key for certificate" means the wrong file is configured
sshd -t

# Confirm both trust anchors are loaded
sshd -T | grep -iE '^(hostkey|hostcertificate|trusteduserca)'

# Confirm the client trusts the host CA for this name
ssh-keygen -F server.example.com -f ~/.ssh/known_hosts

# Connect; -v reports the certificate the client accepted
ssh -v alice@server.example.com true 2>&1 | grep -i 'host certificate'
# Don't: sign keys, configure a path that was never created, and stop there
ssh-keygen -t ed25519 -f ssh-ca -C "SSH CA"
# TrustedUserCAKeys /etc/ssh/ca.pub   # ssh-ca.pub was never installed here,
# so every user certificate is rejected and the host certificate is unused

Access Control

AllowUsers / AllowGroups

Projects MUST explicitly allow users or groups. Each policy below is a complete alternative and MUST be deployed on its own; they are not interchangeable fragments of one block.

# Do: user allowlist only. Any listed account may log in from anywhere.
AllowUsers alice bob deploy
# Do: group allowlist only (preferred for larger teams). Membership of
# ssh-users or admins is sufficient; no per-user list is consulted.
AllowGroups ssh-users admins
# Do: deliberate intersection. alice and bob MUST also be in ssh-users or
# admins, and deploy MUST additionally connect from 10.0.0.*
AllowUsers alice bob deploy@10.0.0.*
AllowGroups ssh-users admins
# Don't: three policies in one file. Repeated AllowUsers appends patterns, so
# the bare "deploy" entry still permits deploy from any address, and alice is
# locked out unless she is also in ssh-users or admins.
AllowUsers alice bob deploy
AllowGroups ssh-users admins
AllowUsers deploy@10.0.0.*

Why: default SSH allows all local users, so an explicit allowlist prevents access when a new account is created. AllowUsers and AllowGroups are evaluated as separate gates and a login must pass every gate that is set, but the patterns within one keyword are alternatives, and a repeated keyword appends to the list rather than narrowing it. sshd checks DenyUsers, then AllowUsers, then DenyGroups, then AllowGroups; a user matched by a Deny list is rejected before any Allow list is consulted.

Prove the policy before closing the session that deployed it:

# Every pattern sshd holds, expanded one per line
sshd -T | grep -iE '^(allowusers|allowgroups|denyusers|denygroups)'

# One live login per intended account, from a second terminal
ssh -o BatchMode=yes alice@server.example.com true && echo "alice OK"
ssh -o BatchMode=yes deploy@server.example.com true && echo "deploy OK"

DenyUsers / DenyGroups

# Deny specific users (checked before AllowUsers)
DenyUsers guest test

# Deny groups (checked before AllowGroups)
DenyGroups no-ssh

Match Blocks

Apply settings to specific users/groups/hosts:

# /etc/ssh/sshd_config.d/00-site-match.conf

# SFTP-only users
Match Group sftp-only
    ForceCommand internal-sftp
    ChrootDirectory /srv/sftp/%u
    DisableForwarding yes
    PermitTTY no
    PermitUserRC no

# Restricted deploy user
Match User deploy
    ForceCommand /usr/local/bin/deploy.sh
    DisableForwarding yes
    PermitTTY no
    PermitUserRC no

# Allow forwarding for specific user
Match User tunnel-user
    AllowTcpForwarding yes

Why: ForceCommand restricts the command, not the channels around it. Forwarding stays available unless it is disabled explicitly, and AllowStreamLocalForwarding (Unix-domain sockets) and PermitUserRC both default to yes. DisableForwarding yes closes TCP, Unix-socket, agent, X11, and tunnel forwarding in one directive, and covers any forwarding type added by a later release.

Version note: DisableForwarding exists from OpenSSH 7.4, but until OpenSSH 10.0 it failed to disable X11 and agent forwarding as documented. On releases before 10.0, set X11Forwarding no and AllowAgentForwarding no alongside it — as the baseline in Hardened sshd_config already does.

Restrictions MUST be applied per role. Do not add DisableForwarding yes to a bastion account: ProxyJump needs TCP forwarding on the jump host.

Verify each role from its own connection profile:

sshd -T -C user=deploy,addr=10.0.0.5,host=deploy.example.com | \
  grep -iE '^(forcecommand|disableforwarding|permittty|permituserrc)'

Cryptographic Settings

Modern Algorithms Only

Projects MUST disable legacy algorithms:

# /etc/ssh/sshd_config.d/00-site-crypto.conf

# Key exchange (most secure first)
KexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org

# Host key algorithms
HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512

# Ciphers
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com

# MACs
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com

Host Keys

Configure which keys the daemon offers:

# /etc/ssh/sshd_config.d/00-site-hostkeys.conf
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_rsa_key

A host key is a trust anchor that every existing client has already recorded. Rotation MUST be staged: advertise the replacement alongside the current key, let clients learn it, then retire the old key. Projects MUST NOT delete host keys before replacements are generated, configured, and verified.

Why: ssh_config(5) documents UpdateHostKeys — enabled by default for clients that have not overridden UserKnownHostsFile — as the option that “supports graceful key rotation by allowing a server to send replacement public keys before old ones are removed”. sshd advertises every configured HostKey to the client after authentication, so the overlap period is what makes rotation invisible to clients. Deleting first removes the anchor before any replacement has been advertised: clients get a host key verification failure that is indistinguishable from an attack, and a failed regeneration leaves the daemon with no key to start with.

# Don't: this destroys every trust anchor before a replacement exists
rm /etc/ssh/ssh_host_*
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

Staged rotation:

# 1. Record the current fingerprints through an out-of-band channel
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

# 2. Generate the replacement alongside the existing key, never over it
ssh-keygen -t ed25519 -N "" -C "server.example.com 2026-09" \
  -f /etc/ssh/ssh_host_ed25519_key.new
# 3. Advertise both. The current key stays first, so it remains the key
#    offered during negotiation while clients learn the replacement.
# /etc/ssh/sshd_config.d/00-site-hostkeys.conf
HostKey /etc/ssh/ssh_host_ed25519_key
HostKey /etc/ssh/ssh_host_ed25519_key.new
HostKey /etc/ssh/ssh_host_rsa_key
# 4. Validate, reload and confirm both keys are loaded
sshd -t && systemctl reload sshd
sshd -T | grep -i '^hostkey '

# 5. Publish the replacement fingerprint on the same out-of-band channel
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.new.pub

# 6. Let every client connect at least once during the overlap window; clients
#    with UpdateHostKeys enabled record the replacement automatically
ssh -o UpdateHostKeys=yes admin@server.example.com true
# 7. Retire only after the window has covered every client: drop the old
#    HostKey line, reload, then archive rather than delete the old files
# /etc/ssh/sshd_config.d/00-site-hostkeys.conf
HostKey /etc/ssh/ssh_host_ed25519_key.new
HostKey /etc/ssh/ssh_host_rsa_key
sshd -t && systemctl reload sshd
install -d -m 700 /root/ssh-hostkey-archive
mv /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_ed25519_key.pub \
  /root/ssh-hostkey-archive/

# 8. Optional: restore the canonical filename. This renames the same key
#    material, so no client has to learn anything again.
mv /etc/ssh/ssh_host_ed25519_key.new /etc/ssh/ssh_host_ed25519_key
mv /etc/ssh/ssh_host_ed25519_key.new.pub /etc/ssh/ssh_host_ed25519_key.pub
# then point HostKey back at /etc/ssh/ssh_host_ed25519_key, sshd -t, reload

Rollback: while the archived files exist, restoring the old HostKey line and reloading restores the previous identity. Repeat the procedure for every host key type in use. Retire unwanted DSA or ECDSA host keys the same way — remove the HostKey line, reload, then archive the files — rather than deleting them from a running server.

For a client that missed the overlap window, verify the published fingerprint before trusting the new key:

ssh-keyscan -t ed25519 server.example.com | ssh-keygen -lf -   # compare by eye
ssh-keygen -R server.example.com                               # drop stale entry

Certificate variant: where the host is authenticated by a host certificate, clients trust the CA rather than the key, so sign the replacement host key with the host CA and install the new certificate (see Host Certificates). UpdateHostKeys is not used for connections authenticated by a certificate.


Client Configuration

User SSH Config

# ~/.ssh/config

# Defaults for all hosts
Host *
    AddKeysToAgent yes
    IdentitiesOnly yes
    ServerAliveInterval 60
    ServerAliveCountMax 3

# Specific host
Host prod
    HostName prod.example.com
    User deploy
    IdentityFile ~/.ssh/deploy_ed25519
    Port 2222

# Jump host (bastion)
Host internal-*
    ProxyJump bastion.example.com
    User admin

Host bastion.example.com
    User admin
    IdentityFile ~/.ssh/bastion_ed25519

# Git hosting
Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/github_ed25519

Host gitlab.com
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/gitlab_ed25519

ProxyJump (Bastion)

# Direct command
ssh -J bastion.example.com internal-server

# Via config (above)
ssh internal-server

# Multiple jumps
ssh -J bastion1,bastion2 internal-server

Key Management

Generate Keys

Projects MUST use Ed25519 for new keys:

# Ed25519 (recommended)
ssh-keygen -t ed25519 -C "user@example.com" -f ~/.ssh/id_ed25519

# RSA (for legacy systems)
ssh-keygen -t rsa -b 4096 -C "user@example.com" -f ~/.ssh/id_rsa

# With specific filename
ssh-keygen -t ed25519 -C "deploy@prod" -f ~/.ssh/deploy_ed25519

Key Types

TypeKey SizeSecurityUse Case
Ed25519256-bitExcellentDefault choice
RSA4096-bitGoodLegacy compatibility
ECDSA256-bitGoodAvoid (NSA curve concerns)
DSA1024-bitWeakNever use

SSH Agent

# Start agent
eval "$(ssh-agent -s)"

# Add key (with passphrase prompt)
ssh-add ~/.ssh/id_ed25519

# List loaded keys
ssh-add -l

# Add with timeout (1 hour)
ssh-add -t 3600 ~/.ssh/id_ed25519

# macOS: Use Keychain
ssh-add --apple-use-keychain ~/.ssh/id_ed25519

Key Rotation

Projects SHOULD rotate keys annually:

  1. Generate new key
  2. Add new public key to authorized_keys
  3. Test new key
  4. Remove old public key from authorized_keys
  5. Archive or destroy old private key

fail2ban

Installation

apt install fail2ban

SSH Jail Configuration

The jail’s port MUST be the numeric port sshd actually listens on.

# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
port = 22
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
ignoreip = 127.0.0.1/8 10.0.0.0/8 192.168.0.0/16

Why: Fail2Ban passes the jail’s port straight into the banning action, so the firewall rule it installs blocks that port and no other. The upstream default port = ssh resolves through /etc/services to 22; on a host moved to another port, detection still works while every ban lands on port 22 and the attacker keeps connecting.

Aggressive Mode

# /etc/fail2ban/jail.d/sshd-aggressive.local
[sshd-aggressive]
enabled = true
port = 22
filter = sshd[mode=aggressive]
logpath = /var/log/auth.log
maxretry = 1
findtime = 86400
bantime = 604800

Changing the Listening Port

A non-standard port reduces log noise but does not add security. Projects that change it MUST change every dependent setting in the same maintenance window, using the rollout procedure in Validating and Rolling Out Changes.

# 1. Server: /etc/ssh/sshd_config.d/00-site-hardening.conf
Port 2222

# 2. Firewall: allow the new port BEFORE reloading sshd; withdraw the old
#    allowance only after step 5 of the rollout procedure has succeeded
ufw allow 2222/tcp

# 3. RHEL/Fedora only: label the port for SELinux
semanage port -a -t ssh_port_t -p tcp 2222

# 4. Every fail2ban jail for this service
sed -i 's/^port = 22$/port = 2222/' /etc/fail2ban/jail.d/sshd*.local

# 5. Clients: ~/.ssh/config
#    Host prod
#        Port 2222

Verify every step before closing the session that made the change:

sshd -T | grep -i '^port'                          # daemon policy
ss -tlnp | grep sshd                               # daemon is listening
fail2ban-client -d | grep -o "\['port', '[^']*'\]" # jail action port
nft list chain inet f2b-table f2b-chain            # rule installed by the action

Management Commands

# Status
fail2ban-client status sshd

# Unban IP
fail2ban-client set sshd unbanip 192.168.1.100

# Ban IP manually
fail2ban-client set sshd banip 192.168.1.100

# Reload config
fail2ban-client reload

Monitoring

Log Locations

DistributionAuth Log
Debian/Ubuntu/var/log/auth.log
RHEL/Fedora/var/log/secure
systemdjournalctl -u ssh

Useful Commands

# Recent SSH logins
journalctl -u ssh --since "1 hour ago"

# Failed login attempts
grep "Failed password" /var/log/auth.log | tail -20

# Successful logins
grep "Accepted" /var/log/auth.log | tail -20

# Connection attempts by IP
grep "sshd" /var/log/auth.log | grep -oP '\d+\.\d+\.\d+\.\d+' | \
  sort | uniq -c | sort -rn | head

# Currently connected
who
w

# Active SSH sessions
ss -tnp | grep ssh

Log Verbosity

For debugging:

# /etc/ssh/sshd_config
LogLevel DEBUG3  # Very verbose
LogLevel VERBOSE # Recommended for production
LogLevel INFO    # Default

Quick Hardening Checklist

  • PermitRootLogin no
  • PasswordAuthentication no
  • PubkeyAuthentication yes
  • AllowUsers or AllowGroups configured — one policy, not both by accident
  • Site policy in a drop-in that sorts before vendor snippets (00-*.conf)
  • Effective values confirmed with sshd -T and sshd -T -C user=...
  • Modern ciphers/MACs/KEX only
  • X11Forwarding no
  • AllowTcpForwarding no (unless needed)
  • AllowStreamLocalForwarding no and PermitUserRC no
  • Every authorized_keys record on one physical line, starting from restrict
  • MaxAuthTries 3
  • LoginGraceTime 60
  • fail2ban installed and enabled, with port equal to the SSH listener
  • Host keys are Ed25519/RSA only
  • LogLevel VERBOSE
  • Second session verified before the deploying session is closed

See Also


References