TLS certificates for Apache and nginx on RHEL, Fedora and Debian

Updated November 2026

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

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

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