mod_ssl is Apache 129 's TLS engine, built on the system OpenSSL (3.5.5 here). An HTTPS site is a virtual host on port 443 with SSLEngine on, a certificate chain and its key. Pair it with a port-80 vhost holding only Redirect permanent / https://localhost/ (Canonical HTTPS Block does the same from .htaccess):
<VirtualHost *:443>
ServerName localhost
DocumentRoot /var/www/example
SSLEngine on
SSLCertificateFile /etc/ssl/localhost/localhost.crt
SSLCertificateKeyFile /etc/ssl/localhost/localhost.key
Protocols h2 http/1.1
</VirtualHost>SSLCertificateFile takes the leaf plus intermediates, exactly Certbot 1,690 's fullchain.pem; SSLCertificateChainFile has been obsolete since 2.4.8. Locally, a self-signed certificate will do; browsers match only its subjectAltName, and without CA:FALSE mod_ssl logs AH01906 (a CA certificate) at each start. To avoid browser warnings, mkcert 59,701 (github.com/FiloSottile/mkcert (https://github.com/FiloSottile/mkcert 59,701 ), v1.4.4) signs from a local CA it adds to your trust stores.
sudo mkdir -p /etc/ssl/localhost && cd /etc/ssl/localhost
sudo openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 30 \
-keyout localhost.key -out localhost.crt -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \
-addext "basicConstraints=critical,CA:FALSE"
sudo chmod 600 localhost.key
sudo a2enmod ssl http2 && sudo a2ensite example-ssl
sudo apache2ctl configtest && sudo systemctl restart apache2
curl -vI --cacert localhost.crt https://localhost/ 2>&1 |
grep -E "ALPN|SSL connection|subject:|expire|verified|^< (HTTP|Server)"* ALPN: curl offers h2,http/1.1 * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey * ALPN: server accepted http/1.1 * subject: CN=localhost * expire date: Oct 23 07:06:01 2026 GMT * SSL certificate verified via OpenSSL. < HTTP/1.1 200 OK < Server: Apache/2.4.66 (Ubuntu)
TLS 1.3 used X25519MLKEM768, the hybrid post-quantum key exchange OpenSSL 3.5 prefers. Yet the third line shows HTTP/1.1, though curl 3,008 offered h2 and the vhost allows it; HTTP/2 and HTTP/3 explains. A renewed certificate needs only a graceful systemctl reload apache2 (Renewal and Hooks).