QCecuring - Enterprise Security Solutions

Apache SSL/TLS Configuration Guide: Complete Setup & Hardening

SSL/TLS 15 May, 2026 · 06 Mins read

Configure Apache HTTPD with SSL/TLS from scratch — mod_ssl setup, VirtualHost HTTPS, cipher hardening, HSTS, OCSP stapling, Let's Encrypt with Certbot, SNI multi-site hosting, and mTLS client authentication. Working configs for Ubuntu/Debian and RHEL/CentOS.


Apache HTTPD still powers roughly 30% of all websites. Despite the rise of Nginx and Caddy, Apache remains the default on RHEL, CentOS, and many enterprise Linux distributions. Getting TLS right on Apache means understanding mod_ssl directives, VirtualHost scoping, and the quirks that differ between Debian-based and Red Hat-based systems.

This guide covers everything from initial mod_ssl installation through production hardening — protocol selection, cipher suites, HSTS, OCSP stapling, Let’s Encrypt automation, SNI multi-site hosting, and mutual TLS. Every config block is copy-paste ready.


How Apache TLS Works: Request Flow

Flowchart showing top-down process flow


Installing mod_ssl

Ubuntu / Debian

sudo apt update
sudo apt install apache2
sudo a2enmod ssl
sudo a2enmod headers
sudo a2enmod rewrite
sudo systemctl restart apache2

RHEL / CentOS / Rocky Linux

sudo dnf install httpd mod_ssl
sudo systemctl enable httpd
sudo systemctl start httpd

On RHEL-based systems, mod_ssl installs a default config at /etc/httpd/conf.d/ssl.conf. You’ll want to replace its contents with a hardened configuration.


Basic HTTPS VirtualHost Configuration

Ubuntu/Debian — /etc/apache2/sites-available/example.com-ssl.conf

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/public

    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
    SSLHonorCipherOrder on

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

    # Security headers
    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"

    # Logging
    ErrorLog ${APACHE_LOG_DIR}/example.com-ssl-error.log
    CustomLog ${APACHE_LOG_DIR}/example.com-ssl-access.log combined
</VirtualHost>

Enable the site:

sudo a2ensite example.com-ssl.conf
sudo apachectl configtest
sudo systemctl reload apache2

RHEL/CentOS — /etc/httpd/conf.d/example.com-ssl.conf

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com/public

    SSLEngine on
    SSLCertificateFile /etc/pki/tls/certs/example.com.crt
    SSLCertificateKeyFile /etc/pki/tls/private/example.com.key
    SSLCertificateChainFile /etc/pki/tls/certs/example.com-chain.crt

    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
    SSLHonorCipherOrder on

    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
    Header always set X-Content-Type-Options "nosniff"
    Header always set X-Frame-Options "DENY"

    ErrorLog /var/log/httpd/example.com-ssl-error.log
    CustomLog /var/log/httpd/example.com-ssl-access.log combined
</VirtualHost>

Reload:

sudo apachectl configtest
sudo systemctl reload httpd

Certificate File Paths Explained

Apache uses three directives for certificate files:

DirectivePurposeExample Path
SSLCertificateFileServer certificate (or full chain in Apache 2.4.8+)/etc/letsencrypt/live/domain/fullchain.pem
SSLCertificateKeyFilePrivate key/etc/letsencrypt/live/domain/privkey.pem
SSLCertificateChainFileIntermediate CA certificates (deprecated in 2.4.8+)/etc/pki/tls/certs/chain.crt

Since Apache 2.4.8, you can include the full chain in SSLCertificateFile (leaf + intermediates concatenated). The separate SSLCertificateChainFile directive still works but is no longer required.


Protocol Hardening: Disable TLS 1.0 and 1.1

# Allow only TLS 1.2 and TLS 1.3
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1

For Apache 2.4.36+ with OpenSSL 1.1.1+, you can also set TLS 1.3 cipher suites separately:

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
SSLCipherSuite TLSv1.3 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
SSLHonorCipherOrder on

Verify your protocol configuration:

# Test TLS 1.0 (should fail)
openssl s_client -connect example.com:443 -tls1 2>&1 | grep -i "alert\|error"

# Test TLS 1.2 (should succeed)
openssl s_client -connect example.com:443 -tls1_2 -brief

# Test TLS 1.3 (should succeed)
openssl s_client -connect example.com:443 -tls1_3 -brief

OCSP Stapling

OCSP stapling eliminates the need for clients to contact the CA’s OCSP responder directly. Apache fetches and caches the OCSP response, then staples it to the TLS handshake.

# Global settings (in ssl.conf or global config)
SSLUseStapling on
SSLStaplingCache shmcb:/var/run/ocsp(128000)
SSLStaplingResponderTimeout 5
SSLStaplingReturnResponderErrors off

# Per-VirtualHost (optional — override CA responder URL)
SSLStaplingForceURL http://ocsp.example-ca.com

The SSLStaplingCache directive must be placed outside any <VirtualHost> block — it’s a global setting.

