Every security-conscious organization hardens SSH, configures firewalls, and sets up intrusion detection. But there's a critical layer that often gets treated as an afterthought: the cryptographic foundations underpinning every encrypted connection on your Linux systems. Your web servers, databases, API gateways, internal microservices, package managers, email relays, and LDAP clients all depend on TLS libraries and certificate infrastructure. You can harden SSH all day long — if your web server still negotiates TLS 1.0 with RC4, or your internal services trust an expired CA, you've got a gaping hole in your defense.
The threat landscape has shifted, and not in our favor. Nation-state actors are running "harvest now, decrypt later" campaigns — intercepting and storing encrypted traffic today with the expectation that quantum computers will crack it tomorrow. This isn't a theoretical concern. The NSA, CISA, and NIST have all published advisories urging organizations to begin their post-quantum migration now. Meanwhile, real vulnerabilities keep popping up in classical TLS implementations: the Marvin Attack (CVE-2023-6129) against RSA PKCS#1 v1.5 decryption in OpenSSL, the Terrapin attack (CVE-2023-48795) against SSH, and ongoing discoveries in certificate validation logic across every major TLS library.
This guide covers the full stack of Linux cryptographic hardening: system-wide crypto policies, OpenSSL configuration, web server TLS, automated certificate management, post-quantum TLS readiness, and continuous auditing. Every configuration example has been tested against current stable releases as of early 2026. If you're looking for SSH-specific hardening (including post-quantum SSH key exchange), check out our dedicated SSH hardening guide — this article focuses on everything else.
Understanding Linux Cryptographic Subsystems
Before you start hardening, you need to understand a fundamental truth about Linux cryptography: there's no single library that controls everything. Linux systems typically run three or more independent TLS/crypto libraries, each used by different applications. Hardening one while ignoring the others leaves you with a false sense of security — and honestly, I've seen this catch experienced sysadmins off guard more often than you'd expect.
The Big Three: OpenSSL, GnuTLS, and NSS
- OpenSSL (currently 3.5.x) is the most widely used. Nginx, Apache (via mod_ssl), Python, Ruby, most database clients, curl, and the majority of server software link against OpenSSL. Its configuration lives in
/etc/ssl/openssl.cnf(or/etc/pki/tls/openssl.cnfon RHEL-based systems). - GnuTLS (currently 3.8.x) is the default for GNOME-based applications, systemd (for DNS-over-TLS in resolved), and some packages on Debian/Ubuntu that avoid OpenSSL for licensing reasons. Its system-wide priority strings are configured differently from OpenSSL cipher suites.
- NSS (Network Security Services) is Mozilla's library. Firefox, Thunderbird, and some enterprise tools use NSS. It has its own certificate database format (cert9.db) — completely separate from the system CA store.
Additionally, Go applications use their own built-in crypto/tls library, Java applications use the JDK's JSSE, and Rust applications typically rely on rustls or ring. Each has its own configuration surface. The practical takeaway? Changing a cipher suite in OpenSSL does absolutely nothing to a Java application running on the same host. You need a system-wide approach.
System-Wide Crypto Policies
The cleanest solution to the multi-library problem is a system-wide cryptographic policy that configures all supported libraries from a single control point. RHEL and Fedora have had this since RHEL 8, and it's genuinely one of the best security features in the Red Hat ecosystem.
RHEL/Fedora: crypto-policies
The crypto-policies framework provides four built-in policy levels that configure OpenSSL, GnuTLS, NSS, OpenSSH, Libreswan, and BIND simultaneously:
- LEGACY — Allows TLS 1.0+, SHA-1 signatures, RSA 1024-bit keys. Only use this for compatibility with ancient systems you truly can't upgrade.
- DEFAULT — TLS 1.2+, 2048-bit RSA minimum, SHA-256+. A reasonable baseline, but not aggressive.
- FUTURE — TLS 1.2+ with 128-bit+ security, 3072-bit RSA minimum, no SHA-1 anywhere, no CBC mode ciphers. This is where you should be heading.
- FIPS — FIPS 140-3 compliant settings. Required for US government systems and many regulated industries.
# Check current policy
update-crypto-policies --show
# Output: DEFAULT
# Set system-wide policy to FUTURE
sudo update-crypto-policies --set FUTURE
# Verify the change
update-crypto-policies --show
# Output: FUTURE
# Check what actually changed (shows all backend config files)
update-crypto-policies --check
# The policy takes effect immediately for new connections.
# Restart services to apply to existing long-running processes.
sudo systemctl restart nginx httpd postfix dovecot
The policy files live in /usr/share/crypto-policies/ and the active configuration is symlinked from /etc/crypto-policies/back-ends/. When you set a policy, it generates configuration snippets for each supported library:
# See what the FUTURE policy sets for OpenSSL
cat /etc/crypto-policies/back-ends/opensslcnf.config
# See what it sets for GnuTLS
cat /etc/crypto-policies/back-ends/gnutls.config
# See what it sets for NSS
cat /etc/crypto-policies/back-ends/nss.config
Custom Policy Modules
The built-in levels are a starting point. For production hardening, you'll often want a custom policy module that modifies a base level. For example, to enforce TLS 1.3 only while keeping FUTURE as your base:
# Create a custom subpolicy module
sudo mkdir -p /etc/crypto-policies/policies/modules
cat <<'EOF' | sudo tee /etc/crypto-policies/policies/modules/TLS13ONLY.pmod
# Force TLS 1.3 minimum for all libraries
min_tls_version = TLS1.3
# Disable all CBC mode ciphers (mitigates padding oracle attacks)
cipher = -CBC
# Disable RSA key exchange (no forward secrecy)
key_exchange = -RSA
# Require minimum 256-bit symmetric encryption
cipher = -AES-128-GCM -AES-128-CCM
EOF
# Apply the custom module on top of FUTURE
sudo update-crypto-policies --set FUTURE:TLS13ONLY
# Verify
update-crypto-policies --show
# Output: FUTURE:TLS13ONLY
Debian/Ubuntu: Achieving Equivalent Control
Debian and Ubuntu don't have an equivalent to crypto-policies out of the box. You'll have to configure each library separately, which is more work but still doable. The key files are:
# OpenSSL system-wide defaults
/etc/ssl/openssl.cnf
# GnuTLS priority string (if used by applications)
# Set via environment variable or per-application config
# NSS system-wide policy
/etc/crypto-policies/back-ends/nss.config # if crypto-policies is installed
# CA certificate store
/etc/ssl/certs/ca-certificates.crt
/usr/local/share/ca-certificates/
On Debian 13 (Trixie) and Ubuntu 24.04+, you can install the crypto-policies package from the repositories for a more unified experience, though it isn't as deeply integrated as on RHEL:
# On Debian 13+ / Ubuntu 24.04+
sudo apt install crypto-policies
sudo update-crypto-policies --set FUTURE
For older Debian/Ubuntu releases, you'll need to harden each component individually, as described in the following sections.
Hardening OpenSSL Configuration
Since OpenSSL is the most widely used crypto library on Linux, hardening its system-wide configuration gives you the broadest impact. OpenSSL 3.5 (the current stable release) supports TLS 1.3 with post-quantum hybrid key exchange, making it a solid foundation for forward-looking security.
System-Wide openssl.cnf Hardening
The system-wide OpenSSL configuration file controls default behavior for all applications that don't override it. On RHEL-based systems it lives at /etc/pki/tls/openssl.cnf; on Debian-based systems at /etc/ssl/openssl.cnf. Add or modify these sections:
# /etc/ssl/openssl.cnf (append to or modify existing file)
[system_default_sect]
# Enforce TLS 1.2 as absolute minimum, prefer TLS 1.3
MinProtocol = TLSv1.2
# Disable anything below TLS 1.2
MaxProtocol = TLSv1.3
# Cipher suite list for TLS 1.2 (TLS 1.3 ciphers are separate)
CipherString = ECDHE+AESGCM:ECDHE+CHACHA20:DHE+AESGCM:DHE+CHACHA20:!aNULL:!MD5:!DSS:!RC4:!3DES:!SEED:!IDEA:!CBC
# TLS 1.3 cipher suites (these are the only three you should allow)
Ciphersuites = TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
# Minimum DH parameter size (prevents Logjam attack)
MinDHSize = 2048
# ECDH curves - prefer X25519 and P-256
Groups = x25519:x448:secp256r1:secp384r1
# Signature algorithms
SignatureAlgorithms = ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA384:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512
[openssl_init]
ssl_conf = ssl_configuration
[ssl_configuration]
system_default = system_default_sect
Verifying the Configuration
After making changes, you'll want to verify that OpenSSL actually respects your settings:
# List enabled ciphers under the new configuration
openssl ciphers -v 'ALL:!aNULL' | head -20
# Specifically check that weak ciphers are gone
openssl ciphers -v 'ALL' | grep -iE 'RC4|DES|MD5|NULL'
# Should return nothing
# Test a connection to a server using TLS 1.3
openssl s_client -connect example.com:443 -tls1_3 -brief
# Should show: Protocol version: TLSv1.3
# Verify TLS 1.0 is rejected
openssl s_client -connect example.com:443 -tls1
# Should fail with a protocol error
# Start a test server with your hardened config to verify
openssl s_server -accept 4433 -cert server.crt -key server.key \
-tls1_2 -tls1_3 -no_tls1 -no_tls1_1 -no_ssl3
# In another terminal, test the connection
openssl s_client -connect localhost:4433 -brief
Disabling Legacy Providers
OpenSSL 3.x uses a provider architecture. The "legacy" provider includes deprecated algorithms like MD4, RC2, DES, and Blowfish. Make sure it's not loaded by default:
# Check which providers are currently loaded
openssl list -providers
# In openssl.cnf, ensure the legacy provider is NOT activated:
[openssl_init]
providers = provider_sect
[provider_sect]
default = default_sect
# Do NOT include: legacy = legacy_sect
[default_sect]
activate = 1
TLS Hardening for Web Servers
Web servers are the most exposed TLS endpoints in most organizations. A misconfigured Nginx or Apache instance is visible to the entire internet and will get flagged by every scanner and grading service within hours. Getting this right is table stakes.
Nginx TLS Hardening (Modern Profile)
This configuration targets Nginx 1.27+ with OpenSSL 3.5+ and enforces a modern TLS profile suitable for 2026 deployments. It'll score A+ on SSL Labs and Qualys:
# /etc/nginx/conf.d/tls-hardening.conf
# TLS protocol versions - TLS 1.2 and 1.3 only
ssl_protocols TLSv1.2 TLSv1.3;
# TLS 1.2 cipher suites - AEAD ciphers with forward secrecy only
ssl_ciphers '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';
# Server chooses the cipher order (ensures strongest cipher is used)
ssl_prefer_server_ciphers on;
# TLS 1.3 ciphers (Nginx 1.19.4+)
# These are negotiated separately from TLS 1.2 ciphers
ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
# ECDH curves - prefer X25519 for performance and security
ssl_ecdh_curve X25519:secp256r1:secp384r1;
# Session configuration
ssl_session_timeout 1d;
ssl_session_cache shared:TLS:10m;
# Disable session tickets for perfect forward secrecy
# (or rotate ticket keys every hour if you need them for performance)
ssl_session_tickets off;
# OCSP Stapling - serves OCSP response with the TLS handshake
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# Certificate and key
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
# DH parameters (only needed for DHE ciphers, not ECDHE)
# Generate with: openssl dhparam -out /etc/nginx/dhparam.pem 4096
ssl_dhparam /etc/nginx/dhparam.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
Apache mod_ssl Hardening
Here's the equivalent configuration for Apache 2.4 with mod_ssl:
# /etc/httpd/conf.d/ssl-hardening.conf (RHEL)
# /etc/apache2/conf-available/ssl-hardening.conf (Debian)
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite 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
SSLCipherSuite TLSv1.3 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
SSLHonorCipherOrder on
# OCSP Stapling
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
SSLStaplingCache shmcb:/var/run/ocsp(128000)
# HSTS header
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Compression must be off (mitigates BREACH attack)
SSLCompression off
# Disable SSL session renegotiation (mitigates renegotiation attacks)
SSLInsecureRenegotiation off
HSTS Preloading
HSTS (HTTP Strict Transport Security) tells browsers to always use HTTPS. But that first visit? Still vulnerable to downgrade attacks — unless you submit your domain to the HSTS preload list, which gets hardcoded into browser binaries. The requirements are:
- Serve a valid HTTPS certificate on the root domain.
- Redirect all HTTP traffic to HTTPS on the same host.
- Include the
Strict-Transport-Securityheader withmax-ageof at least 31536000 (one year),includeSubDomains, and thepreloaddirective. - Submit your domain at
hstspreload.org.
A word of caution: once preloaded, removing HSTS from your domain can take months. Make absolutely sure every subdomain supports HTTPS before you submit.
Certificate Transparency Monitoring
Certificate Transparency (CT) logs are append-only, publicly auditable logs of every certificate issued by participating CAs. Monitoring these logs for your domains is critical — it lets you detect unauthorized certificate issuance (a sign of domain hijacking or CA compromise) within minutes rather than days.
# Monitor CT logs for your domain using certspotter
# https://github.com/SSLMate/certspotter
sudo apt install certspotter
# Watch for new certificates issued for your domains
certspotter -watchlist /etc/certspotter/watchlist
# /etc/certspotter/watchlist format (one domain per line):
# example.com
# .example.com (includes all subdomains)
# Alternatively, use the crt.sh API for ad-hoc queries
curl -s "https://crt.sh/?q=%.example.com&output=json" | \
python3 -m json.tool | grep -E '"common_name"|"not_after"'
Automated Certificate Management
Manual certificate management doesn't scale, and it's a leading cause of outages. Expired certificates caused high-profile incidents at Microsoft Teams (2020), Spotify (2020), Let's Encrypt themselves (2021, when the DST Root CA X3 expired), and Starlink (2023). Automation isn't optional here — it's a reliability requirement.
Let's Encrypt with Certbot
Certbot remains the standard ACME client for Let's Encrypt. For production deployments, use the systemd timer method rather than the legacy cron approach:
# Install Certbot (prefer snap for auto-updates)
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
# Obtain certificate with automatic Nginx configuration
sudo certbot --nginx -d example.com -d www.example.com \
--must-staple \
--hsts \
--uir \
--staple-ocsp \
--rsa-key-size 4096
# Or with standalone HTTP challenge (for non-web servers)
sudo certbot certonly --standalone \
-d mail.example.com \
--preferred-challenges http \
--key-type ecdsa \
--elliptic-curve secp384r1
# Verify automatic renewal is configured
sudo systemctl list-timers | grep certbot
Systemd Timer for Certificate Renewal
If Certbot's snap package isn't available or you want more control, you can set up a custom systemd timer:
# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot certificate renewal
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
# Run with reduced privileges
ProtectSystem=full
PrivateTmp=true
# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Run Certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 02,14:30:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renew.timer
# Verify timer is active
sudo systemctl list-timers certbot-renew.timer
# Test a dry run
sudo certbot renew --dry-run
Certificate Expiry Monitoring
Even with automation, you should monitor certificate expiry independently. Automation can fail silently — DNS changes, firewall rules, rate limits, or disk space issues can all prevent renewal. I've personally seen a renewal break because someone moved a DNS zone without telling anyone. Here's a simple but effective monitoring script:
#!/bin/bash
# /usr/local/bin/check-cert-expiry.sh
# Check TLS certificate expiry for a list of hosts
WARN_DAYS=30
CRIT_DAYS=14
HOSTS="example.com:443 api.example.com:443 mail.example.com:465"
for hostport in $HOSTS; do
host=$(echo "$hostport" | cut -d: -f1)
port=$(echo "$hostport" | cut -d: -f2)
expiry=$(echo | openssl s_client -connect "$hostport" -servername "$host" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry" ]; then
echo "CRITICAL: Cannot connect to $hostport"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$CRIT_DAYS" ]; then
echo "CRITICAL: $host expires in $days_left days ($expiry)"
elif [ "$days_left" -lt "$WARN_DAYS" ]; then
echo "WARNING: $host expires in $days_left days ($expiry)"
else
echo "OK: $host expires in $days_left days ($expiry)"
fi
done
Integrate this with your monitoring system (Prometheus, Nagios, Zabbix) or set it up as a systemd timer that sends alerts via email or Slack when certificates approach expiry.
Private CA with step-ca for Internal Services
For internal services, mTLS between microservices, and development environments, running your own certificate authority beats using self-signed certificates or paying for public certs for internal hostnames. step-ca (by Smallstep) is an open-source, production-ready ACME CA that integrates really well with Linux infrastructure:
# Install step CLI and step-ca
wget https://dl.smallstep.com/gh-release/cli/v0.28.0/step-cli_0.28.0_amd64.deb
wget https://dl.smallstep.com/gh-release/certificates/v0.28.0/step-ca_0.28.0_amd64.deb
sudo dpkg -i step-cli_0.28.0_amd64.deb step-ca_0.28.0_amd64.deb
# Initialize the CA
step ca init \
--name "Internal CA" \
--provisioner [email protected] \
--dns ca.internal.example.com \
--address :8443 \
--deployment-type standalone
# Start step-ca (production: run as a systemd service)
step-ca $(step path)/config/ca.json
# Issue a short-lived certificate (24 hours by default)
step ca certificate api.internal.example.com api.crt api.key \
--provisioner [email protected]
# Configure Certbot to use your internal ACME CA
sudo certbot certonly --standalone \
-d api.internal.example.com \
--server https://ca.internal.example.com:8443/acme/acme/directory
Short-lived certificates (24 hours to 7 days) are a massive security improvement over traditional 90-day or 1-year certificates. If a key gets compromised, the exposure window is measured in hours, not months. step-ca supports automatic renewal out of the box with the step-renewer systemd service, and you can configure it to issue certificates with lifetimes as short as 5 minutes for highly sensitive services.
Mutual TLS (mTLS) for Service-to-Service Authentication
mTLS requires both the client and server to present certificates, providing strong mutual authentication. This is essential for zero-trust architectures where network location alone doesn't grant trust:
# Generate client certificate from your internal CA
step ca certificate client.api.example.com client.crt client.key
# Nginx mTLS configuration
server {
listen 443 ssl;
server_name api.internal.example.com;
ssl_certificate /etc/ssl/certs/api.crt;
ssl_certificate_key /etc/ssl/private/api.key;
# Require client certificate
ssl_client_certificate /etc/ssl/certs/internal-ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
# Pass client certificate DN to upstream
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Verify $ssl_client_verify;
}
# Test with curl using client certificate
curl --cert client.crt --key client.key \
--cacert internal-ca.crt \
https://api.internal.example.com/health
Post-Quantum TLS Readiness
Post-quantum cryptography (PQC) for TLS is no longer a future concern — it's happening right now. NIST finalized its first post-quantum standards in August 2024: ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism, formerly known as Kyber) for key exchange, and ML-DSA (formerly Dilithium) for digital signatures. The major browser vendors, cloud providers, and TLS libraries are already deploying hybrid key exchange that combines classical and post-quantum algorithms.
The Hybrid Approach: Classical + Post-Quantum
The industry has settled on a hybrid approach for the transition period: TLS key exchange uses both a classical algorithm (X25519 or P-256) and a post-quantum algorithm (ML-KEM-768) simultaneously. If either algorithm is secure, the connection remains secure. This protects against quantum attacks while keeping a safety net in case the new post-quantum algorithms turn out to have unexpected weaknesses.
Chrome, Firefox, and Edge have all enabled hybrid key exchange (X25519+ML-KEM-768) by default since late 2024. On the server side, OpenSSL 3.5 includes full support for ML-KEM and hybrid TLS key exchange via the OQS (Open Quantum Safe) provider or its built-in implementations.
Enabling Post-Quantum TLS in OpenSSL 3.5+
# Verify your OpenSSL version supports ML-KEM
openssl version
# OpenSSL 3.5.0 14 Apr 2025 (Library: OpenSSL 3.5.0 14 Apr 2025)
# List available KEM algorithms
openssl list -kem-algorithms | grep -i mlkem
# mlkem512
# mlkem768
# mlkem1024
# List available hybrid groups for TLS
openssl list -tls1_3-groups | grep -i mlkem
# X25519MLKEM768
# SecP256r1MLKEM768
# Configure Nginx to offer hybrid PQC key exchange
# Add to your ssl configuration:
ssl_ecdh_curve X25519MLKEM768:SecP256r1MLKEM768:X25519:secp256r1:secp384r1;
Testing PQC TLS Connections
# Test hybrid PQC key exchange with s_client
openssl s_client -connect example.com:443 \
-groups X25519MLKEM768 -brief
# Connection should show:
# Protocol version: TLSv1.3
# Groups: X25519MLKEM768
# Test against your own server
openssl s_server -accept 4433 \
-cert server.crt -key server.key \
-groups X25519MLKEM768:X25519:secp256r1
# In another terminal
openssl s_client -connect localhost:4433 \
-groups X25519MLKEM768 -brief
# Verify the negotiated group
openssl s_client -connect localhost:4433 -groups X25519MLKEM768 2>/dev/null | \
grep "Server Temp Key"
# Should show: Server Temp Key: X25519MLKEM768
Browser and Client Compatibility
As of early 2026, here's where PQC TLS support stands:
- Chrome 131+ — X25519+ML-KEM-768 enabled by default (since November 2024).
- Firefox 132+ — X25519+ML-KEM-768 enabled by default.
- Edge — Follows Chrome's Chromium base, same support.
- Safari — ML-KEM support added in Safari 18.x (macOS Sequoia, iOS 18).
- curl 8.9+ — Supports hybrid PQC when built with OpenSSL 3.5+.
- Java 24+ — ML-KEM support in the JDK's TLS implementation.
- Go 1.24+ — X25519+ML-KEM-768 hybrid key exchange enabled by default.
The key point: adding hybrid PQC groups to your server configuration is backward compatible. Clients that support PQC will negotiate the hybrid key exchange; clients that don't will simply fall back to classical X25519 or ECDHE. There's no downside to enabling it today, and the upside is protection against future quantum decryption of today's traffic.
Migration Planning Timeline
Based on current NIST guidance and where the industry is headed, here's a practical migration timeline:
- Now (2026) — Enable hybrid X25519+ML-KEM-768 key exchange on all public-facing TLS servers. This is a configuration change, not a code change. There's no reason to delay.
- 2026-2027 — Begin testing ML-DSA (post-quantum signatures) for certificate chains. Certificate sizes increase significantly with PQC signatures, so test for compatibility with your infrastructure.
- 2027-2028 — Migrate internal mTLS to hybrid PQC certificates as CA support matures.
- 2028-2030 — Complete migration to PQC-only or hybrid certificates for all services, aligned with NIST's target deprecation timeline for classical-only cryptography.
Auditing and Monitoring TLS Security
Configuration drift, software updates, and certificate changes can silently degrade your TLS security posture. Continuous auditing is essential — you want to catch regressions before attackers or compliance auditors do.
Scanning with testssl.sh
testssl.sh is the gold standard for comprehensive TLS testing from the command line. It checks protocols, ciphers, vulnerabilities, certificate chain validity, and more — all without external dependencies:
# Install testssl.sh
git clone --depth 1 https://github.com/drwetter/testssl.sh.git
cd testssl.sh
# Full scan of a host
./testssl.sh example.com
# Quick scan focusing on vulnerabilities only
./testssl.sh --vulnerable example.com
# Check specific issues
./testssl.sh --protocols --ciphers --server-preference example.com
# JSON output for automation and integration with SIEM
./testssl.sh --jsonfile results.json example.com
# Scan multiple hosts from a file
./testssl.sh --file hostlist.txt --jsonfile bulk-results.json
# Check for specific vulnerabilities
./testssl.sh --heartbleed --ccs-injection --ticketbleed --robot \
--crime --breach --poodle --freak --logjam example.com
Automated Scanning with sslyze
sslyze is a Python-based scanner that's particularly useful for CI/CD integration and compliance checking against Mozilla's TLS guidelines:
# Install sslyze
pip install sslyze
# Scan with Mozilla compliance check
sslyze --mozilla_config=modern example.com
# JSON output for pipeline integration
sslyze --json_out=results.json example.com
# Check multiple servers
sslyze --mozilla_config=modern \
example.com api.example.com mail.example.com:465
Continuous Monitoring with Lynis
Lynis is a comprehensive security auditing tool that includes TLS and cryptographic configuration checks as part of its broader system hardening assessment:
# Install Lynis
sudo apt install lynis # Debian/Ubuntu
sudo dnf install lynis # RHEL/Fedora
# Run a full system audit (includes crypto/TLS checks)
sudo lynis audit system
# Check just the crypto-related tests
sudo lynis audit system --tests-from-group crypto
# Lynis checks for:
# - Weak SSL/TLS configurations in web servers
# - Expired or soon-to-expire certificates
# - Weak cipher suites enabled
# - Missing HSTS headers
# - OpenSSL version and known vulnerabilities
# - SSH cryptographic settings (key exchange, ciphers, MACs)
Building an Automated Compliance Pipeline
For organizations that need continuous compliance verification, you can combine these tools into an automated pipeline that runs on a schedule and alerts on regressions:
# /etc/systemd/system/tls-audit.service
[Unit]
Description=Automated TLS security audit
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/tls-audit.sh
User=security-auditor
Group=security-auditor
# /etc/systemd/system/tls-audit.timer
[Unit]
Description=Weekly TLS security audit
[Timer]
OnCalendar=Mon *-*-* 03:00:00
Persistent=true
[Install]
WantedBy=timers.target
#!/bin/bash
# /usr/local/bin/tls-audit.sh
# Automated TLS audit with alerting
REPORT_DIR="/var/log/tls-audit"
DATE=$(date +%Y-%m-%d)
ALERT_EMAIL="[email protected]"
mkdir -p "$REPORT_DIR"
# Run testssl.sh against all public endpoints
/opt/testssl.sh/testssl.sh \
--jsonfile "$REPORT_DIR/testssl-$DATE.json" \
--file /etc/tls-audit/endpoints.txt \
--quiet
# Parse results for critical findings
CRITICAL=$(python3 -c "
import json, sys
with open('$REPORT_DIR/testssl-$DATE.json') as f:
data = json.load(f)
findings = [r for r in data if r.get('severity') in ('CRITICAL', 'HIGH')]
for f in findings:
print(f'{f[\"ip\"]}: {f[\"id\"]} - {f[\"finding\"]}')
if findings:
sys.exit(1)
")
if [ $? -ne 0 ]; then
echo "$CRITICAL" | mail -s "TLS Audit ALERT: Critical findings on $DATE" "$ALERT_EMAIL"
fi
Frequently Asked Questions
What is the minimum TLS version I should allow in 2026?
TLS 1.2 is the absolute minimum. TLS 1.0 and 1.1 were formally deprecated by RFC 8996 in March 2021, and all major browsers have dropped support entirely. If your client base allows it — and for most internet-facing services it does — enforce TLS 1.3 only. TLS 1.3 eliminates entire classes of attacks (BEAST, POODLE, Lucky13, padding oracle attacks on CBC mode) by design, cuts handshake latency from two round trips to one, and mandates forward secrecy. The only legitimate reason to keep TLS 1.2 around is backward compatibility with older clients, embedded devices, or legacy systems that simply can't be upgraded. If you do allow TLS 1.2, restrict it to AEAD cipher suites (AES-GCM and ChaCha20-Poly1305) with ECDHE key exchange only.
How do I disable TLS 1.0 and 1.1 on Linux?
The approach depends on whether you have system-wide crypto policies available. On RHEL/Fedora, the simplest method is sudo update-crypto-policies --set DEFAULT (or FUTURE), which disables TLS 1.0 and 1.1 across all supported libraries in one command. On Debian/Ubuntu, you'll need to configure each service individually. In the OpenSSL system config (/etc/ssl/openssl.cnf), set MinProtocol = TLSv1.2 in the [system_default_sect] section. For Nginx, use ssl_protocols TLSv1.2 TLSv1.3;. For Apache, use SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1. After making changes, verify with openssl s_client -connect yourserver:443 -tls1 — the connection should fail.
What are crypto policies in RHEL and how do I use them?
Crypto policies are a RHEL/Fedora feature (available since RHEL 8) that gives you a single control point for cryptographic settings across all major libraries on the system — OpenSSL, GnuTLS, NSS, OpenSSH, Libreswan, and BIND. Instead of editing individual configuration files for each application, you run update-crypto-policies --set POLICY_NAME and the framework generates appropriate config snippets for each library. The four built-in levels are LEGACY (maximum compatibility), DEFAULT (reasonable security), FUTURE (forward-looking security), and FIPS (FIPS 140-3 compliance). You can also create custom policy modules that modify a base level — for example, FUTURE:TLS13ONLY to enforce TLS 1.3 minimum on top of the FUTURE policy. This is particularly valuable in large environments where ensuring consistent cryptographic settings across hundreds of services would otherwise be an error-prone, manual nightmare.
Is TLS 1.3 enough for post-quantum security?
No, and this is an important distinction. TLS 1.3 uses classical cryptographic algorithms (X25519, P-256, RSA, ECDSA) that are vulnerable to quantum computers running Shor's algorithm. The protocol itself isn't the problem — it's the underlying algorithms that need to change. To achieve post-quantum security, you need hybrid key exchange that combines a classical algorithm with a post-quantum one like ML-KEM-768. OpenSSL 3.5+ supports hybrid groups like X25519MLKEM768 that you can configure in your server's TLS settings. The protocol is still TLS 1.3, but the key exchange becomes quantum-resistant. If you're worried about "harvest now, decrypt later" attacks on today's traffic, enabling hybrid PQC key exchange is the single most impactful step you can take right now.
How do I test my server's TLS configuration?
Use multiple tools for a thorough assessment. testssl.sh is the most comprehensive command-line option — run ./testssl.sh yourdomain.com for a full report covering protocols, ciphers, vulnerabilities, and certificate chain validation. sslyze is great for checking compliance against Mozilla's recommended TLS profiles (sslyze --mozilla_config=modern yourdomain.com). For a quick visual report, the Qualys SSL Labs test at ssllabs.com/ssltest works well — aim for an A+ grade. For internal services, openssl s_client is invaluable for targeted testing: openssl s_client -connect host:443 -brief shows the negotiated protocol and cipher in a single command. Run these tests after every configuration change, and automate them on a weekly or monthly schedule to catch configuration drift before it becomes a problem.