Skip to content
EdgeServers
Blog

apache

Apache TLS hardening in 2026 — ciphers, OCSP stapling, and the cert renewal pipeline

TLS 1.3 is the default, but most Apache installs still have config from the 2018 ciphersuite wars. Here's what actually belongs in your SSL config today.

May 15, 2026 · 10 min · by Sudhanshu K.

Apache TLS hardening in 2026 — ciphers, OCSP stapling, and the cert renewal pipeline

A useful diagnostic for the age of any Apache install: look at the SSL config. If you see SSLProtocol all -SSLv2 -SSLv3 or a SSLCipherSuite with !aNULL:!MD5 somewhere in it, that's a 2014-era template that's been carried forward through every Apache upgrade since. It probably still works. It almost certainly isn't what you should be running in 2026.

This post is about what an Apache TLS config actually looks like today on a managed Apache server, and the renewal pipeline that keeps certificates valid without anyone having to remember anything.

TLS 1.3 is the default, finally

In 2026, every browser and every reasonable client supports TLS 1.3. The handshake is faster (one round-trip instead of two), the cipher selection is dramatically simpler (no more bring-your-own-MAC-and-key-exchange combinatorial explosions), and the protocol is significantly cleaner.

This means the TLS config we ship is short. Here's the SSL section we drop into every new virtual host:

# /etc/apache2/sites-available/default-ssl.conf — the parts that matter
<VirtualHost *:443>
    ServerName www.example.com
    DocumentRoot /var/www/html
 
    SSLEngine on
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    SSLHonorCipherOrder off
    SSLCipherSuite TLSv1.3 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
    SSLCipherSuite TLSv1.2 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
 
    SSLCertificateFile      /etc/letsencrypt/live/www.example.com/fullchain.pem
    SSLCertificateKeyFile   /etc/letsencrypt/live/www.example.com/privkey.pem
 
    SSLUseStapling on
    SSLStaplingCache shmcb:/var/run/ocsp(128000)
    SSLStaplingResponderTimeout 5
    SSLStaplingReturnResponderErrors off
 
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Header always set X-Content-Type-Options "nosniff"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "geolocation=(), camera=(), microphone=()"
</VirtualHost>

A few specific choices to call out:

SSLProtocol -all +TLSv1.2 +TLSv1.3. We disable everything and then re-enable specifically. TLS 1.0 and 1.1 have been deprecated by the IETF since RFC 8996 (2021); they're still required for some very legacy clients but we've not had a customer needing them in over two years. If you do, it's a conscious decision documented in the config, not a default that silently accepted bad traffic.

SSLHonorCipherOrder off. Counterintuitively, in 2026 you want the client to pick the cipher. TLS 1.3 ciphers are all good. ChaCha20-Poly1305 is faster on mobile devices without AES hardware acceleration; AES-GCM is faster on x86 with AES-NI. Letting the client choose means each device gets its preferred cipher, which matters more than you might think for mobile-heavy workloads.

The TLS 1.2 cipher list. We keep TLS 1.2 enabled for the long tail of clients (mostly older Android devices and some embedded clients). The cipher list is the Mozilla "Intermediate" configuration: ECDHE for forward secrecy, ECDSA preferred over RSA where supported, AEAD ciphers only (no CBC). No 3DES, no RC4, no MD5, no SHA-1.

OCSP stapling: yes, still

OCSP stapling is the optimisation where the server pre-fetches the certificate's revocation status from the CA and "staples" it to the TLS handshake. Without stapling, every connecting client has to do its own OCSP lookup, which leaks the user's browsing to the CA and adds latency.

The four lines that turn it on:

SSLUseStapling on
SSLStaplingCache shmcb:/var/run/ocsp(128000)
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off

The two settings that catch people out:

  • SSLStaplingResponderTimeout 5 — five seconds for the OCSP responder to respond. Default is 10; we drop it because if Let's Encrypt's OCSP responder is slow enough to take 10 seconds, it's effectively down and we don't want to block.
  • SSLStaplingReturnResponderErrors off — if the OCSP fetch fails, don't serve the error to the client. Without this, an outage at the CA's OCSP responder breaks your HTTPS site. With it, the handshake proceeds without stapling and the client falls back to its own (cached) revocation check.

Stapling adds maybe 2-5% to handshake performance for repeat visitors and meaningfully more for first-time TLS sessions. It's free; turn it on.

HSTS, and the preload list

Strict-Transport-Security tells browsers "for the next N seconds, never let users connect to this site over plain HTTP, no matter what." It's the defence against SSL stripping attacks on networks the user doesn't control.

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

The three parameters:

  • max-age=63072000 — two years in seconds. Long enough that the protection is meaningful, short enough that browsers will eventually let go if you migrate away.
  • includeSubDomains — every subdomain inherits the policy. You have to be confident every subdomain can do HTTPS before turning this on; the migration is one-way.
  • preload — opts the domain into the browser preload list. Once on the list, browsers refuse plain HTTP to the domain before the first request, closing the bootstrap-time gap where HSTS doesn't yet apply.

The preload list is hard to reverse — getting removed takes weeks and propagates slowly. So we only add the directive after the customer has run HSTS without preload for at least 90 days, confirmed every subdomain is reachable over HTTPS, and explicitly consented to the long-term commitment.

