SSH Hardening
SSH is the primary remote administration path into most Linux, network, and appliance infrastructure โ which makes a misconfigured SSH daemon one of the highest-value targets on the network. A compromised SSH service typically means full shell access. This guide covers hardening OpenSSH (client and server) aligned to the CIS Benchmarks (Distribution Independent Linux, section 5.2 "Configure SSH Server"), NIST SP 800-53 Rev. 5 control mappings, and NIST SP 800-123 (Guide to General Server Security).
Scope: OpenSSH server (
sshd) and client configuration on Linux/Unix hosts. Network device SSH (Cisco, Juniper, etc.) follows the same principles โ key-based or MFA auth, strong algorithms, restricted access โ implemented via vendor-specific syntax.
1. Authentication โ Eliminate Password Auth
Password authentication over SSH is the single highest-risk default. It is subject to credential stuffing, brute force, and reuse from breached credential dumps. CIS and NIST both treat this as a baseline control, not optional hardening.
Enforce Public Key Authentication
# /etc/ssh/sshd_config
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitEmptyPasswords no
AuthenticationMethods publickey
- Use Ed25519 keys for new deployments (
ssh-keygen -t ed25519) โ smaller, faster, and resistant to the implementation weaknesses that have affected some RSA/ECDSA usage. Where FIPS 140-3 validated modules are required, use RSA 3072-bit or ECDSA P-256/P-384. - Never generate or distribute keys with no passphrase for interactive human users. Passphrase-protect private keys and rely on
ssh-agentfor session convenience. - Rotate keys on personnel offboarding, suspected compromise, or a fixed schedule (e.g., annually for standing access).
# Generate a modern key
ssh-keygen -t ed25519 -a 100 -C "user@host-purpose"
# FIPS-constrained environments
ssh-keygen -t rsa -b 3072 -C "user@host-purpose"
Disable Root Login
PermitRootLogin no
Administrators should authenticate as a named user and elevate via sudo, preserving individual accountability in audit logs. If emergency direct root access is unavoidable (e.g., console/OOB only), scope it there โ never over the network.
Multi-Factor Authentication
Align to NIST SP 800-63B IAL2/AAL2 for privileged remote access by layering MFA on top of public key auth:
# /etc/ssh/sshd_config
AuthenticationMethods publickey,keyboard-interactive
# /etc/pam.d/sshd โ example with Google Authenticator PAM module
auth required pam_google_authenticator.so
For enterprise environments, prefer centrally managed MFA (Duo, FIDO2/WebAuthn hardware keys via OpenSSH's native sk-ecdsa-sha2-nistp256@openssh.com key types, or a SAML/OIDC-backed SSH CA) over per-host TOTP secrets, which are harder to revoke at scale.
2. Access Control โ Restrict Who Can Connect
Allow-List Users and Groups
Deny-by-default; explicitly allow only the accounts and groups that require SSH access.
AllowUsers admin_jsmith admin_mchen svc_backup
AllowGroups ssh-users sysadmins
DenyUsers root
DenyGroups guest
Restrict by Network Source
Layer OS-level and network-level controls rather than relying on sshd_config alone:
# iptables โ allow SSH only from the management network / bastion
iptables -A INPUT -p tcp --dport 22 -s 10.10.5.0/24 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
# /etc/hosts.allow and /etc/hosts.deny (TCP wrappers, where supported)
sshd : 10.10.5.0/255.255.255.0
sshd : ALL
Use a Bastion / Jump Host
Do not expose SSH on production hosts directly to user networks or the internet. Route all interactive SSH through a hardened, heavily monitored bastion:
# ~/.ssh/config on the client
Host bastion
HostName bastion.internal.example.com
User jsmith
Host prod-*
ProxyJump bastion
The bastion should have its own MFA enforcement, session recording, and be the only host with an SSH listener reachable from broader internal networks. This directly supports NIST SP 800-53 AC-17 (Remote Access), which requires monitoring and control of remote access paths.
3. Cryptographic Hardening โ Ciphers, MACs, Key Exchange
Disable legacy algorithms with known weaknesses (CBC-mode ciphers vulnerable to plaintext-recovery attacks, MD5/SHA-1 based MACs, Diffie-Hellman groups below 2048-bit). This is a direct CIS Benchmark requirement (5.2.11โ5.2.13) and supports NIST SP 800-131A cryptographic algorithm transitions.
# /etc/ssh/sshd_config โ CIS/NIST-aligned algorithm restriction
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256
HostKeyAlgorithms ssh-ed25519,rsa-sha2-512,rsa-sha2-256
Never re-enable for compatibility with legacy clients:
| Algorithm class | Deprecated / insecure |
|---|---|
| Ciphers | arcfour*, blowfish-cbc, 3des-cbc, *-cbc (CBC mode generally) |
| MACs | hmac-md5*, hmac-sha1 (non-ETM), umac-64* |
| Key exchange | diffie-hellman-group1-sha1, diffie-hellman-group14-sha1, any group < 2048-bit |
| Host keys | ssh-dss (DSA โ removed upstream), ssh-rsa with SHA-1 signatures |
Verify what a running daemon actually offers:
ssh -Q cipher
ssh -Q mac
ssh -Q kex
nmap --script ssh2-enum-algos -p 22 <host>
Protocol Version
Protocol 2
SSH Protocol 1 is cryptographically broken and removed entirely from modern OpenSSH โ confirm no legacy device on the network still negotiates it.
4. Session and Connection Controls
# /etc/ssh/sshd_config
LoginGraceTime 30
MaxAuthTries 3
MaxSessions 4
MaxStartups 10:30:60
ClientAliveInterval 300
ClientAliveCountMax 2
TCPKeepAlive no
- MaxAuthTries limits credential-guessing attempts per connection (CIS 5.2.16).
- ClientAliveInterval / ClientAliveCountMax terminate idle sessions (300s x 2 = 10-minute idle timeout) โ CIS 5.2.20, and supports NIST SP 800-53 AC-12 (Session Termination).
- LoginGraceTime bounds how long an unauthenticated connection can hold a slot, mitigating connection-exhaustion DoS.
- MaxStartups throttles concurrent unauthenticated connections.
Disable Unused Forwarding and Features
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
GatewayPorts no
PermitTunnel no
PermitUserEnvironment no
Only enable AllowAgentForwarding, AllowTcpForwarding, or X11Forwarding where there's an explicit operational need (e.g., a bastion that legitimately needs AllowTcpForwarding for ProxyJump) โ each is additional attack surface for pivoting.
Banner
Banner /etc/issue.net
Display an authorized-use warning banner before authentication (CIS 5.2.21) โ a legal/administrative control supporting NIST SP 800-53 AC-8 (System Use Notification), not a technical one.
5. Host Key Management
- Generate host keys only in Ed25519 and RSA (3072+) โ remove or don't generate DSA/ECDSA-P256 host keys if not required:
# Remove weak/legacy host keys
rm -f /etc/ssh/ssh_host_dsa_key*
rm -f /etc/ssh/ssh_host_ecdsa_key*
# Regenerate strong host keys if needed
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ''
ssh-keygen -t rsa -b 3072 -f /etc/ssh/ssh_host_rsa_key -N ''
- File permissions (CIS 5.2.1โ5.2.4): host private keys must be root-owned and unreadable by any other principal.
chown root:root /etc/ssh/ssh_host_*_key
chmod 600 /etc/ssh/ssh_host_*_key
chown root:root /etc/ssh/sshd_config
chmod 600 /etc/ssh/sshd_config
- Distribute host key fingerprints out-of-band (configuration management, MDM, or a signed known_hosts baseline) so users/clients can detect a host key change indicating a potential MITM rather than blindly accepting
ssh-keyscanoutput. - For fleets, consider an SSH Certificate Authority (
ssh-keygen -s, or Vault SSH secrets engine, or Smallstepstep-ssh) issuing short-lived host and user certificates instead of managing long-lived keys andknown_hosts/authorized_keyssprawl.
6. User Key Hygiene
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
StrictModes yes(default) makessshdrefuse logins if~/.sshorauthorized_keyshave loose permissions โ don't disable it.- Pin
authorized_keysentries withcommand=,from=, andno-port-forwardingrestrictions for automation/service keys so a leaked key has bounded blast radius:
command="/usr/local/bin/backup-only.sh",from="10.10.5.20",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAA... svc_backup
- Centralize
authorized_keysmanagement (AuthorizedKeysCommandagainst an internal API, LDAP, or config management) rather than editing files ad hoc across a fleet โ this is what makes timely revocation on offboarding actually feasible.
7. Logging, Monitoring, and Brute-Force Protection
# /etc/ssh/sshd_config
LogLevel VERBOSE
SyslogFacility AUTH
VERBOSE additionally logs the key fingerprint used for each authentication, which is essential for tracing which specific key was used in an incident (CIS 5.2.5).
Forward to Central Logging / SIEM
Ship auth.log / journalctl -u sshd to a SIEM with alerting on:
| Event | Why it matters |
|---|---|
Repeated Failed password / Failed publickey from one source |
Brute-force or credential stuffing |
Accepted publickey for a privileged account outside change windows |
Possible compromised key use |
Invalid user attempts |
Reconnaissance / username enumeration |
| New key fingerprint accepted for an existing user | Unauthorized key addition |
Received disconnect immediately after connect at scale |
Scanning activity |
This supports NIST SP 800-53 AU-2 (Audit Events) and AU-6 (Audit Review, Analysis, and Reporting).
Automated Brute-Force Mitigation
Deploy fail2ban (or an equivalent, e.g., sshguard, cloud security-group auto-remediation) to temporarily ban sources with repeated auth failures:
# /etc/fail2ban/jail.local
[sshd]
enabled = true
port = ssh
filter = sshd
maxretry = 4
findtime = 600
bantime = 3600
This is a compensating control, not a substitute for key-based auth and network restriction โ treat it as defense-in-depth.
8. Network-Level Considerations
- Non-standard port: Moving SSH off port 22 reduces automated scanner noise but is not a security control โ treat it purely as noise reduction, never as a substitute for the controls above. Document it clearly if used, since it affects firewall rules and monitoring queries.
- Rate limiting at the firewall/load balancer, independent of
fail2ban, catches distributed attempts that stay under a single-host threshold. - Disable SSH entirely on hosts that don't need interactive access (many appliances and hardened images ship
sshdrunning by default โ turn it off if unused:systemctl disable --now sshd). - Segment management-plane SSH access onto a dedicated VLAN/subnet reachable only from the bastion and jump infrastructure, not from general user or production application networks.
Hardening Checklist
| Control | Priority | Verify With |
|---|---|---|
| Password authentication disabled | Critical | sshd -T \| grep -i passwordauthentication |
| Root login disabled | Critical | sshd -T \| grep -i permitrootlogin |
| Public key / certificate auth enforced | Critical | sshd -T \| grep -i pubkeyauthentication |
| MFA layered on privileged access | High | PAM config / AuthenticationMethods |
| Legacy ciphers/MACs/KEX disabled | High | ssh -Q cipher/mac/kex + nmap ssh2-enum-algos |
| Protocol 2 only | Critical | sshd -T \| grep -i protocol |
| MaxAuthTries โค 3โ4 | Medium | sshd -T \| grep -i maxauthtries |
| Idle session timeout configured | Medium | ClientAliveInterval/ClientAliveCountMax |
| AllowUsers/AllowGroups restricts access | High | sshd -T \| grep -i allow |
| X11/Agent/TCP forwarding disabled unless required | Medium | sshd -T \| grep -i forwarding |
Host key files 600, owned by root |
High | stat /etc/ssh/ssh_host_*_key |
sshd_config 600, owned by root |
High | stat /etc/ssh/sshd_config |
~/.ssh 700, authorized_keys 600 |
High | stat ~/.ssh ~/.ssh/authorized_keys |
| LogLevel VERBOSE, forwarded to SIEM | High | sshd -T \| grep -i loglevel |
| Brute-force protection (fail2ban/sshguard) | Medium | fail2ban-client status sshd |
| Direct internet exposure eliminated (bastion in place) | Critical | Network/firewall audit |
| Warning banner configured | Low | sshd -T \| grep -i banner |
Unused sshd disabled on non-admin hosts |
Medium | systemctl status sshd |
Validate the running effective configuration (not just the file, which may have duplicate or overridden directives) with:
sudo sshd -T
NIST SP 800-53 Rev. 5 Control Mapping
| Control | Title | How SSH hardening addresses it |
|---|---|---|
| AC-2 | Account Management | Named accounts only, no shared/root login |
| AC-3 | Access Enforcement | AllowUsers/AllowGroups, key-based ACLs |
| AC-6 | Least Privilege | Restricted authorized_keys (command=, no-port-forwarding) |
| AC-8 | System Use Notification | Pre-auth banner |
| AC-12 | Session Termination | ClientAliveInterval/CountMax |
| AC-17 | Remote Access | Bastion/jump host, network restriction |
| IA-2 | Identification and Authentication | Public key auth, MFA for privileged access |
| IA-5 | Authenticator Management | Key rotation, passphrase-protected keys |
| SC-8 | Transmission Confidentiality/Integrity | Strong ciphers/MACs, ETM modes |
| SC-12 | Cryptographic Key Establishment | Modern KEX (Curve25519, DH group 16/18) |
| AU-2 / AU-3 | Audit Events / Content of Audit Records | VERBOSE logging with key fingerprints |
| AU-6 | Audit Review, Analysis, Reporting | SIEM forwarding and alerting |
| SI-4 | System Monitoring | fail2ban/sshguard, SIEM detection rules |
References
- CIS Benchmarks โ Distribution Independent Linux, Section 5.2 (Configure SSH Server)
- NIST SP 800-53 Rev. 5 โ Security and Privacy Controls
- NIST SP 800-123 โ Guide to General Server Security
- NIST SP 800-63B โ Digital Identity Guidelines: Authentication and Lifecycle Management
- NIST SP 800-131A โ Transitioning the Use of Cryptographic Algorithms and Key Lengths
- OpenSSH sshd_config manual
- Mozilla OpenSSH Security Guide