Overview
SSH (Secure Shell) is the core channel for Linux operations and a primary target for attackers. A misconfigured SSH service can lead to a full server compromise. This article covers SSH security best practices comprehensively — from authentication mechanisms, server-side hardening, bastion host architecture, and tunneling techniques to audit logging and brute-force protection.
Key-Based Authentication
Generating Key Pairs
# Generate Ed25519 key (recommended)
$ ssh-keygen -t ed25519 -C "admin@sre.wang" -f ~/.ssh/id_ed25519
# Generate RSA key (for compatibility, at least 4096 bits)
$ ssh-keygen -t rsa -b 4096 -C "admin@sre.wang" -f ~/.ssh/id_rsa
# Generate a passphrase-protected key
$ ssh-keygen -t ed25519 -N "strong-passphrase" -f ~/.ssh/id_ed25519
# View key fingerprint
$ ssh-keygen -lf ~/.ssh/id_ed25519.pub
256 SHA256:abc123... admin@sre.wang (ED25519)
Key Algorithm Comparison
| Algorithm | Key Length | Security | Performance | Recommendation |
|---|---|---|---|---|
| Ed25519 | 256 bit | Very high | Very fast | First choice |
| RSA-4096 | 4096 bit | High | Medium | Compatibility scenarios |
| RSA-2048 | 2048 bit | Medium | Fast | Not recommended |
| ECDSA | 256/384/521 | High | Fast | Controversial (NIST curves) |
| DSA | 1024 bit | Low | - | Deprecated |
Deploying Public Keys
# Method 1: ssh-copy-id (recommended)
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
# Method 2: Manual deployment
$ cat ~/.ssh/id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
# Method 3: Batch deployment
$ for host in web-{01..10}; do
ssh-copy-id -i ~/.ssh/id_ed25519.pub admin@$host
done
authorized_keys Permission Control
# Correct permissions
~/.ssh/ → 700
~/.ssh/authorized_keys → 600
Private key file → 600
# Restrict public key permissions in authorized_keys
# Restrict source IP
from="10.0.0.0/8" ssh-ed25519 AAAAC3... admin@sre.wang
# Restrict command
command="/usr/bin/rsync --server" ssh-ed25519 AAAAC3... backup@sre.wang
# Restrict port forwarding
no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3... user@sre.wang
# Combined restrictions
from="10.0.0.0/8",command="/usr/bin/backup.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding ssh-ed25519 AAAAC3... backup@sre.wang
Certificate Authentication
Problems with Traditional Key Authentication
- Each server requires the user’s public key to be deployed individually
- Key revocation is difficult (must delete from each server)
- No way to set key expiration
- Key management lacks auditing
SSH Certificate Authentication Architecture
[CA Private Key]
/ \
[Sign User Cert] [Sign Host Cert]
| |
[User Holds Cert] [Server Configures Trusted CA]
| |
[SSH Connection] → [Server Verifies Cert Signature] → [Login Success]
Setting Up SSH CA
# 1. Generate CA key pair on the CA server
$ ssh-keygen -t ed25519 -f ~/.ssh/ca_user_key -C "SSH User CA"
# Do not set a passphrase (needed for automated signing)
# 2. Sign user certificate
$ ssh-keygen -s ~/.ssh/ca_user_key \
-I "admin_user" \
-n admin,root \
-V +1d \
~/.ssh/id_ed25519.pub
# -I: Certificate identifier (for auditing)
# -n: Allowed usernames
# -V: Validity period (+1d = 1 day)
# Certificate file generated: ~/.ssh/id_ed25519-cert.pub
# 3. Configure trusted CA on target servers
$ cat ~/.ssh/ca_user_key.pub >> /etc/ssh/user_ca.pub
# Add to /etc/ssh/sshd_config:
# TrustedUserCAKeys /etc/ssh/user_ca.pub
# 4. Restart sshd
$ systemctl restart sshd
Host Certificates (No More known_hosts Confirmation)
# 1. Generate host key pair on the server (if not already present)
$ ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
# 2. Sign host certificate with CA
$ ssh-keygen -s ~/.ssh/ca_user_key \
-I "web01.sre.wang" \
-h \
-n web01.sre.wang,web01 \
-V +52w \
/etc/ssh/ssh_host_ed25519_key.pub
# 3. Configure sshd to use host certificate
# /etc/ssh/sshd_config
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
# 4. Configure client to trust CA
# ~/.ssh/known_hosts or /etc/ssh/ssh_known_hosts
@cert-authority *.sre.wang ssh-ed25519 AAAAC3...
Certificate Revocation
# Create revocation list
$ cat > /etc/ssh/revoked_keys << 'EOF'
ssh-ed25519 AAAAC3... revoked-key-1
ssh-ed25519 AAAAC3... revoked-key-2
EOF
# Configure in sshd_config
RevokedKeys /etc/ssh/revoked_keys
# Or use KRL (Key Revocation List)
$ ssh-keygen -k -f revoked_krl -s ~/.ssh/ca_user_key ~/.ssh/id_ed25519.pub
# Configure in sshd_config
RevokedKeys /etc/ssh/revoked_krl
sshd_config Hardening
Security Baseline Configuration
# /etc/ssh/sshd_config
# === Protocol and Encryption ===
Protocol 2
HostKey /etc/ssh/ssh_host_ed25519_key
# Disable weak keys
# HostKey /etc/ssh/ssh_host_rsa_key
# HostKey /etc/ssh/ssh_host_ecdsa_key
# Encryption algorithms (allow only strong algorithms)
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
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman16-sha512
# === Authentication ===
PermitRootLogin no # Disable root direct login
PasswordAuthentication no # Disable password authentication
KbdInteractiveAuthentication no # Disable keyboard-interactive auth
ChallengeResponseAuthentication no
PubkeyAuthentication yes # Enable key-based authentication
PermitEmptyPasswords no # Disallow empty passwords
# CA certificate authentication
TrustedUserCAKeys /etc/ssh/user_ca.pub
# === Access Control ===
AllowUsers admin deploy # Whitelist users
AllowGroups ssh-users # Whitelist groups
DenyUsers nobody guest # Blacklist users
# Source restriction (via Match)
Match Address 10.0.0.0/8
AllowUsers admin root
PermitRootLogin yes
# === Session Control ===
MaxAuthTries 3 # Max authentication attempts
MaxSessions 10 # Max sessions
MaxStartups 10:30:60 # Concurrent unauthenticated connection limit
LoginGraceTime 30 # Authentication timeout (seconds)
# === Timeout Settings ===
ClientAliveInterval 300 # Keepalive interval (seconds)
ClientAliveCountMax 2 # Max keepalive checks
# 300 × 2 = 600 seconds before disconnecting idle connections
# === Logging ===
LogLevel VERBOSE # Verbose logging
SyslogFacility AUTHPRIV
PrintMotd no # Do not print MOTD
PrintLastLog yes # Show last login time
# === Forwarding ===
AllowTcpForwarding yes # Configure as needed
X11Forwarding no # Disable X11 forwarding
AllowAgentForwarding no # Disable Agent forwarding
PermitTunnel no # Disable tunneling
# === Other ===
UseDNS no # Disable DNS reverse lookup (speeds up login)
GSSAPIAuthentication no # Disable GSSAPI (speeds up login)
Banner /etc/ssh/banner # Pre-login banner
Configuration Validation and Reload
# Validate configuration syntax (important!)
$ sshd -t
# Reload configuration (without dropping existing connections)
$ systemctl reload sshd
# Or
$ systemctl reload ssh
# Verify active configuration
$ sshd -T | grep -E "permitroot|passwordauth|maxauth"
Safe operation procedure: After modifying sshd_config, always keep your current SSH session open and test the new configuration from a separate terminal. If the configuration is incorrect, you can restore it using the original session.
Security Impact of Each Parameter
| Parameter | Default | Secure Value | Risk Description |
|---|---|---|---|
PermitRootLogin | yes | no | Root direct login means full compromise |
PasswordAuthentication | yes | no | Passwords can be brute-forced |
MaxAuthTries | 6 | 3 | Reduces brute-force window |
ClientAliveInterval | 0 | 300 | Idle connections can be hijacked |
X11Forwarding | yes | no | X11 protocol has security vulnerabilities |
AllowTcpForwarding | yes | As needed | Can be used to build tunnels bypassing firewalls |
UseDNS | yes | no | DNS reverse lookup delay + DNS spoofing risk |
SSH Bastion Host
Bastion Host Architecture
[Operator] → [Bastion/Jump Host] → [Internal Servers]
SSH SSH
(public reachable) (internal only)
ProxyJump Method (Recommended)
# ~/.ssh/config
Host bastion
HostName bastion.sre.wang
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519
Host web-*
User deploy
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion # Connect through bastion host
# Or use the legacy syntax
# ProxyCommand ssh -W %h:%p bastion
Host web-01
HostName 10.0.1.10
Host web-02
HostName 10.0.1.11
Host web-03
HostName 10.0.1.12
# Usage: directly connect to internal servers
$ ssh web-01
# Actual path: Operator → bastion → web-01
Multi-Level Bastion
# ~/.ssh/config
Host bastion-1
HostName bastion1.sre.wang
User admin
Host bastion-2
HostName 10.0.0.2
User admin
ProxyJump bastion-1 # Through first-level bastion
Host internal-db
HostName 192.168.1.100
User dba
ProxyJump bastion-2 # Through second-level bastion
# Usage
$ ssh internal-db
# Path: Operator → bastion-1 → bastion-2 → internal-db
Bastion Host Security Configuration
# Special sshd_config for bastion host
# /etc/ssh/sshd_config
# Restrict bastion to forwarding only, no command execution
Match Group jump-users
AllowTcpForwarding yes
PermitTTY no
ForceCommand /bin/false # Disallow command execution
X11Forwarding no
AllowAgentForwarding no
# Or use a restricted shell
Match Group jump-users
ForceCommand /usr/local/bin/ssh-jump-menu
Recording Bastion Sessions
# Use tlog to record sessions
$ dnf install tlog
# /etc/sshd_config
ForceCommand /usr/bin/tlog-rec-session
# Or use the script command
# /etc/profile.d/audit.sh
if [ -n "$SSH_CONNECTION" ]; then
LOGDIR=/var/log/ssh-session
mkdir -p $LOGDIR
script -qf $LOGDIR/${USER}_$(date +%Y%m%d_%H%M%S).log
fi
Port Forwarding and Reverse Tunnels
Local Port Forwarding
# Forward local port 8080 to remote server's port 80
$ ssh -L 8080:localhost:80 user@server
# Accessing localhost:8080 = accessing server:80
# Forward to a third machine accessible from the remote server
$ ssh -L 8080:10.0.0.10:80 user@jump-server
# Access 10.0.0.10:80 through jump-server
# Run in background
$ ssh -fNL 8080:localhost:80 user@server
# Configure in ~/.ssh/config
Host tunnel
HostName server.sre.wang
User admin
LocalForward 8080 localhost:80
Remote Port Forwarding (Reverse Tunnel)
# Forward remote server's port 9090 to local port 80
$ ssh -R 9090:localhost:80 user@remote-server
# On remote-server, accessing localhost:9090 = accessing local port 80
# Typical use case: NAT traversal
# Internal machine → public server (reverse tunnel)
$ ssh -fNR 2222:localhost:22 user@public-server
# On the public server: ssh -p 2222 user@localhost connects to the internal machine
# Allow remote port forwarding to bind to all interfaces
# /etc/ssh/sshd_config (on the public server)
GatewayPorts yes
Dynamic Port Forwarding (SOCKS Proxy)
# Create a SOCKS5 proxy
$ ssh -D 1080 user@server
# Local port 1080 acts as a SOCKS5 proxy, traffic forwarded through server
# Use the proxy
$ curl --socks5 127.0.0.1:1080 http://internal-service:8080
# Configure browser SOCKS5 proxy: 127.0.0.1:1080
# Run in background
$ ssh -fND 1080 user@server
Tunnel Keepalive
# ~/.ssh/config
Host tunnel
HostName server.sre.wang
User admin
RemoteForward 2222 localhost:22
ServerAliveInterval 60 # Send heartbeat every 60 seconds
ServerAliveCountMax 3 # Disconnect after 3 missed responses
ExitOnForwardFailure yes # Disconnect if forwarding fails
ControlMaster auto # Reuse connection
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 600 # Keep connection for 10 minutes
# Use autossh for auto-reconnect
$ autossh -M 0 -fN tunnel
# -M 0: Disable monitoring port, rely on ServerAliveInterval
Tunnel Use Case Quick Reference
| Scenario | Command | Description |
|---|---|---|
| Access remote database | ssh -L 3306:db:3306 jump | Access database through bastion |
| NAT traversal | ssh -R 2222:localhost:22 public | Access internal from public network |
| SOCKS proxy | ssh -D 1080 server | Global proxy |
| Access internal web | ssh -L 8080:internal-web:80 jump | Access internal web through bastion |
| File transfer | ssh -L 21:ftp:21 jump | Access FTP through tunnel |
SSH Agent
How It Works
[SSH Agent] ← [SSH Client] ← [Remote Server]
↑
[Private key stored in Agent]
1. When SSH Client needs authentication, it requests Agent to sign
2. Agent signs with the private key but never exports it
3. Client sends the signature to the Server
4. Private key never leaves the Agent
Basic Usage
# Start Agent
$ eval $(ssh-agent)
Agent pid 12345
# Add key
$ ssh-add ~/.ssh/id_ed25519
# If the key has a passphrase, you will be prompted
# List added keys
$ ssh-add -l
256 SHA256:abc123... admin@sre.wang (ED25519)
# Remove key
$ ssh-add -d ~/.ssh/id_ed25519
# Remove all keys
$ ssh-add -D
Agent Forwarding
# Enable Agent forwarding (single session)
$ ssh -A user@jump-server
# On jump-server, you can use local keys to connect to a third server
# Configure in ~/.ssh/config
Host jump-server
ForwardAgent yes
# Security risk: root on the bastion host can use your Agent
# In production, prefer ProxyJump over Agent forwarding
Agent Security Best Practices
# 1. Set Agent timeout
$ ssh-add -t 3600 ~/.ssh/id_ed25519 # Auto-remove after 1 hour
# 2. Confirm signature requests
$ ssh-add -c ~/.ssh/id_ed25519
# Requires confirmation each time the key is used
# 3. Limit Agent forwarding
# Do not enable ForwardAgent on untrusted servers
# 4. Use SSH Agent instead of storing private keys on servers
# Bad practice: copying private keys to every server
# Good practice: use local private keys via Agent forwarding
Audit Logging
SSH Log Locations
# Debian/Ubuntu
$ journalctl -u ssh
# Or
$ tail -f /var/log/auth.log
# RHEL/CentOS
$ journalctl -u sshd
# Or
$ tail -f /var/log/secure
Key Log Events
# Successful login
$ journalctl -u sshd | grep "Accepted"
# Accepted publickey for admin from 10.0.0.1 port 54321 ssh2: ED25519 SHA256:abc123
# Failed login
$ journalctl -u sshd | grep "Failed"
# Failed password for invalid user admin from 1.2.3.4 port 54321 ssh2
# Invalid user attempts
$ journalctl -u sshd | grep "Invalid"
# Invalid user admin from 1.2.3.4 port 54321
# Connection closed
$ journalctl -u sshd | grep "Disconnected"
# Disconnected from user admin 10.0.0.1 port 54321
# Certificate authentication
$ journalctl -u sshd | grep "Certificate"
# Certificate ID "admin_user" (serial 0) for user admin from CA accepted
Log Analysis
# Count source IPs of failed logins
$ journalctl -u sshd --since "24 hours ago" | grep "Failed" | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
# Count successfully logged-in users
$ journalctl -u sshd --since "24 hours ago" | grep "Accepted" | awk '{print $4, $6}' | sort | uniq -c | sort -rn
# Detect brute-force (multiple failures from same IP)
$ journalctl -u sshd --since "1 hour ago" | grep "Failed" | awk '{print $NF}' | sort | uniq -c | sort -rn | awk '$1 > 10 {print "ALERT:", $0}'
# View currently online users
$ who
$ w
$ last -n 20
Configuring Verbose Logging
# /etc/ssh/sshd_config
LogLevel VERBOSE
# VERBOSE level records:
# - Key fingerprints
# - Certificate IDs
# - Authentication methods
# - Port forwarding
# Audit logging
# /etc/audit/audit.rules
-w /etc/ssh/sshd_config -p wa -k ssh_config_change
-w /etc/ssh/ -p wa -k ssh_key_change
Fail2ban
How It Works
[SSH Logs] → [Fail2ban Log Parser] → [Match Failure Rules] → [Call iptables/nftables] → [Ban IP]
↑
[jail.local config]
Installation and Configuration
# Install
$ apt install fail2ban # Debian/Ubuntu
$ dnf install fail2ban # RHEL/CentOS
# Configuration (do not edit jail.conf directly; create jail.local)
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
port = ssh
filter = sshd
backend = systemd
maxretry = 3 # Max failure count
findtime = 600 # Detection window (seconds)
bantime = 3600 # Ban duration (seconds)
bantime.increment = true # Incremental banning
bantime.maxtime = 604800 # Max ban: 7 days
ignoreip = 127.0.0.1/8 10.0.0.0/8 # Whitelist
action = iptables-allports # Ban all ports
Fail2ban Management
# Start
$ systemctl enable --now fail2ban
# View SSH jail status
$ fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 156
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 5
|- Total banned: 23
`- Banned IP list: 1.2.3.4 5.6.7.8 ...
# Manually ban an IP
$ fail2ban-client set sshd banip 1.2.3.4
# Manually unban an IP
$ fail2ban-client set sshd unbanip 1.2.3.4
# View all jails
$ fail2ban-client status
Custom Rules
# /etc/fail2ban/filter.d/sshd-aggressive.conf
[INCLUDES]
before = common.conf
[Definition]
failregex = ^%(__prefix_line)sFailed publickey for invalid user <FMT_USER>.*</FMT_USER> from <HOST> port \d+ ssh2$
^%(__prefix_line)sInvalid user <FMT_USER>.*</FMT_USER> from <HOST> port \d+$
^%(__prefix_line)sConnection closed by <HOST> port \d+ \[preauth\]$
ignoreregex =
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
filter = sshd-aggressive
maxretry = 2
findtime = 300
bantime = 86400
Real-World Examples
Example 1: Production SSH Complete Configuration
# === Server Configuration ===
# /etc/ssh/sshd_config
# Protocol and encryption
Protocol 2
HostKey /etc/ssh/ssh_host_ed25519_key
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com
MACs hmac-sha2-512-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org
# Authentication
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
# Certificate authentication
TrustedUserCAKeys /etc/ssh/user_ca.pub
# Access control
AllowGroups ssh-users admin-group
MaxStartups 10:30:100
# Timeout
ClientAliveInterval 300
ClientAliveCountMax 0 # Disconnect immediately on no response
# Logging
LogLevel VERBOSE
# Forwarding
AllowTcpForwarding yes
X11Forwarding no
AllowAgentForwarding no
# === Client Configuration ===
# ~/.ssh/config
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
HashKnownHosts yes
StrictHostKeyChecking ask
Host bastion
HostName bastion.sre.wang
User admin
Port 2222
IdentityFile ~/.ssh/id_ed25519
Host prod-*
User deploy
IdentityFile ~/.ssh/id_ed25519
ProxyJump bastion
StrictHostKeyChecking yes
Example 2: SSH Certificate Automated Issuance
#!/bin/bash
# /usr/local/bin/ssh-cert-issue.sh
# Issue short-lived SSH certificates for users
CA_KEY=~/.ssh/ca_user_key
USER_PUBKEY=$1
USERNAME=$2
VALIDITY=${3:-1d}
if [ $# -lt 2 ]; then
echo "Usage: $0 <user_pubkey> <username> [validity]"
exit 1
fi
# Issue certificate
ssh-keygen -s $CA_KEY \
-I "$USERNAME@$(date +%Y%m%d)" \
-n $USERNAME \
-V +$VALIDITY \
-O clear \
-O permit-pty \
-O force-command="/usr/local/bin/ssh-audit-wrapper.sh" \
$USER_PUBKEY
echo "Certificate issued:"
ssh-keygen -L -f ${USER_PUBKEY%.pub}-cert.pub
Example 3: Accessing Internal Services via SSH Tunnel
# Scenario: Access internal PostgreSQL through bastion host
# Internal PostgreSQL: 192.168.1.100:5432
# Bastion: bastion.sre.wang
# Method 1: Local port forwarding
$ ssh -fNL 15432:192.168.1.100:5432 bastion
$ psql -h localhost -p 15432 -U admin -d mydb
# Method 2: Configure in ~/.ssh/config
Host db-tunnel
HostName bastion.sre.wang
User admin
LocalForward 15432 192.168.1.100:5432
$ ssh -fN db-tunnel
$ psql -h localhost -p 15432 -U admin -d mydb
# Method 3: Use autossh to maintain the tunnel
$ autossh -M 0 -fN db-tunnel
# Method 4: Use ProxyCommand to connect directly (no tunnel needed)
$ psql "host=192.168.1.100 port=5432 user=admin dbname=mydb \
sslmode=disable \
hostaddr=127.0.0.1 \
port=15432"
SSH Security Checklist
#!/bin/bash
# SSH security audit script
echo "=== SSH Security Check ==="
# 1. Check root login
ROOT_LOGIN=$(sshd -T | grep permitrootlogin)
echo "Root Login: $ROOT_LOGIN"
# 2. Check password authentication
PWD_AUTH=$(sshd -T | grep passwordauthentication)
echo "Password Auth: $PWD_AUTH"
# 3. Check key algorithms
echo "Host Keys:"
ls -la /etc/ssh/ssh_host_*
# 4. Check encryption configuration
echo "Ciphers:"
sshd -T | grep ciphers
echo "MACs:"
sshd -T | grep macs
# 5. Check MaxAuthTries
MAX_AUTH=$(sshd -T | grep maxauthtries)
echo "Max Auth Tries: $MAX_AUTH"
# 6. Check Fail2ban
if systemctl is-active --quiet fail2ban; then
echo "Fail2ban: Active"
fail2ban-client status sshd
else
echo "Fail2ban: INACTIVE (WARNING)"
fi
# 7. Check known_hosts
echo "Known Hosts entries: $(wc -l < ~/.ssh/known_hosts 2>/dev/null || echo 0)"
# 8. Check key permissions
echo "SSH Directory Permissions:"
ls -ld ~/.ssh
ls -la ~/.ssh/authorized_keys 2>/dev/null
# 9. Check recent logins
echo "Recent Logins:"
last -n 5
# 10. Check failed logins
echo "Failed Logins (last 24h):"
journalctl -u sshd --since "24 hours ago" 2>/dev/null | grep "Failed" | wc -l
Summary
SSH is the core channel for system operations, and its security configuration directly affects the security of the entire infrastructure. Key takeaways:
- Key-based authentication is the foundation: Ed25519 is the preferred algorithm, password authentication must be disabled, and authorized_keys can restrict source IPs and commands.
- Certificate authentication is the advanced solution: Centralized key management, supports expiration and revocation — suitable for large teams and compliance requirements.
- sshd_config hardening is mandatory: Disable root login, restrict encryption algorithms, set timeouts and max authentication attempts.
- Bastion host is the standard architecture: ProxyJump is the modern approach, more secure than Agent forwarding.
- Port forwarding should be on-demand: Configure AllowTcpForwarding based on actual needs; X11 and Agent forwarding should be disabled.
- Audit logging is the security cornerstone: LogLevel VERBOSE records key fingerprints and certificate IDs; regularly analyze failed logins.
- Fail2ban is the standard for brute-force protection: Combined with incremental banning strategies, effectively deters automated attacks.
- SSH security is part of defense-in-depth: Must be combined with firewalls, intrusion detection, network segmentation, and other comprehensive protections.
The golden rule of SSH security: least privilege + layered defense. Every configuration item should follow the principle of least privilege, combined with bastion hosts, Fail2ban, and audit logging to form a multi-layered defense system.