Table of Contents

TLS / SSL Configuration

Apache terminates TLS with mod_ssl. This page covers a modern HTTPS virtual host and then automated, free certificates with Let's Encrypt.

Enabling mod_ssl

LoadModule ssl_module    modules/mod_ssl.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so  # TLS session cache
Listen 443

Basic HTTPS Virtual Host

<VirtualHost *:443>
    ServerName example.com

    SSLEngine on
    SSLCertificateFile      "/usr/local/apache2/conf/ssl/example.com.crt"
    SSLCertificateKeyFile   "/usr/local/apache2/conf/ssl/example.com.key"

    # Modern protocol and cipher policy (disable SSLv3/TLS 1.0/1.1)
    SSLProtocol             -all +TLSv1.2 +TLSv1.3
    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     off
    SSLSessionTickets       off

    # Reverse proxy to a backend (mark the connection as HTTPS)
    ProxyPreserveHost On
    RequestHeader set X-Forwarded-Proto "https"
    ProxyPass        "/" "http://backend:80/"
    ProxyPassReverse "/" "http://backend:80/"
</VirtualHost>
Directive Purpose
SSLEngine on Enables TLS for this virtual host.
SSLCertificateFile The server certificate (with mod_ssl in Apache 2.4.8+, may contain the full chain).
SSLCertificateKeyFile The matching private key (keep readable only by root).
SSLProtocol -all +TLSv1.2 +TLSv1.3 Enables only TLS 1.2 and 1.3; disables SSLv3, TLS 1.0/1.1.
SSLCipherSuite Allowed ciphers. Use the Mozilla generator for a current list.
SSLHonorCipherOrder off With TLS 1.3 / modern suites, let the client choose; set on only for legacy ordering needs.
Tip

Generate a vetted, up-to-date mod_ssl config with the Mozilla SSL Configuration Generator (choose "Apache"). See SSL vs TLS for protocol background.

Redirect HTTP to HTTPS

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

Automated TLS with Let's Encrypt

Let's Encrypt issues free, publicly trusted certificates via the ACME protocol, valid for 90 days and designed to renew automatically. Prefer it over self-signed certificates for anything public-facing.

Note

This page covers Let's Encrypt for Apache. For the ACME protocol itself, other clients, DNS-01 wildcard validation, and rate limits, see the ACME section and the Certbot guide.

Prerequisites

  • A registered domain whose DNS A/AAAA record points at the server running Apache.
  • Inbound TCP port 80 reachable from the internet (for HTTP-01 validation).
  • Apache serving the domain named in ServerName.

Method 1 — Certbot with the Apache plugin

For Apache installed on the host, the Certbot Apache plugin obtains the certificate and edits your virtual host to use it:

# Debian/Ubuntu (see the Certbot guide for snap/other distros)
sudo apt update
sudo apt install certbot python3-certbot-apache

# Obtain and install the certificate, adding an HTTP->HTTPS redirect
sudo certbot --apache -d example.com -d www.example.com --redirect

# Verify and reload
sudo apachectl configtest && sudo systemctl reload apache2

Certbot writes the issued files under /etc/letsencrypt/live/example.com/ and points mod_ssl at them:

SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

Method 2 — Webroot (keep Apache serving)

The webroot method drops the ACME challenge file into a directory Apache already serves, so Apache never stops. Serve the challenge path from your HTTP virtual host:

<VirtualHost *:80>
    ServerName example.com

    # Serve Let's Encrypt HTTP-01 challenges from the webroot
    Alias "/.well-known/acme-challenge/" "/var/www/certbot/.well-known/acme-challenge/"
    <Directory "/var/www/certbot">
        Require all granted
    </Directory>

    # Redirect everything else to HTTPS
    RewriteEngine On
    RewriteCond %{REQUEST_URI} !^/\.well-known/acme-challenge/
    RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [L,R=301]
</VirtualHost>

Request the certificate, then reference it from the HTTPS virtual host:

sudo certbot certonly --webroot -w /var/www/certbot \
    -d example.com -d www.example.com \
    --non-interactive --agree-tos -m admin@example.com

Method 3 — Dockerized Apache + Certbot

When Apache runs in a container, run Certbot as a companion container sharing the certificate and webroot volumes:

# docker-compose.yml (excerpt)
services:
  apache:
    image: httpd:2.4
    container_name: apache
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./apache/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro
      - certbot-webroot:/var/www/certbot:ro       # serve challenge files
      - certbot-certs:/etc/letsencrypt:ro          # read issued certs
    networks: [ web ]

  certbot:
    image: certbot/certbot:latest
    container_name: certbot
    volumes:
      - certbot-webroot:/var/www/certbot           # write challenge files
      - certbot-certs:/etc/letsencrypt             # store issued certs
    # Attempt renewal every 12 hours; certbot renews only when due
    entrypoint: >
      sh -c 'trap exit TERM;
      while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done'

volumes:
  certbot-webroot:
  certbot-certs:

networks:
  web:
    driver: bridge

Point mod_ssl at the shared certs (/etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem), then issue the initial certificate once:

docker compose run --rm certbot certonly --webroot -w /var/www/certbot \
    -d example.com -d www.example.com \
    --non-interactive --agree-tos -m admin@example.com
docker compose up -d
Tip

For a fully hands-off container setup, consider nginx-proxy + acme-companion or a reverse proxy with built-in ACME (Caddy, Traefik) in front of Apache.

Automatic Renewal

Certificates last 90 days; Certbot renews within 30 days of expiry. A host install adds a systemd timer/cron job automatically. Apache keeps serving the old certificate until reloaded, so reload it after renewal with a deploy hook:

# Confirm the renewal timer
systemctl list-timers | grep certbot

# Test the whole renewal path without issuing a real certificate
sudo certbot renew --dry-run

# Reload Apache only when a certificate actually renews
sudo certbot renew --deploy-hook "apachectl graceful"

For the containerized setup, the renew loop handles renewal; reload Apache afterward by signalling the container (docker exec apache apachectl graceful).

Important

Test against Let's Encrypt's staging environment first (--dry-run or --test-cert) to avoid production rate limits. Always reference the /etc/letsencrypt/live/<domain>/ symlinks — never copy the files elsewhere, or renewals will not take effect.

Wildcards (DNS-01)

A wildcard certificate (*.example.com) requires DNS-01 validation and a DNS-provider API credential. See the Certbot guide for provider plugins (Cloudflare, Route 53, and others).

◄ Load Balancing · Apache Overview · Security and Hardening ►