DevOpsil
Apache
94%
Needs Review

Apache SSL/TLS Hardening: Get an A+ on SSL Labs

Sarah ChenSarah Chen6 min read

A default Apache SSL configuration will get you a "B" on Qualys SSL Labs at best — and that's if you're lucky. Out-of-the-box settings still support outdated protocols, weak ciphers, and missing security headers that any competent attacker will exploit. This guide walks through every setting you need to go from mediocre to A+.

Prerequisites

  • Apache 2.4.x (2.4.36+ strongly recommended for TLS 1.3 support)
  • OpenSSL 1.1.1+ on the server
  • A valid certificate (Let's Encrypt works fine)
  • mod_ssl and mod_headers enabled
sudo a2enmod ssl headers
apache2ctl -M | grep -E "ssl|headers"

The Baseline: Disable Old Protocols

TLS 1.0 and 1.1 are deprecated. SSLv2 and SSLv3 have been broken for years. Disable them all and only allow TLS 1.2 and 1.3:

# /etc/apache2/mods-available/ssl.conf or your VirtualHost

SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

This keeps TLS 1.2 and 1.3 (the all enables everything, then we subtract the old ones). To verify what protocols your server actually negotiates:

# Test TLS 1.1 — should fail
openssl s_client -connect example.com:443 -tls1_1 2>&1 | grep -E "Cipher|Protocol|error"

# Test TLS 1.2 — should succeed
openssl s_client -connect example.com:443 -tls1_2 2>&1 | grep -E "Protocol"

# Test TLS 1.3 — should succeed
openssl s_client -connect example.com:443 -tls1_3 2>&1 | grep -E "Protocol"

Cipher Suite Configuration

This is where most people get it wrong. The cipher string controls which algorithms Apache will negotiate. A modern, secure configuration:

SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384

# Let the server choose cipher order (not the client)
SSLHonorCipherOrder on

# Disable compression — CRIME attack vector
SSLCompression off

# Disable session tickets for perfect forward secrecy
SSLSessionTickets off

For TLS 1.3, cipher configuration works differently — the TLS 1.3 ciphers are set separately and are all secure by default. You can optionally restrict them:

# TLS 1.3 ciphers (all are good — this is optional hardening)
SSLOpenSSLConfCmd Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256

Cipher Suite Cheat Sheet

CipherForward SecrecyRecommended
ECDHE-RSA-AES256-GCM-SHA384Yes (ECDHE)Yes
ECDHE-RSA-CHACHA20-POLY1305Yes (ECDHE)Yes
AES256-GCM-SHA384NoNo
RC4-SHANoNever
DES-CBC3-SHANoNever

HSTS: Force HTTPS at the Browser Level

HTTP Strict Transport Security tells browsers to never connect over plain HTTP again. Once set, even if your redirect disappears, browsers refuse to make the insecure connection:

# Add inside your HTTPS VirtualHost
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"

Important: Start with a short max-age (e.g., 300) and test for a week before setting it to one year. A misconfigured HSTS header with a long max-age can lock users out if your certificate breaks.

To get on the HSTS preload list (which gets baked into browsers), you need:

  • max-age of at least 31536000 (1 year)
  • includeSubDomains present
  • preload present
  • All subdomains must also serve valid HTTPS

OCSP Stapling

Without OCSP stapling, every TLS handshake requires the client to contact your CA's OCSP responder to check if your certificate has been revoked. This adds latency and a privacy leak. Stapling caches the OCSP response on your server:

SSLUseStapling on
SSLStaplingCache "shmcb:/var/run/apache2/stapling(32768)"
SSLStaplingResponseMaxAge 900

# Inside your VirtualHost:
SSLStaplingReturnResponderErrors off
SSLStaplingFakeStartTLSExt on

The SSLStaplingCache directive must be in the global server config or a top-level context, not inside a VirtualHost.

Verify stapling is working:

openssl s_client -connect example.com:443 -status 2>&1 | grep -A 10 "OCSP Response"
# Look for: OCSP Response Status: successful

Diffie-Hellman Parameters

Weak DH parameters (the Logjam attack used 1024-bit params) undermine forward secrecy. Generate strong 4096-bit parameters:

# This takes a few minutes — run it once
openssl dhparam -out /etc/ssl/dhparams.pem 4096
SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparams.pem"

Using 2048-bit is acceptable if 4096-bit generation time is a concern; 4096-bit is overkill for most but doesn't hurt performance in practice since DH is only used during the handshake.

Complete Hardened VirtualHost Configuration

Putting it all together — a production-ready HTTPS VirtualHost:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com

    # Redirect all HTTP to HTTPS
    RewriteEngine On
    RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    DocumentRoot /var/www/html

    # Certificate
    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    # Protocol hardening
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
    SSLHonorCipherOrder on
    SSLCompression off
    SSLSessionTickets off

    # DH parameters
    SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparams.pem"

    # OCSP stapling
    SSLUseStapling on

    # Security headers
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"
    Header always set X-XSS-Protection "1; mode=block"
    Header always set Referrer-Policy "strict-origin-when-cross-origin"
    Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"

    # Remove fingerprinting headers
    Header unset X-Powered-By
    Header always unset Server

    ServerTokens Prod
    ServerSignature Off

    ErrorLog  /var/log/apache2/ssl-error.log
    CustomLog /var/log/apache2/ssl-access.log combined
</VirtualHost>

Content Security Policy

CSP is the most powerful browser-enforced security header but requires understanding your app's resource usage. Start in report-only mode:

# Report-only first — doesn't break anything, just logs violations
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; report-uri https://example.com/csp-report"

# Once validated, switch to enforcing:
# Header always set Content-Security-Policy "default-src 'self'; ..."

SSL Labs Score Checklist

Run through these before testing:

CategorySettingRequired For A+
CertificateValid, trusted chainYes
Protocol supportTLS 1.2 + 1.3 onlyYes
Cipher strengthECDHE + AES-GCMYes
Forward secrecyECDHE or DHE ciphersYes
HSTSmax-age >= 6 monthsYes
OCSP staplingEnabledYes
DH params2048-bit minimumYes
Cipher orderServer-preferredYes
# Quick local test with testssl.sh
docker run --rm -it drwetter/testssl.sh example.com

# Or install directly
git clone https://github.com/drwetter/testssl.sh.git
./testssl.sh/testssl.sh example.com

After applying all changes:

apache2ctl configtest && sudo systemctl reload apache2

Then head to ssllabs.com/ssltest and run the scan. With this configuration, you should see an A+ without caveats.

Share:

Was this article helpful?

Sarah Chen
Sarah Chen

CI/CD Engineering Lead

Automation evangelist who believes no deployment should require a human. I write pipelines, break pipelines, and write about both. Code-first, always.

Related Articles

More in Apache

View all →
ApacheQuick RefBeginnerNeeds Review

Fix Apache mod_rewrite Not Working

Diagnose and fix Apache mod_rewrite rules not being applied, covering module loading, AllowOverride settings, .htaccess issues, and rewrite logging.

Aareez Asif·
3 min read

Discussion