TLS certificates for Apache and nginx on RHEL, Fedora and Debian
A public web server needs a certificate from a trusted certificate authority. The free and normal way to get one is Let's Encrypt with the certbot client, which proves you control the domain, installs the certificate in Apache or nginx and renews it automatically. This page shows that on RHEL-family systems, Fedora, Debian and Ubuntu, then the manual server configuration, test certificates with openssl, and how to check the result.
"SSL" is the old name. Every current server speaks TLS 1.2 and 1.3, and SSL 3.0, TLS 1.0 and TLS 1.1 should stay off. The files and directives still say "ssl" (mod_ssl, ssl_certificate), which is why both names are used.
What you need first
- A domain name whose DNS A and/or AAAA record points at the server. Check with
dig +short www.example.com. - Port 80 reachable from the Internet for the usual HTTP-01 check, and port 443 for HTTPS. On RHEL and Fedora:
sudo firewall-cmd --permanent --add-service=http --add-service=https && sudo firewall-cmd --reload. - Apache or nginx already serving the site over plain HTTP.
If the server cannot be reached from the Internet (an intranet host), use the DNS-01 challenge with a certbot DNS plugin for your DNS provider, or an internal certificate authority.
Install certbot
RHEL, AlmaLinux and Rocky Linux 9 and 10
certbot is in EPEL, the extra packages repository maintained by Fedora volunteers. EPEL needs the CodeReady Builder (CRB) repository:
# RHEL 10 (on RHEL 9: rhel-9 in the repo name, latest-9 in the URL)
sudo subscription-manager repos --enable codeready-builder-for-rhel-10-$(arch)-rpms
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
# AlmaLinux / Rocky Linux (use "latest-9" on version 9)
sudo dnf config-manager --set-enabled crb
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm
# then, on all of them
sudo dnf install certbot python3-certbot-apache # or python3-certbot-nginx
sudo systemctl enable --now certbot-renew.timer
Fedora
sudo dnf install certbot python3-certbot-apache # or python3-certbot-nginx
sudo systemctl enable --now certbot-renew.timer
On Fedora and EPEL the renewal timer is not started by the package. If you skip that last command, your certificate expires.
Debian and Ubuntu
sudo apt install certbot python3-certbot-apache # or python3-certbot-nginx
systemctl list-timers certbot.timer
Debian's package enables certbot.timer for you.
Snap (any distribution, newest version)
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
The certbot project recommends the snap because distribution packages fall behind. As of November 2026 upstream certbot is 5.9.0 (released at the end of October 2026); Fedora 44 and 45 have 5.8.0, Fedora 43 has 5.7.0, EPEL 10 has 4.2.0, Debian 13 has 4.0.0 and EPEL 9 has 3.1.0. Versions before 4.1.0 do not support ACME Renewal Information (ARI), which lets the CA ask for early renewal. They still renew on schedule.
Get the certificate
With the Apache or nginx plugin, certbot finds the matching virtual host, gets the certificate, adds the TLS settings and reloads the server:
sudo certbot --apache -d example.com -d www.example.com
sudo certbot --nginx -d example.com -d www.example.com
If you prefer to edit the server configuration yourself, get the certificate only. The webroot method writes the challenge file into your document root, so the web server keeps running:
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
The files land in /etc/letsencrypt/live/example.com/:
fullchain.pem- your certificate plus the intermediate certificate. Servers need this one.
privkey.pem- the private key. Readable by root only. Never copy it to another machine you do not control.
Since certbot 2.0, new keys are ECDSA P-256 by default. Use --key-type rsa only if you must support very old clients. sudo certbot certificates lists every certificate, its domains, key type and expiry date.
Configure Apache (mod_ssl) by hand
On RHEL and Fedora, install the module with sudo dnf install mod_ssl. It adds /etc/httpd/conf.d/ssl.conf with a default HTTPS host. On Debian and Ubuntu, run sudo a2enmod ssl. Then add a virtual host (RHEL: /etc/httpd/conf.d/example.conf; Debian: /etc/apache2/sites-available/example-ssl.conf followed by sudo a2ensite example-ssl):
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/html
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
</VirtualHost>
Since Apache 2.4.8, SSLCertificateFile can hold the intermediate certificates too, so the old SSLCertificateChainFile directive is not needed. On RHEL, leave SSLProtocol and SSLCipherSuite out: the system-wide crypto policy already limits Apache to TLS 1.2 and 1.3 with current ciphers. Check and reload:
sudo apachectl configtest # "Syntax OK"
sudo systemctl reload httpd # Debian/Ubuntu: apache2
Configure nginx by hand
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name example.com www.example.com;
root /var/www/html;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_session_cache shared:SSL:10m;
}
The http2 on; directive exists since nginx 1.25.1; older guides put http2 on the listen line, a parameter that is now deprecated. nginx already defaults to ssl_protocols TLSv1.2 TLSv1.3. Test and reload with sudo nginx -t && sudo systemctl reload nginx.
Renewal and shorter lifetimes
Test renewal once, right after setup:
sudo certbot renew --dry-run
Since certbot 4.0.0, a certificate is renewed when less than one third of its lifetime is left. If you used certonly, the server must reload to pick up the new file. Add a deploy hook, which runs only after a successful renewal:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx <<'EOF'
#!/bin/sh
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx
Let's Encrypt certificates are getting shorter, as all public CAs must do under the CA/Browser Forum rules. As of November 2026 the default certificates last 90 days. From February 10, 2027 they last 64 days, and from February 16, 2028, 45 days. Automated renewal handles this. Fixed schedules such as "renew every 60 days" will fail. Let's Encrypt also stopped sending expiry warning emails on June 4, 2025, so set up your own monitoring (see the -checkend command below).
Self-signed certificates for testing
For a lab machine or a service only you use, a self-signed certificate is enough. Browsers will warn about it because nobody vouches for it. This creates an ECDSA key and a certificate valid for 30 days, with the host name in the Subject Alternative Name, which browsers require:
sudo openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc \
-days 30 -subj "/CN=test.lan" \
-addext "subjectAltName=DNS:test.lan,IP:192.168.1.10" \
-keyout /etc/pki/tls/private/test.lan.key \
-out /etc/pki/tls/certs/test.lan.crt
sudo chmod 600 /etc/pki/tls/private/test.lan.key
-noenc leaves the key without a passphrase so the server can start unattended. It replaces -nodes, which is deprecated since OpenSSL 3.0. On Debian, use /etc/ssl/private/ and /etc/ssl/certs/ instead of the /etc/pki/tls/ paths. If you need certificates for many internal hosts, run a small internal CA and install its root certificate on your clients, rather than clicking through warnings.
Check a certificate
# What does the server send? (-servername sets SNI)
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
# Dates and names in a certificate file
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -subject -dates -ext subjectAltName
# Same, straight from the server
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | \
openssl x509 -noout -dates
# Exit code 1 if it expires within 14 days (14*24*3600 = 1209600 s) - good for a cron check
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -checkend 1209600
With -brief, look for "Verification: OK", the protocol (TLSv1.3) and the cipher. Add -showcerts without -brief to see every certificate the server sends. If you only see one, the intermediate is missing: point the server at fullchain.pem, not cert.pem. To check the same things from outside your network, an online SSL checker shows the names, expiry date and chain the server actually sends.
Common mistakes
- Using
cert.peminstead offullchain.pem. Desktop browsers may cope; phones, curl and other servers fail with "unable to get local issuer certificate". - Renewal timer not enabled on Fedora and EPEL. Check with
systemctl list-timers | grep certbot. - Port 80 closed. HTTP-01 validation needs it, even if your site redirects everything to HTTPS.
- SELinux labels on RHEL. If you create key files in your home directory and
mvthem into place, they keep the wrong label and httpd cannot read them. Runsudo restorecon -Rv /etc/pki/tls. - Copying old cipher lists from 2015-era guides. They often enable weak ciphers or break TLS 1.3. On RHEL use the crypto policy; elsewhere use the server defaults or a current generator.
- Key readable by everyone. Private keys must be mode 600, owned by root.
The classic 2001 version of this HOWTO, about building Apache with mod_ssl on Red Hat Linux, is kept by the LDP at tldp.org.
Sources
- Certbot instructions (snap), EFF
- Get Certbot and User Guide (renewal, hooks, key types), Certbot documentation
- Certbot change log (5.9.0, ARI in 4.1.0, renewal at 1/3 lifetime in 4.0.0)
- certbot.spec (certbot-renew.timer not started by default), Fedora package sources
- certbot release history, PyPI
- certbot package versions, Fedora Packages
- python-certbot 4.0.0 packaging (certbot.timer), Debian sources
- Getting started with EPEL, EPEL documentation source
- Decreasing certificate lifetimes to 45 days, Let's Encrypt
- Ending support for expiration notification emails, Let's Encrypt
- Challenge types, Let's Encrypt
- mod_ssl, Apache HTTP Server 2.4 documentation
- ngx_http_ssl_module and ngx_http_v2_module and ngx_http_core_module (listen), nginx documentation
- Deploying web servers and reverse proxies, Red Hat Enterprise Linux 10 documentation
- openssl-req(1), openssl-x509(1) and openssl-s_client(1), OpenSSL 3.5 documentation