Verify OCSP stapling is working:

openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null | grep -A 5 "OCSP Response"

Expected output:

OCSP Response Status: successful (0x0)

HTTP to HTTPS Redirect

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

    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

Or the simpler approach without mod_rewrite:

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

Let’s Encrypt with Certbot for Apache

Certbot has a dedicated Apache plugin that handles certificate installation and VirtualHost configuration automatically.

Installation

# Ubuntu/Debian
sudo apt install certbot python3-certbot-apache

# RHEL/CentOS
sudo dnf install certbot python3-certbot-apache

Obtain and Install Certificate

# Interactive — Certbot modifies your VirtualHost config
sudo certbot --apache -d example.com -d www.example.com

# Non-interactive (for automation)
sudo certbot --apache --non-interactive --agree-tos --email admin@example.com -d example.com -d www.example.com

Certificate-Only (No Apache Config Changes)

sudo certbot certonly --apache -d example.com -d www.example.com

Auto-Renewal

Certbot installs a systemd timer or cron job automatically. Verify:

sudo certbot renew --dry-run

Check the timer:

systemctl list-timers | grep certbot

SNI: Multiple Sites on One IP

Server Name Indication (SNI) allows Apache to serve different certificates for different domains on the same IP address and port. Every modern client supports SNI.

# Site 1
<VirtualHost *:443>
    ServerName app.example.com
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/app.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/app.example.com/privkey.pem
    DocumentRoot /var/www/app
</VirtualHost>

# Site 2
<VirtualHost *:443>
    ServerName api.example.com
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/api.example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/api.example.com/privkey.pem
    DocumentRoot /var/www/api
</VirtualHost>

# Site 3 — wildcard
<VirtualHost *:443>
    ServerName *.example.com
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    DocumentRoot /var/www/wildcard
</VirtualHost>

Apache matches VirtualHosts in order. Place specific ServerName entries before wildcard entries.


Mutual TLS (mTLS) — Client Certificate Authentication

mTLS requires clients to present a certificate during the TLS handshake. Useful for API-to-API communication, zero-trust architectures, and internal services.

<VirtualHost *:443>
    ServerName internal-api.example.com
    SSLEngine on
    SSLCertificateFile /etc/pki/tls/certs/server.crt
    SSLCertificateKeyFile /etc/pki/tls/private/server.key

    # Client certificate verification
    SSLVerifyClient require
    SSLVerifyDepth 3
    SSLCACertificateFile /etc/pki/tls/certs/client-ca-bundle.crt

    # Optional: restrict by client cert attributes
    <Location /admin>
        SSLRequire %{SSL_CLIENT_S_DN_O} eq "My Organization"
    </Location>

    # Pass client cert info to application
    SSLOptions +StdEnvVars +ExportCertData
    RequestHeader set X-Client-DN "%{SSL_CLIENT_S_DN}s"
    RequestHeader set X-Client-Verify "%{SSL_CLIENT_VERIFY}s"
</VirtualHost>

SSLVerifyClient options:

  • none — No client certificate requested
  • optional — Client certificate requested but not required
  • require — Client must present a valid certificate
  • optional_no_ca — Client certificate requested, CA validation skipped

Common Apache SSL Errors and Fixes

AH02572: Failed to configure at least one certificate and key

Cause: Certificate and key files don’t match, or file paths are wrong.

# Verify cert and key match
openssl x509 -noout -modulus -in /path/to/cert.crt | openssl md5
openssl rsa -noout -modulus -in /path/to/key.key | openssl md5
# Both MD5 hashes must be identical

AH02565: Certificate and private key do not match

Cause: You’re using a certificate from a different key pair.

# Check which key signed the certificate
openssl x509 -noout -text -in cert.crt | grep "Public Key"
openssl rsa -noout -text -in key.key | grep "Public-Key"

Regenerate the CSR with the correct key and reissue the certificate.

AH01909: RSA certificate configured for server does not include an ID which matches the server name

Cause: The ServerName in your VirtualHost doesn’t match any SAN (Subject Alternative Name) or CN in the certificate.

# Check certificate SANs
openssl x509 -noout -text -in cert.crt | grep -A 1 "Subject Alternative Name"

Ensure your certificate covers the exact domain (or wildcard) specified in ServerName.

SSL Library Error: error:0A0000C1:SSL routines::no shared cipher

Cause: No overlap between server cipher suite and client capabilities. Usually means your cipher string is too restrictive or the certificate type (RSA vs ECDSA) doesn’t match the configured ciphers.

# Test which ciphers your server accepts
nmap --script ssl-enum-ciphers -p 443 example.com

Permission Denied on Key File

# Key file permissions should be restrictive
sudo chmod 600 /etc/letsencrypt/live/example.com/privkey.pem
sudo chown root:root /etc/letsencrypt/live/example.com/privkey.pem

# Apache must be able to read it during startup (runs as root initially)
sudo apachectl configtest

Global Hardening Configuration

