Hardening Linux Servers with SSH TLS and Firewall Rules
Alex Rivera, Senior Systems Architect
SSH Daemon Hardening
To harden the SSH daemon, edit /etc/ssh/sshd_config with the following directives. Each setting reduces the attack surface by disabling legacy protocols, weak ciphers, and unnecessary authentication methods.
ssh# /etc/ssh/sshd_config Port 22 Protocol 2 PermitRootLogin no PubkeyAuthentication yes PasswordAuthentication no ChallengeResponseAuthentication no UsePAM yes X11Forwarding no AllowTcpForwarding no PermitEmptyPasswords no MaxAuthTries 3 LoginGraceTime 30 ClientAliveInterval 300 ClientAliveCountMax 2 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,hmac-sha2-512,hmac-sha2-256 KexAlgorithms curve25519-sha256@libssh.org,curve25519-sha256,diffie-hellman-group-exchange-sha256 HostKeyAlgorithms ssh-ed25519-cert-v01@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512
After editing, validate syntax and reload:
bashsshd -t && systemctl reload sshd
Why these values?
Protocol 2enforces SSH‑2 only (RFC 4253).CiphersandMACsrestrict to AEAD algorithms providing confidentiality and integrity.KexAlgorithmsselects elliptic‑curve Diffie‑Hellman for forward secrecy.HostKeyAlgorithmsprefers Ed25519 and RSA‑SHA2‑512, eliminating SHA‑1 signatures.
Verification
bashssh -Q cipher | grep -E 'chacha20|aes.*gcm' ssh -Q mac | grep -E 'hmac-sha2' ssh -Q kex | grep -E 'curve25519|group-exchange-sha256'
TLS/SSL Configuration
Deploy TLS 1.2 and TLS 1.3 exclusively. The following Nginx snippet demonstrates a hardened server block. It references the Mozilla SSL Configuration Generator (intermediate profile) and aligns with NIST SP 800‑52 Rev. 2.
nginx# /etc/nginx/conf.d/ssl-hardening.conf server { listen 443 ssl http2; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256'; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; ssl_session_tickets off; # OCSP Stapling ssl_stapling on; ssl_stapling_verify on; resolver 1.1.1.1 8.8.8.8 valid=300s; resolver_timeout 5s; # Security Headers add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; style-src 'self' 'nonce-$request_id'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; add_header X-Content-Type-Options nosniff always; add_header X-Frame-Options DENY always; add_header Referrer-Policy strict-origin-when-cross-origin always; add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always; }
Reload Nginx:
bashnginx -t && systemctl reload nginx
Cipher Suite Comparison
| Protocol | Cipher Suite (IANA) | Forward Secrecy | AEAD | Recommended |
|---|---|---|---|---|
| TLS 1.3 | TLS_AES_256_GCM_SHA384 | Yes | Yes | ✅ |
| TLS 1.3 | TLS_CHACHA20_POLY1305_SHA256 | Yes | Yes | ✅ |
| TLS 1.2 | ECDHE-ECDSA-AES256-GCM-SHA384 | Yes | Yes | ✅ |
| TLS 1.2 | ECDHE-RSA-AES256-GCM-SHA384 | Yes | Yes | ✅ |
| TLS 1.2 | ECDHE-ECDSA-CHACHA20-POLY1305 | Yes | Yes | ✅ |
| TLS 1.2 | ECDHE-RSA-CHACHA20-POLY1305 | Yes | Yes | ✅ |
| TLS 1.2 | AES256-GCM-SHA384 (static RSA) | No | Yes | ❌ |
| TLS 1.1 | Any | N/A | N/A | ❌ |
Source: IANA TLS Cipher Suite Registry
Firewall Rules
iptables Baseline
The following ruleset implements a default‑deny posture, permits established/related traffic, allows SSH (port 22) and HTTPS (port 443), and logs dropped packets for audit.
bash# Flush existing rules iptables -F iptables -X iptables -Z # Default policies iptables -P INPUT DROP iptables -P FORWARD DROP iptables -P OUTPUT ACCEPT # Loopback iptables -A INPUT -i lo -j ACCEPT iptables -A OUTPUT -o lo -j ACCEPT # Established/related iptables -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT # ICMP (rate‑limited) iptables -A INPUT -p icmp --icmp-type echo-request -m limit --limit 1/s --limit-burst 5 -j ACCEPT # SSH (restrict to management subnet 10.0.0.0/24) iptables -A INPUT -p tcp -s 10.0.0.0/24 --dport 22 -m conntrack --ctstate NEW -j ACCEPT # HTTPS iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW -j ACCEPT # Logging dropped packets iptables -A INPUT -j LOG --log-prefix "IPT-DROP: " --log-level 4 # Save iptables-save > /etc/iptables/rules.v4
Persist across reboots (Debian/Ubuntu):
bashapt-get install -y iptables-persistent netfilter-persistent save
UFW Equivalent
For administrators preferring UFW, the same policy translates to:
bashufw default deny incoming ufw default allow outgoing ufw allow from 10.0.0.0/24 to any port 22 proto tcp comment 'SSH mgmt' ufw allow 443/tcp comment 'HTTPS' ufw logging medium ufw enable
Security Headers (CSP, HSTS, etc.)
The Nginx block above already injects a robust Content‑Security‑Policy (CSP) using nonces generated per request ($request_id). For Apache, the equivalent configuration:
apache# /etc/apache2/conf-available/security-headers.conf Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" Header always set Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-%{UNIQUE_ID}e'; style-src 'self' 'nonce-%{UNIQUE_ID}e'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" Header always set X-Content-Type-Options nosniff Header always set X-Frame-Options DENY Header always set Referrer-Policy strict-origin-when-cross-origin Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Enable with a2enconf security-headers && systemctl reload apache2.
Compliance Mapping
| Control | Framework | Implementation Evidence |
|---|---|---|
| AC‑3 (Access Enforcement) | NIST SP 800‑53 Rev. 5 | SSH PermitRootLogin no, PasswordAuthentication no |
| SC‑8 (Transmission Confidentiality/Integrity) | NIST SP 800‑53 Rev. 5 | TLS 1.2/1.3 only, AEAD ciphers |
| CM‑7 (Least Functionality) | NIST SP 800‑53 Rev. 5 | Default‑deny firewall, minimal open ports |
| A.9.2.3 (Management of Privileged Access) | ISO/IEC 27001:2022 | SSH key‑only auth, MaxAuthTries 3 |
| A.13.1.1 (Network Controls) | ISO/IEC 27001:2022 | iptables/UFW baseline ruleset |
| A.14.2.5 (Secure System Engineering) | ISO/IEC 27001:2022 | CSP, HSTS, secure headers |
References: NIST SP 800‑53 Rev. 5, ISO/IEC 27001:2022, OWASP Secure Headers Project.
Troubleshooting Checklist
-
SSH Fails After Config Change
a. Run
sshd -tto validate syntax.
b. Check journal:journalctl -u sshd -n 50 --no-pager.
c. Verify client offers supported ciphers:ssh -vvv user@host 2>&1 | grep -i kex.
d. Temporarily enablePasswordAuthentication yesto regain access, then re‑apply key‑only. -
TLS Handshake Errors
a. Test withopenssl s_client -connect example.com:443 -tls1_3.
b. Confirm certificate chain:openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /etc/letsencrypt/live/example.com/fullchain.pem.
c. Ensuressl_staplingresolver reaches OCSP responder (check logs forOCSP response not received). -
Firewall Blocks Legitimate Traffic
a. List rules with line numbers:iptables -L INPUT -n --line-numbers.
b. Verify source IP matches management subnet.
c. Checkdmesgor/var/log/kern.logforIPT-DROPentries.
d. Insert a temporary allow rule for diagnostics:iptables -I INPUT 1 -s <test-ip> -j ACCEPT. -
CSP Violations in Browser Console
a. InspectContent-Security-Policyheader via devtools.
b. Ensure nonce generation matches script tags (<script nonce="{{request_id}}">).
c. Adjustscript-srcdirective to include required third‑party origins (e.g.,https://cdn.example.com). -
Audit Log Review
a. Centralize logs withrsyslogorsystemd-journaldforwarding.
b. Correlate SSH auth failures (Failed password), firewall drops, and TLS alerts.
c. Schedule weekly review against NIST SP 800‑92 log management guide.
Frequently Asked Questions (FAQ)
Q1: Why disable PasswordAuthentication entirely instead of using strong passwords?
A1: Password‑based auth is vulnerable to credential stuffing, brute‑force, and keylogging. Public‑key authentication provides cryptographic proof of possession without transmitting secrets. NIST SP 800‑63B recommends phasing out memorized secrets for privileged access.
Q2: Can I keep TLS 1.1 for legacy clients?
A2: No. TLS 1.1 lacks AEAD ciphers and is deprecated by RFC 8996. Maintaining it expands the attack surface (POODLE, BEAST). If legacy support is mandatory, isolate those clients on a segmented network with a dedicated termination proxy.
Q3: How do I rotate SSH host keys without downtime?
A3: Generate new keys (ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key_new), add them to sshd_config via HostKey /etc/ssh/ssh_host_ed25519_key_new, reload sshd, then after client verification remove the old key file and update config. This rolling approach avoids service interruption.
Q4: What is the impact of ssl_session_tickets off on performance?
A4: Disabling session tickets forces full handshakes for each new connection, increasing CPU usage (~5‑10 % on high‑traffic sites). However, tickets can weaken forward secrecy if the ticket key is compromised. For environments requiring PFS compliance (e.g., PCI‑DSS), keep tickets off and enable ssl_session_cache shared:SSL:10m for session resumption without<content>