HTTPS on Apache: mod_ssl, Let’s Encrypt, mod_md and TLS settings
HTTPS on Apache httpd 2.4 needs three things: mod_ssl, a certificate that renews itself, and sensible TLS settings. Let’s Encrypt certificates are free and can be renewed by certbot or by Apache’s own mod_md module. Since 2025 Let’s Encrypt has stopped OCSP and expiry emails, and from February 2027 its certificates get shorter, so a renewal job you set up years ago may need checking.
Before you start
- The domain name (for example
example.comandwww.example.com) points to your server in DNS. - Port 80 is open from the internet. Let’s Encrypt’s usual HTTP-01 check fetches a file over plain HTTP; the TLS-ALPN-01 check uses port 443 instead. Wildcard certificates need the DNS-01 check.
- Port 443 is open in the firewall.
- mod_ssl is installed and loaded.
On Debian and Ubuntu, mod_ssl ships with the apache2 package and only needs enabling. The package already contains Listen 443 (in ports.conf), so do not add a second one.
sudo a2enmod ssl headers
sudo apache2ctl configtest
sudo systemctl restart apache2
On RHEL, AlmaLinux, Rocky Linux and Fedora, mod_ssl is a separate package. It installs /etc/httpd/conf.d/ssl.conf with Listen 443 https and a default virtual host that uses a self-signed test certificate.
sudo dnf install mod_ssl
sudo apachectl configtest
sudo systemctl restart httpd
Option 1: certbot
Certbot is the ACME client made by the Electronic Frontier Foundation. Its own instructions recommend the snap package; remove any certbot installed from apt or dnf first so that the right one runs.
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --apache
certbot --apache gets a certificate and edits your virtual host for you. If you would rather write the configuration yourself, run sudo certbot certonly --apache; the files then land in /etc/letsencrypt/live/example.com/ as fullchain.pem (certificate plus intermediate) and privkey.pem. The package installs a systemd timer or cron job for renewals. Test it:
sudo certbot renew --dry-run
Certbot 4.1.0 (June 2025) and later ask Let’s Encrypt when to renew through ACME Renewal Information (ARI). For Let’s Encrypt certificates that usually means renewing at about two thirds of the lifetime, and it also lets the CA trigger an early renewal if a certificate has to be revoked. As of November 2026 the current certbot release is 5.9.0.
Option 2: mod_md
mod_md (in httpd since 2.4.30) is an ACME client built into Apache. You list the domain names, and Apache fetches and renews the certificate itself; there is no separate tool or cron job. The module is still marked “Experimental” in the documentation, but it is widely used. It needs mod_watchdog, which Debian compiles into httpd and Fedora’s packages load by default.
# Debian/Ubuntu
sudo a2enmod md ssl
# RHEL/Fedora
sudo dnf install mod_md
A minimal configuration, for example in /etc/apache2/sites-available/example.com.conf or /etc/httpd/conf.d/example.com.conf:
MDCertificateAgreement accepted
MDContactEmail admin@example.com
MDomain example.com www.example.com
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com
SSLEngine on
# no SSLCertificateFile lines: mod_md supplies the certificate
Protocols h2 http/1.1 acme-tls/1
</VirtualHost>
MDCertificateAgreement accepted says you accept the CA’s terms of service. acme-tls/1 in Protocols lets mod_md answer the TLS-ALPN-01 check on port 443. Until the first certificate arrives, requests to that host get a 503 Service Unavailable.
A reload activates the certificate. mod_md fetches the new certificate in the background but only puts it into use at the next restart or graceful reload. Add a daily reload, for example a root cron line 30 4 * * * systemctl reload apache2 (or httpd), or use MDMessageCmd to be told when a certificate was renewed.
Useful mod_md settings, all verified in the httpd 2.4 documentation:
MDRenewWindow 33%is the default: renew when a third of the lifetime is left.MDRenewViaARI onis the default since httpd 2.4.66 (mod_md 2.6.1): renewals can also be triggered by the CA through ARI.MDProfile(2.4.64 and later) picks a Let’s Encrypt profile such astlsserverorshortlived.MDPrivateKeys secp256r1 rsa3072gets an ECDSA and an RSA certificate; modern clients use the smaller EC one.- A
<Location "/md-status">withSetHandler md-statusshows JSON status. Protect it withRequire iporRequire local.
If you redirect HTTP to HTTPS yourself, leave /.well-known/ out of the redirect, or use MDRequireHttps permanent, which handles that for you.
A complete HTTPS virtual host
This example uses the certbot file paths. The redirect keeps /.well-known/acme-challenge/ on plain HTTP, the way the TLSRef configuration generator writes it (it needs mod_rewrite).
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
RewriteEngine On
RewriteCond %{REQUEST_URI} !^/.well-known/acme-challenge/
RewriteRule ^ https://example.com%{REQUEST_URI} [R=301,L]
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Protocols h2 http/1.1
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
</VirtualHost>
Since httpd 2.4.8, SSLCertificateFile can hold the certificate and the intermediate certificates together, so the old SSLCertificateChainFile directive is deprecated. Point it at fullchain.pem, not cert.pem, or some clients will fail with chain errors.
Recommended TLS settings
The Mozilla SSL Configuration Generator (ssl-config.mozilla.org) now redirects to the TLSRef Configurator at configurator.tlsref.org, which carries on the same guidelines. Its “Intermediate” profile (guideline 6.0, as of November 2026) is the right choice for almost every public site. For Apache it produces these server-wide settings, which you can put in /etc/apache2/conf-available/tls.conf (then sudo a2enconf tls) or a file in /etc/httpd/conf.d/:
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLOpenSSLConfCmd Curves X25519MLKEM768:X25519:prime256v1:secp384r1
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_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
SSLHonorCipherOrder off
SSLSessionTickets off
Check your OpenSSL version first. X25519MLKEM768 is a hybrid post-quantum key exchange that needs OpenSSL 3.5 or later; with an older OpenSSL, httpd refuses to start with that Curves line. Run openssl version. On older systems use Curves X25519:prime256v1:secp384r1. The generator drops the hybrid group automatically when you enter an older OpenSSL version.
Notes: TLS 1.0 and 1.1 are off. The client picks the cipher, because every listed suite is strong. Session tickets are off because, as the mod_ssl documentation says, keeping them on without restarting the server regularly weakens forward secrecy. On RHEL-family systems the packaged ssl.conf uses SSLCipherSuite PROFILE=SYSTEM, which follows the system-wide crypto policy. Keeping that policy is also a valid choice.
HSTS
The Strict-Transport-Security header tells browsers to use only HTTPS for your domain for max-age seconds; 63,072,000 seconds is two years (the generator’s value). Browsers keep that promise even if you later turn HTTPS off, so start with max-age=300 (five minutes), check every subdomain works over HTTPS, then raise it. Add includeSubDomains only if all subdomains have HTTPS. Header always makes sure the header goes out on redirects and error pages too.
Let’s Encrypt changes 2025–2028
Every date below comes from the Let’s Encrypt blog and documentation (checked November 2026).
| Date | Change | What to do |
|---|---|---|
| June 4, 2025 | Expiration notification emails ended; stored email addresses were deleted. | Set up your own certificate monitoring. |
| May 7, 2025 | OCSP URLs removed from new certificates; requests with OCSP Must-Staple fail. | Remove MDMustStaple on or --must-staple. |
| Aug 6, 2025 | OCSP service turned off. Revocation is published only through CRLs. | Nothing to staple for Let’s Encrypt certificates (see below). |
| Feb 11, 2026 | Default certificates no longer carry the TLS Client Authentication EKU; the tlsclient profile ended July 8, 2026. | Only matters if you used these certificates as client certificates, for example for mutual TLS to a backend. |
| May 13, 2026 | The opt-in tlsserver profile issues 45-day certificates; 6-day shortlived certificates have been available to everyone since January 15, 2026. | Optional; only if your renewals are fully automated. |
| Feb 10, 2027 | Default (classic) certificates drop from 90 to 64 days; authorization reuse falls from 30 to 10 days. Staging switched on October 14, 2026. | Renew at about two thirds of the lifetime, or use ARI. |
| Feb 16, 2028 | Default certificates drop to 45 days, authorization reuse to 7 hours. | Same: no hard-coded renewal dates. |
The arithmetic shows why hard-coded schedules break. Two thirds of 90 days is 60 days, so “renew every 60 days” used to work. With 64-day certificates, two thirds is about 43 days; with 45-day certificates it is 30 days. Let’s Encrypt suggests searching cron jobs and scripts for numbers like 60, 80 or 83. Certbot 4.1+ and mod_md with its default 33% window already handle this, and rate limits do not change, because renewals are exempt.
OCSP stapling today
OCSP stapling (SSLUseStapling On in mod_ssl, or MDStapling on in mod_md) makes the server send a signed “this certificate is still valid” answer from the CA during the handshake. Current Let’s Encrypt certificates have no OCSP address, so there is nothing to fetch. Leave stapling off (the default in both modules) for them. If you also use certificates from a CA that still runs OCSP, stapling still works; the httpd SSL How-To lists recommended production values such as SSLStaplingResponderTimeout 4 and SSLStaplingReturnResponderErrors off. To see whether a certificate has an OCSP address:
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -ocsp_uri
No output means no OCSP address.
Test your setup
After every change, test the configuration and then reload:
sudo apachectl configtest # Debian: sudo apache2ctl -t
sudo systemctl reload apache2 # RHEL/Fedora: httpd
Check which certificate is served and when it expires:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -issuer -dates
The output shows the issuer (it should name Let’s Encrypt) and the notBefore and notAfter dates. Check the redirect and HSTS header with curl -sI http://example.com/ (expect 301 and a Location: https://…) and curl -sI https://example.com/ | grep -i strict. For a quick outside check of the certificate installation (names, expiry date and the intermediate chain), use an online SSL checker; for a full report, the Qualys SSL Labs Server Test grades protocols, ciphers and the chain.
Common mistakes
- Using
cert.peminstead offullchain.pem. Browsers may cope, but other clients report “unable to get local issuer certificate”. - Redirecting
/.well-known/acme-challenge/somewhere that does not serve it. Renewals then fail silently until the certificate expires. - Never reloading Apache. Apache reads certificates only at start or reload. If you used
certbot certonly, add a deploy hook such as--deploy-hook "systemctl reload apache2"; with mod_md, schedule a reload. - No monitoring. Let’s Encrypt no longer emails you before expiry. A check that alerts when
notAfteris less than about 10 days away catches broken renewals. - Copying old cipher lists. Configurations with
RC4,3DESorSSLProtocol allfrom old tutorials are unsafe.
Next steps: turn on HTTP/2, set up HTTP to HTTPS and www redirects, and work through the hardening checklist.
Sources
- SSL/TLS Strong Encryption: How-To, Apache httpd 2.4 documentation
- mod_ssl, Apache httpd 2.4 documentation
- mod_md, Apache httpd 2.4 documentation
- CHANGES_2.4 (mod_md 2.5.1 and 2.6.1 entries), Apache HTTP Server Project
- Certbot instructions: Apache on Linux (snap), EFF
- Certbot change log (ARI support in 4.1.0), Certbot project
- Ending OCSP Support in 2025, Let’s Encrypt
- OCSP Service Has Reached End of Life, Let’s Encrypt
- Expiration Notification Service Has Ended, Let’s Encrypt
- Ending TLS Client Authentication Certificate Support in 2026, Let’s Encrypt
- Decreasing Certificate Lifetimes to 45 Days, Let’s Encrypt
- Shorter Certificate Lifetimes and Rate Limits, Let’s Encrypt
- 64-Day Certificate Lifetimes Coming Feb 2027, Let’s Encrypt
- 6-day and IP Address Certificates are Generally Available, Let’s Encrypt
- Profiles, Let’s Encrypt documentation
- Challenge Types, Let’s Encrypt documentation
- Mozilla SSL Configuration Generator (now redirects to TLSRef)
- TLSRef TLS Configurator
- Server-Side TLS recommendations, TLSRef
- TLSRef guideline 6.0 (JSON)
- ports.conf in the Debian apache2 package, Debian Sources
- ssl.conf in the Fedora httpd package, Fedora Package Sources
- SSL Server Test, Qualys SSL Labs