The other security headers

Four headers we ship on every site by default:

  • X-Content-Type-Options: nosniff — disables browser MIME-type sniffing. Prevents a class of attacks where a malicious upload is served back to a victim with a guessed Content-Type.
  • Referrer-Policy: strict-origin-when-cross-origin — sane default. Internal navigations get full referrer; cross-origin gets just the origin; HTTPS-to-HTTP gets nothing.
  • Permissions-Policy — disables browser features the site doesn't use. Tightens the attack surface against compromised third-party JavaScript.
  • Content-Security-Policywe don't ship a default. CSP is too application-specific to template, and a wrong CSP breaks the site for users in confusing ways. We work with the customer's application team to define one per-site, starting in report-only mode for at least two weeks before enforcing.

The fifth header that comes up in audits: X-Frame-Options. In 2026 this is superseded by CSP's frame-ancestors directive, but we still ship X-Frame-Options: SAMEORIGIN for clients that ignore CSP. Belt and braces.

The cert renewal pipeline

Certificates expire. In 2026, Let's Encrypt certs are 90 days; some commercial CAs are starting to issue 47-day certs in line with CA/B Forum directions. Either way, renewal can't be manual. The pipeline we run:

# /etc/cron.d/certbot-renewal
0 2,14 * * * root certbot renew --quiet --deploy-hook "/usr/local/bin/cert-deploy-hook.sh"

Twice daily because Let's Encrypt's renewal cadence works out to "30 days before expiry" — running twice daily means even if a renewal window briefly fails, the next attempt is hours away. The --deploy-hook only runs when a renewal actually happened, so we're not reloading Apache every 12 hours.

The deploy hook itself:

#!/bin/bash
# /usr/local/bin/cert-deploy-hook.sh
set -euo pipefail
 
# Validate the new cert before touching the running service
for domain in $RENEWED_DOMAINS; do
    openssl x509 -in "/etc/letsencrypt/live/$domain/fullchain.pem" -noout -checkend 86400
    openssl rsa -in "/etc/letsencrypt/live/$domain/privkey.pem" -check -noout
done
 
# Test Apache config before reloading
apache2ctl configtest
 
# Graceful reload — does not drop existing connections
systemctl reload apache2
 
# Verify the live cert chain matches what we just deployed
for domain in $RENEWED_DOMAINS; do
    served_fp=$(echo | openssl s_client -connect "$domain:443" -servername "$domain" 2>/dev/null | openssl x509 -noout -fingerprint -sha256)
    deployed_fp=$(openssl x509 -in "/etc/letsencrypt/live/$domain/fullchain.pem" -noout -fingerprint -sha256)
    if [ "$served_fp" != "$deployed_fp" ]; then
        echo "FINGERPRINT MISMATCH for $domain after reload" | mail -s "Cert deploy failure" ops@example.com
        exit 1
    fi
done

The verification step at the end is the load-bearing one. We've seen too many "renewals" that succeeded in writing the new cert to disk but failed to actually load it into Apache (config error, file permissions, stale cache). Pulling the cert back over the wire after the reload and comparing fingerprints is the only way to know the renewal worked end-to-end.

The expiry monitor that runs independently of the renewal cron:

# Runs every 6 hours, alerts at 21 days and pages at 7 days
for domain in $(get-monitored-domains); do
    days_left=$(get-cert-days-remaining "$domain")
    if [ "$days_left" -lt 7 ]; then
        page "Cert for $domain expires in $days_left days"
    elif [ "$days_left" -lt 21 ]; then
        warn "Cert for $domain expires in $days_left days"
    fi
done

This catches the case where the renewal cron has been silently failing for weeks. The renewal cron is one layer of defence; the independent expiry monitor is the second. Both need to fail before a cert actually expires.

What we ship by default

For every Apache HTTPS site under our management:

  • TLS 1.2 and 1.3 only, the cipher list above
  • OCSP stapling enabled with sensible failure handling
  • HSTS with 2-year max-age, includeSubDomains after subdomain audit, preload only by explicit request
  • Security headers: nosniff, referrer-policy, permissions-policy, X-Frame-Options
  • Customer-defined CSP, deployed in report-only first
  • Let's Encrypt with twice-daily renewal cron and validating deploy hook
  • Independent cert expiry monitor with 21-day warning and 7-day page
  • Quarterly SSL Labs / testssl.sh scan with results in the customer report
  • TLS metrics scraped to Prometheus: handshake duration, OCSP cache hit rate, cert days-until-expiry

A site shipped to this config rates A+ on SSL Labs without any further work. More importantly, the renewal pipeline runs without anyone having to think about it — we've had customers with hundreds of certs across provisioned infrastructure go years without a single cert-expiry incident.

If your TLS config is older than the last LTS release of your OS, it's probably time. The migration is low-risk (mostly removing options, not adding them) and the gain on both security and performance is real. If you'd like us to audit what you've got and produce a remediation list under our cybersecurity service, that's usually a couple of hours of work.

Sudhanshu K. is a senior platform engineer at EdgeServers (RemotIQ Pty Ltd, ABN 91 682 628 128). She has been writing Apache SSL configs since OpenSSL 0.9.8 and is delighted she will probably never type "DES-CBC3-SHA" again.