For a complete hardened setup, place these directives in your global SSL configuration file:

Ubuntu/Debian — /etc/apache2/conf-available/ssl-hardening.conf

# TLS Protocol and Cipher Configuration
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
SSLCipherSuite TLSv1.3 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off

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

Enable it:

sudo a2enconf ssl-hardening
sudo apachectl configtest
sudo systemctl reload apache2

RHEL/CentOS — /etc/httpd/conf.d/ssl-hardening.conf

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
SSLCipherSuite TLSv1.3 TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
SSLHonorCipherOrder on
SSLCompression off
SSLSessionTickets off

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

Testing Your Configuration

SSL Labs

Submit your domain to SSL Labs for a comprehensive grade. Target: A+.

Command-Line Verification

# Full handshake details
openssl s_client -connect example.com:443 -servername example.com

# Check certificate expiry
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

# Verify HSTS header
curl -sI https://example.com | grep -i strict-transport

# Check supported protocols
nmap --script ssl-enum-ciphers -p 443 example.com

# Verify no TLS 1.0/1.1
openssl s_client -connect example.com:443 -tls1 2>&1 | grep -i "handshake\|error"
openssl s_client -connect example.com:443 -tls1_1 2>&1 | grep -i "handshake\|error"

FAQ

Q: What’s the difference between SSLCertificateFile and SSLCertificateChainFile?

Since Apache 2.4.8, SSLCertificateFile can contain the full chain (leaf + intermediates concatenated). SSLCertificateChainFile is deprecated but still functional for older configs. If you’re on Apache 2.4.8+, just put the full chain in SSLCertificateFile and skip SSLCertificateChainFile entirely.

Q: Why does Apache show “AH02572” after certificate renewal?

The renewed certificate doesn’t match the private key. This happens when you regenerate the key during renewal but point SSLCertificateKeyFile to the old key path. Verify with openssl x509 -noout -modulus -in cert.crt | openssl md5 and compare against the key’s modulus.

Q: Can I use the same certificate for multiple VirtualHosts?

Yes — if it’s a wildcard certificate or a multi-SAN certificate covering all the domains. Each VirtualHost can reference the same SSLCertificateFile and SSLCertificateKeyFile paths.

Q: How do I force Apache to prefer server cipher order?

Set SSLHonorCipherOrder on. This tells Apache to use the server’s cipher preference (the order in your SSLCipherSuite directive) rather than the client’s preference. This ensures the strongest cipher is always selected.

Q: Where should SSLStaplingCache be placed?

It must be placed in the global server config, outside any <VirtualHost> block. Placing it inside a VirtualHost causes Apache to fail with a syntax error. On Debian, put it in /etc/apache2/conf-available/ssl-hardening.conf. On RHEL, put it in /etc/httpd/conf.d/ssl-hardening.conf.

Q: How do I configure Apache for both RSA and ECDSA certificates?

Apache 2.4.8+ supports multiple SSLCertificateFile directives in the same VirtualHost. Apache automatically selects the appropriate certificate based on the cipher negotiated:

SSLCertificateFile /etc/certs/example.com-rsa.crt
SSLCertificateKeyFile /etc/certs/example.com-rsa.key
SSLCertificateFile /etc/certs/example.com-ecdsa.crt
SSLCertificateKeyFile /etc/certs/example.com-ecdsa.key

Related Reading:

SSL Config Generator

Generate hardened Apache TLS configs with one click — protocol versions, ciphers, and headers pre-configured.

Generate Config

Related Insights

SSL/TLS

Fix 'The Certificate Chain Could Not Be Built to a Trusted Root Authority'

Fix the Windows certificate chain trust error. Covers missing root CA, intermediate certificate gaps, AIA/CDP issues, GPO trust distribution, and manual import — with certutil verification commands.

By Shivam sharma

15 May, 2026 · 06 Mins read

SSL/TLSTroubleshootingPKI

SSL/TLS

Fix 'Unable to Get Local Issuer Certificate' (OpenSSL, curl, Git, npm)

Fix the 'unable to get local issuer certificate' error in OpenSSL, curl, Git, npm, pip, and Docker. Covers missing CA bundles, corporate proxies, and trust store configuration for every platform.

By Sneha gupta

15 May, 2026 · 07 Mins read

SSL/TLSTroubleshootingDevOps

SSL/TLS

Fix 'PKIX Path Building Failed' in Java: Every Cause & Solution

Fix the PKIX path building failed error in Java. Covers keytool import, cacerts configuration, corporate proxies, Spring Boot, Maven/Gradle builds, and Docker containers — without disabling certificate validation.

By Shivam sharma

15 May, 2026 · 06 Mins read

SSL/TLSTroubleshootingDevOps

Ready to Secure Your Enterprise?

Experience how our cryptographic solutions simplify, centralize, and automate identity management for your entire organization.

Stay ahead on cryptography & PKI

Get monthly insights on certificate management, post-quantum readiness, and enterprise security. No spam.

We respect your privacy. Unsubscribe anytime.