HTTPS with Let's Encrypt¶
HTTPS encrypts traffic between browsers and your server, authenticates your site's identity, and is required for HTTP/2, many browser APIs, and avoiding "Not Secure" warnings. Let's Encrypt issues free, automated, 90-day TLS certificates, and certbot is the standard client. This page covers issuing certificates two ways, automatic renewal, and a solid TLS config.
Tested on
AlmaLinux 9 / RHEL 9 with certbot from EPEL. Debian/Ubuntu notes are inline.
Why HTTPS / TLS¶
- Confidentiality — passwords, cookies, and form data are encrypted in transit.
- Integrity — prevents ISPs/proxies from injecting ads or tampering with pages.
- Authentication — the certificate proves visitors reached your server, not an impostor.
- Required features — HTTP/2, service workers, geolocation, and more only work over HTTPS.
Let's Encrypt validates that you control the domain using the ACME protocol — typically an HTTP-01 challenge, where it fetches a token from http://yourdomain/.well-known/acme-challenge/.... Your domain's DNS must already point to this server and port 80 must be reachable.
Install certbot¶
certbot lives in EPEL on RHEL/AlmaLinux. Install EPEL, then certbot with the plugin for your web server.
Make sure ports 80 and 443 are open before validating:
See Firewalls for firewall details.
Method 1 — the web server plugin (easiest)¶
The --nginx / --apache plugins find your existing virtual host, obtain the certificate, and edit the config for you to enable TLS. Your site must already serve the domain over HTTP (see Virtual Hosts).
certbot will prompt for an email (for expiry notices), ask you to agree to the terms, and offer to set up an HTTP→HTTPS redirect — choose redirect. It then reloads the web server. Done.
Method 2 — the webroot method (no config edits)¶
The webroot method writes the challenge file into your existing document root and leaves your web server config untouched — ideal when you manage TLS config by hand or run a setup the plugins do not understand. The web server must already serve the domain on port 80 from that root.
sudo certbot certonly --webroot \
-w /var/www/example.com/public \
-d example.com -d www.example.com
certonly— obtain the cert but do not touch web server config.--webroot -w <path>— certbot drops the token under<path>/.well-known/acme-challenge/, which Let's Encrypt then fetches over HTTP.
Certificates are written to:
/etc/letsencrypt/live/example.com/
├── fullchain.pem # certificate + intermediate chain (use this for the cert)
└── privkey.pem # private key
You then reference these in your web server's TLS config (snippet below) and reload it yourself.
Make sure /.well-known is reachable
If you block dotfiles in nginx, exempt the ACME path — note the (?!well-known) in the deny rule shown on the Nginx page. Otherwise the challenge fetch 404s and issuance fails.
Redirect HTTP → HTTPS¶
The plugins offer to do this automatically. If configuring by hand:
A solid TLS server config (nginx)¶
Use this in your port-443 server block. It enables only TLS 1.2/1.3, modern ciphers, and HSTS:
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on; # nginx >= 1.25.1 syntax
server_name example.com www.example.com;
root /var/www/example.com/public;
index index.html index.php;
# Certificate and key issued above
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern, secure protocols only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # let the client pick (best practice for TLS 1.3)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
# Session caching for performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Tell browsers to always use HTTPS for the next 2 years
add_header Strict-Transport-Security "max-age=63072000" always;
location / {
try_files $uri $uri/ =404;
}
}
For Apache, the same ideas go in your SSL vhost: SSLEngine on, SSLCertificateFile fullchain.pem, SSLCertificateKeyFile privkey.pem, SSLProtocol -all +TLSv1.2 +TLSv1.3, and a Header always set Strict-Transport-Security .... See Apache.
Grade your config
After going live, test at the SSL Labs server test or run nmap --script ssl-enum-ciphers -p 443 example.com to confirm only TLS 1.2/1.3 are offered.
Automatic renewal¶
Let's Encrypt certs last 90 days. The certbot package installs a systemd timer that renews them automatically — no cron needed. Verify it is active:
# Confirm the renewal timer is enabled and scheduled
systemctl list-timers | grep certbot
sudo systemctl status certbot-renew.timer
If it is not enabled:
The timer runs certbot renew twice daily; certbot only actually renews certs within 30 days of expiry. Test the whole flow without making real changes:
A successful dry run prints Congratulations, all simulated renewals succeeded.
Deploy hook: reload the web server after renewal¶
When certbot renews via the --nginx/--apache plugin it reloads for you. With the webroot method you must reload the web server yourself so it picks up the new cert. Add a deploy hook that runs only when a cert actually renews:
# Runs after any successful renewal
echo '#!/bin/bash
systemctl reload nginx' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Anything executable in /etc/letsencrypt/renewal-hooks/deploy/ runs after each successful renewal. (Use systemctl reload httpd for Apache.)
Behind a CDN or Cloudflare¶
Issuing certs through a proxy/CDN
If your domain's DNS points at Cloudflare or another CDN rather than directly at your origin, the HTTP-01 challenge gets complicated, because Let's Encrypt's fetch of /.well-known/acme-challenge/ hits the CDN, not your origin. You have three good options:
- Keep the path reaching the origin — ensure the CDN passes
/.well-known/acme-challenge/*through to your origin un-cached and un-redirected (Cloudflare allows the ACME path through by default, but a "Always Use HTTPS" or cache rule can break it). - DNS-01 challenge — prove control by creating a TXT record instead of serving a file. This works regardless of CDN/proxying and is the only method for wildcard certs (
*.example.com). Use a DNS plugin, e.g.certbot certonly --dns-cloudflare -d example.com -d '*.example.com'with API credentials. - Origin certificate — let the CDN terminate TLS at the edge and install the CDN's long-lived origin certificate (e.g. Cloudflare Origin CA) on your server for the edge↔origin hop. No Let's Encrypt needed on the origin in this model.
Verify your work¶
# 1. Certificate files exist
sudo ls -l /etc/letsencrypt/live/example.com/
# 2. Inspect the live cert and its expiry
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -issuer -dates
# 3. HTTP redirects to HTTPS
curl -sI http://example.com/ | grep -i location # -> Location: https://example.com/
# 4. Renewal is wired up and works
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
# 5. Only modern TLS is offered
nmap --script ssl-enum-ciphers -p 443 example.com | grep -E 'TLSv1\.[0-3]'
Summary¶
- HTTPS gives confidentiality, integrity, authentication, and unlocks HTTP/2 and modern browser features.
- Install certbot from EPEL:
dnf install epel-releasethencertbot python3-certbot-nginx(or-apache). - Easiest issuance:
certbot --nginx/--apacheedits your config and sets up the redirect. Hands-off config:certbot certonly --webroot -w /var/www/site -d example.com. - Renewal is automatic via
certbot-renew.timer; test withcertbot renew --dry-run, and add arenewal-hooks/deploy/script to reload the web server (needed for the webroot method). - Redirect HTTP→HTTPS and serve only TLS 1.2/1.3 with modern ciphers and HSTS.
- Behind a CDN/Cloudflare, ensure
/.well-known/acme-challenge/reaches the origin, or use a DNS-01 challenge / the CDN's origin certificate.
Related: Nginx, Apache, Virtual Hosts.