Apache’s SSL configuration has improved a lot over the years. Getting it right involves enabling the right modules, configuring ciphers, setting up HSTS, and making sure the certificate chain is complete. Here’s the configuration I use.
Required Modules
Make sure these modules are enabled:
a2enmod ssl
a2enmod rewrite
a2enmod headers # Required for security headers including HSTS
a2enmod http2 # Optional but recommended
systemctl restart apache2
Verify:
apache2ctl -M | grep -E "ssl|rewrite|headers|http2"
Virtual Host Configuration
A complete HTTPS virtual host:
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/html
# Certificate files
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem
# Modern TLS protocols
SSLProtocol -all +TLSv1.2 +TLSv1.3
# Cipher suites
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 off
SSLSessionTickets off
# OCSP Stapling
SSLUseStapling on
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off
# Security headers
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
<Directory /var/www/html>
AllowOverride All
Require all granted
</Directory>
</VirtualHost>
SSLProtocol -all +TLSv1.2 +TLSv1.3 — disables all protocols first, then enables TLS 1.2 and 1.3 explicitly. Cleaner than listing what to disable.
SSLHonorCipherOrder off — let the client choose the cipher. Same reasoning as Nginx’s ssl_prefer_server_ciphers off.
SSLSessionTickets off — disable session tickets for better forward secrecy. A small performance cost.
HTTP Redirect
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
RewriteEngine On
RewriteRule ^(.*)$ https://example.com$1 [R=301,L]
</VirtualHost>
Or simpler with Redirect:
<VirtualHost *:80>
ServerName example.com
Redirect permanent / https://example.com/
</VirtualHost>
I prefer the Redirect approach for simple redirects — RewriteRule is more powerful but the extra complexity isn’t needed for a basic HTTP→HTTPS redirect.
OCSP Stapling Configuration
OCSP stapling needs to be configured at the server level (not just in the virtual host):
In /etc/apache2/mods-enabled/ssl.conf or in your apache2.conf:
SSLUseStapling on
SSLStaplingCache "shmcb:${APACHE_RUN_DIR}/ssl_stapling(512000)"
Restart Apache and verify:
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null | grep "OCSP response"
If you see OCSP Response Status: successful (0x0), stapling is working.
DH Parameters
Generate if you haven’t:
openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
Configure in Apache:
SSLOpenSSLConfCmd DHParameters "/etc/ssl/certs/dhparam.pem"
HTTP/2 Configuration
Protocols h2 http/1.1
<VirtualHost *:443>
# HTTP/2 is negotiated via ALPN during TLS handshake
# Nothing else needed if Protocols is set globally
</VirtualHost>
Set Protocols h2 http/1.1 globally or per virtual host. Apache handles the HTTP/2 negotiation during TLS.
Check HTTP/2 is working:
curl -I --http2 https://example.com | grep "HTTP/2"
SSL Session Cache
# In ssl.conf or httpd.conf
SSLSessionCache "shmcb:/var/run/apache2/ssl_scache(512000)"
SSLSessionCacheTimeout 300
The shared memory cache stores SSL session data, reducing handshake overhead for returning clients.
Virtual Host for Multiple Domains on One IP (SNI)
Apache uses SNI (Server Name Indication) to serve multiple SSL sites from one IP:
# First site
<VirtualHost *:443>
ServerName site1.example.com
SSLCertificateFile /etc/ssl/site1.crt
SSLCertificateKeyFile /etc/ssl/site1.key
SSLCertificateChainFile /etc/ssl/site1-chain.pem
# ...
</VirtualHost>
# Second site
<VirtualHost *:443>
ServerName site2.example.com
SSLCertificateFile /etc/ssl/site2.crt
SSLCertificateKeyFile /etc/ssl/site2.key
SSLCertificateChainFile /etc/ssl/site2-chain.pem
# ...
</VirtualHost>
Each virtual host has its own certificate. SNI is supported by all modern browsers and clients.
Configuration Testing
Always test before restarting:
apachectl configtest
If it says “Syntax OK”, safe to restart:
systemctl restart apache2
For config changes that don’t require a full restart (like security headers), use graceful restart:
apachectl graceful
Graceful restart lets existing connections finish before applying the new config — no dropped connections.
Common Issues
Certificate chain incomplete — if you get SSLCertificateChainFile errors or browsers complain about the chain, verify the chain file contains the intermediate certificates. For Let’s Encrypt, use chain.pem, not cert.pem.
Protocol mismatch — if clients report TLS errors, check SSLProtocol includes what they support. Very old clients need TLS 1.2 minimum.
HSTS cached in browser — if you misconfigure HSTS (wrong max-age, or set it on an HTTP host by mistake), it caches in the browser. Clear HSTS in browser settings (usually under security/HSTS settings in chrome://net-internals/#hsts).
OpenSSL version matters — some cipher suites and TLS features require a specific minimum OpenSSL version. openssl version and apache2 -v show what you have. If they’re outdated (Ubuntu 18.04 ships older OpenSSL), some modern cipher configurations won’t work.
For generating and managing the certificates themselves, the Let’s Encrypt with Certbot guide covers the full certificate workflow, and the SSL certificate formats guide explains the PEM, chain, and combined certificate files Apache uses.