How To

Setup Let’s Encrypt SSL Certificates with Certbot on Linux

Every web service you expose to the internet needs a valid SSL/TLS certificate. Let’s Encrypt makes this free and automated through the ACME protocol, and Certbot is the official client that handles certificate issuance, installation, and renewal. Whether you need a single-domain cert, a wildcard covering all subdomains, or a certificate for a server behind a firewall with no public IP, this guide covers all of it.

Original content from computingforgeeks.com - post 1344

We cover five different methods for obtaining Let’s Encrypt certificates with Certbot: standalone, webroot, the Nginx plugin, the Apache plugin, and DNS-01 validation with Cloudflare (required for wildcard certs and private networks). Each method has its use case, and picking the right one saves you from unnecessary downtime and configuration headaches.

Last verified: March 2026 | Tested on Rocky Linux 10.1, Ubuntu 24.04 LTS

Prerequisites

  • A server running Rocky Linux 10, AlmaLinux 10, RHEL 10, or Ubuntu 24.04 LTS
  • Root or sudo access
  • A registered domain name with DNS pointing to your server (for HTTP-01 challenges)
  • Port 80/tcp open for HTTP-01 validation, port 443/tcp for HTTPS
  • For DNS-01/wildcard: a Cloudflare account with an API token (Zone:DNS:Edit permission)
  • Tested with: Certbot 4.2.0 (Rocky 10 EPEL), Certbot 2.9.0 (Ubuntu 24.04 apt)

How Let’s Encrypt Validation Works

Before issuing a certificate, Let’s Encrypt must verify that you control the domain. There are two challenge types:

HTTP-01 challenge proves control by placing a token file at http://yourdomain.com/.well-known/acme-challenge/TOKEN. The Let’s Encrypt server fetches this file over port 80. This works for single domains and requires the server to have a public IP with port 80 open.

DNS-01 challenge proves control by creating a TXT record at _acme-challenge.yourdomain.com. The Let’s Encrypt server verifies the record via DNS lookup. This is the only method that supports wildcard certificates (*.yourdomain.com), and it works even when the server has no public IP (behind NAT, on a private network).

Install Certbot on Rocky Linux 10 / AlmaLinux 10

Certbot and its plugins are available from the EPEL repository.

Enable EPEL and install Certbot with the plugins you need:

sudo dnf install -y epel-release
sudo dnf install -y certbot

Install the plugin for your web server and/or DNS provider:

sudo dnf install -y python3-certbot-nginx          # Nginx plugin
sudo dnf install -y python3-certbot-apache         # Apache plugin
sudo dnf install -y python3-certbot-dns-cloudflare # Cloudflare DNS plugin

Verify the installation:

certbot --version

On Rocky Linux 10.1, this shows:

certbot 4.2.0

Install Certbot on Ubuntu 24.04 / Debian

Ubuntu 24.04 ships Certbot in the default repositories.

sudo apt update
sudo apt install -y certbot

Install the plugins you need:

sudo apt install -y python3-certbot-nginx          # Nginx plugin
sudo apt install -y python3-certbot-apache         # Apache plugin
sudo apt install -y python3-certbot-dns-cloudflare # Cloudflare DNS plugin

Verify:

certbot --version

Ubuntu 24.04 ships Certbot 2.9.0:

certbot 2.9.0

Method 1: Standalone (No Web Server)

The standalone method runs Certbot’s own temporary HTTP server on port 80 to handle the challenge. Use this when no web server is installed yet, or when you plan to configure one manually after obtaining the certificate.

If Nginx or Apache is already running on port 80, you must stop it first. Otherwise Certbot will fail with Could not bind TCP port 80 because it is already in use.

sudo systemctl stop nginx   # or: sudo systemctl stop httpd

Request the certificate:

sudo certbot certonly --standalone -d example.com -d www.example.com --agree-tos -m [email protected]

On success, Certbot saves the certificate files:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-06-22.

Start your web server again after obtaining the certificate:

sudo systemctl start nginx   # or: sudo systemctl start httpd

When to use standalone: Initial server setup before the web server is configured, CI/CD environments, or services that don’t use a traditional web server (mail servers, database connections).

Method 2: Webroot (Running Web Server, No Config Changes)

The webroot method places the ACME challenge file in a directory that your already-running web server serves. No downtime, no web server restart, no config modifications.

Your web server must be configured to serve files from /.well-known/acme-challenge/. Most default Nginx and Apache configs already do this from the document root.

sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com --agree-tos -m [email protected]

The -w flag specifies the document root where Certbot writes the challenge file. If your Nginx config uses a different root (like /usr/share/nginx/html), adjust accordingly.

If you have multiple domains with different document roots, specify each pair:

sudo certbot certonly --webroot \
  -w /var/www/example -d example.com \
  -w /var/www/app -d app.example.com \
  --agree-tos -m [email protected]

When to use webroot: Production servers where you cannot tolerate any downtime and prefer to manage your own web server configuration manually.

Method 3: Nginx Plugin (Automatic Configuration)

The Nginx plugin handles everything: obtains the certificate, modifies your Nginx server blocks to add SSL directives, and sets up HTTP to HTTPS redirection. This is the fastest path from zero to HTTPS.

Make sure Nginx is running with a server block for your domain before running the command:

sudo certbot --nginx -d example.com -d www.example.com --agree-tos -m [email protected]

Certbot will output the changes it made:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-06-22.

Deploying certificate
Successfully deployed certificate for example.com to /etc/nginx/conf.d/example.conf
Congratulations! You have successfully enabled HTTPS on https://example.com

Certbot adds the following directives to your Nginx server block automatically:

listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

It also creates a separate server block that redirects all HTTP traffic to HTTPS.

When to use the Nginx plugin: Quick setups where you want Certbot to handle the full SSL configuration. Good for straightforward sites. Avoid it if you have complex Nginx configs with custom SSL settings you want to preserve.

Method 4: Apache Plugin (Automatic Configuration)

The Apache plugin works the same way as the Nginx plugin but for Apache HTTP Server. It obtains the certificate and configures the Apache virtual host for SSL.

On Rocky/RHEL, install mod_ssl alongside the plugin:

sudo dnf install -y mod_ssl python3-certbot-apache

Run Certbot with the Apache plugin:

sudo certbot --apache -d example.com -d www.example.com --agree-tos -m [email protected]

Certbot creates or modifies the SSL virtual host in Apache’s configuration directory and enables the redirect from HTTP to HTTPS.

When to use the Apache plugin: Servers running Apache where you want automatic SSL configuration. Particularly useful on RHEL-based systems where Apache is the default web server.

Method 5: DNS-01 with Cloudflare (Wildcard and Private Networks)

DNS-01 validation creates a temporary TXT record in your DNS zone to prove domain ownership. This is required for wildcard certificates and works even when the server has no public IP (Proxmox VMs, private networks, servers behind NAT). If you manage DNS through Cloudflare, the certbot-dns-cloudflare plugin automates the entire process.

Create a Cloudflare API Token

Go to the Cloudflare API Tokens page and create a token with Zone:DNS:Edit permission for your domain. Use an API Token (scoped), not the Global API Key (full account access).

Configure Credentials

Store the token in a credentials file that only root can read:

sudo mkdir -p /etc/letsencrypt
echo "dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN" | sudo tee /etc/letsencrypt/cloudflare.ini
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

Obtain a Single-Domain Certificate via DNS

This is useful when your server is on a private network with no public IP:

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com \
  --agree-tos -m [email protected]

Certbot creates the TXT record, waits for DNS propagation, validates, and then removes the record. The default wait is 10 seconds. If validation fails because DNS has not propagated, increase the wait:

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 60 \
  -d example.com

Obtain a Wildcard Certificate

A wildcard certificate covers all subdomains (*.example.com). Include the bare domain too if you want the apex covered, because the wildcard only matches subdomains:

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.example.com" -d example.com \
  --agree-tos -m [email protected]

The output confirms both names are covered:

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-06-22.

Verify the wildcard cert covers both entries:

sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -ext subjectAltName

The Subject Alternative Name field should list both:

X509v3 Subject Alternative Name:
    DNS:*.example.com, DNS:example.com

Certificate File Locations

Certbot stores certificate files under /etc/letsencrypt/ in a consistent structure. Here is what each file is used for:

FilePathUsed For
fullchain.pem/etc/letsencrypt/live/domain/fullchain.pemYour certificate + intermediate CA chain. Use this in ssl_certificate (Nginx) or SSLCertificateFile (Apache)
privkey.pem/etc/letsencrypt/live/domain/privkey.pemPrivate key. Use in ssl_certificate_key (Nginx) or SSLCertificateKeyFile (Apache)
cert.pem/etc/letsencrypt/live/domain/cert.pemServer certificate only (no chain). Rarely needed directly
chain.pem/etc/letsencrypt/live/domain/chain.pemIntermediate CA certificate. Used with SSLCertificateChainFile in older Apache versions

These paths are symlinks to the current version in /etc/letsencrypt/archive/. When Certbot renews a certificate, the symlinks are updated automatically, so your web server config never needs to change.

Configure Nginx with Let’s Encrypt SSL

If you used certbot certonly (standalone, webroot, or DNS), you need to configure Nginx manually. Here is a production-ready server block with SSL:

sudo vi /etc/nginx/conf.d/example.conf

Add the following configuration:

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # HSTS (optional, uncomment after confirming SSL works)
    # add_header Strict-Transport-Security "max-age=63072000" always;

    root /var/www/html;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

Test the configuration and reload:

sudo nginx -t
sudo systemctl reload nginx

Configure Apache with Let’s Encrypt SSL

If you used certbot certonly and want to configure Apache manually, create or edit the SSL virtual host. On Rocky/RHEL, install mod_ssl first:

sudo dnf install -y mod_ssl

Create the virtual host file:

sudo vi /etc/httpd/conf.d/example-ssl.conf

Add the following configuration:

<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

    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLHonorCipherOrder off

    <Directory /var/www/html>
        AllowOverride All
    </Directory>
</VirtualHost>

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

Test and restart Apache:

# Rocky / RHEL
sudo httpd -t
sudo systemctl restart httpd

# Ubuntu / Debian
sudo apachectl configtest
sudo systemctl restart apache2

Auto-Renewal and Deploy Hooks

Let’s Encrypt certificates expire after 90 days (with shorter lifetimes planned). Certbot sets up automatic renewal through a systemd timer that runs twice daily. This timer is installed automatically when you install Certbot from your distribution’s package manager.

Verify the Renewal Timer

Check that the timer is active:

sudo systemctl list-timers | grep certbot

On Rocky Linux 10, the timer is named certbot-renew.timer:

- - certbot-renew.timer certbot-renew.service

On Ubuntu 24.04, it is named certbot.timer:

Wed 2026-03-25 11:52:08 UTC 23h left - certbot.timer certbot.service

Test Renewal (Dry Run)

Always test renewal after initial setup to catch configuration issues early:

sudo certbot renew --dry-run

If this succeeds, automatic renewal will work when the certificate approaches expiry. Certbot renews when less than one-third of the certificate lifetime remains (around 60 days for a 90-day cert).

Deploy Hooks (Restart Services on Renewal)

When a certificate is renewed, your web server needs to reload to pick up the new files. Certbot supports three hook types: --pre-hook (runs before renewal), --post-hook (runs after, success or failure), and --deploy-hook (runs only after a successful renewal). Deploy hooks are what you want for service reloads.

Option A: Set the hook when obtaining the certificate. It gets saved to the renewal config and runs on every future renewal automatically:

sudo certbot certonly --standalone -d example.com \
  --deploy-hook "systemctl reload nginx" \
  --agree-tos -m [email protected]

The hook is stored in /etc/letsencrypt/renewal/example.com.conf:

[renewalparams]
renew_hook = systemctl reload nginx
authenticator = standalone
server = https://acme-v02.api.letsencrypt.org/directory
key_type = ecdsa

Option B: Add a hook to an existing certificate by editing its renewal config directly:

sudo vi /etc/letsencrypt/renewal/example.com.conf

Add the renew_hook line under the [renewalparams] section:

renew_hook = systemctl reload nginx

Option C: Drop-in hook scripts that run for all certificates. Create a script in the deploy hooks directory:

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

This script runs after every successful renewal for any certificate on the system. Use this approach when all certificates serve the same web server.

For Apache, replace nginx with the appropriate service name:

# Rocky / RHEL
systemctl reload httpd

# Ubuntu / Debian
systemctl reload apache2

Hook Environment Variables

Deploy hook scripts have access to two environment variables for conditional logic:

  • $RENEWED_LINEAGE contains the path to the renewed cert (e.g., /etc/letsencrypt/live/example.com)
  • $RENEWED_DOMAINS contains the space-separated list of domains on the renewed cert

This lets you write hooks that only restart specific services for specific certificates:

echo '#!/bin/bash
if [[ "$RENEWED_LINEAGE" == */live/mail.example.com ]]; then
    systemctl restart postfix dovecot
else
    systemctl reload nginx
fi' | sudo tee /etc/letsencrypt/renewal-hooks/deploy/smart-reload.sh
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/smart-reload.sh

Certificate Management Commands

These commands help you manage certificates after initial setup.

List all certificates managed by Certbot:

sudo certbot certificates

Sample output showing a certificate with its expiry date and paths:

Found the following certs:
  Certificate Name: example.com
    Serial Number: 5623a614f565d960653ea779b071b9861b2
    Key Type: ECDSA
    Domains: example.com
    Expiry Date: 2026-06-22 10:58:55+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem

View certificate details with OpenSSL:

sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -subject -dates -issuer

This shows the subject, validity period, and issuing CA:

subject=CN=example.com
notBefore=Mar 24 10:58:56 2026 GMT
notAfter=Jun 22 10:58:55 2026 GMT
issuer=C=US, O=Let's Encrypt, CN=E7

Revoke a certificate (if compromised or no longer needed):

sudo certbot revoke --cert-name example.com --delete-after-revoke

Add a new domain to an existing certificate by expanding it:

sudo certbot certonly --expand -d example.com -d www.example.com -d api.example.com

Certbot Cheat Sheet

TaskCommand
Standalone certificatecertbot certonly --standalone -d example.com
Webroot certificatecertbot certonly --webroot -w /var/www/html -d example.com
Nginx auto-configcertbot --nginx -d example.com
Apache auto-configcertbot --apache -d example.com
DNS-01 Cloudflarecertbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d example.com
Wildcard + apexcertbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d "*.example.com" -d example.com
Test renewalcertbot renew --dry-run
Force renewalcertbot renew --force-renewal --cert-name example.com
List certificatescertbot certificates
Revoke + deletecertbot revoke --cert-name example.com --delete-after-revoke
Expand cert (add domain)certbot certonly --expand -d example.com -d new.example.com
View cert detailsopenssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -text
Check expiryopenssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates
Test SSL from outsideopenssl s_client -connect example.com:443 -servername example.com
Use staging (testing)Add --staging to any certbot command

OS-Specific Differences

ItemRocky / AlmaLinux / RHEL 10Ubuntu 24.04
Certbot version in repos4.2.0 (EPEL)2.9.0 (apt)
Install commanddnf install certbotapt install certbot
Requires EPELYes (dnf install epel-release)No
DNS Cloudflare pluginpython3-certbot-dns-cloudflarepython3-certbot-dns-cloudflare
Nginx pluginpython3-certbot-nginxpython3-certbot-nginx
Apache pluginpython3-certbot-apache + mod_sslpython3-certbot-apache
Renewal timer namecertbot-renew.timercertbot.timer
Timer scheduleEvery 12 hours + random delayEvery 12 hours + random delay
Apache service namehttpdapache2
Firewallfirewall-cmdufw

Let’s Encrypt Rate Limits

Let’s Encrypt enforces rate limits to prevent abuse. These are the key limits to be aware of:

LimitValueWindow
Certificates per registered domain507 days
Duplicate certificates (same name set)57 days
New orders per account3003 hours
Failed authorizations per hostname51 hour

Renewals of existing certificates do not count toward the per-domain limit. Never use --force-renewal in a cron job or automated script because repeated force-renewals will quickly exhaust the duplicate certificate limit.

For testing and development, always use the staging environment by adding --staging to your certbot command. The staging server has much higher rate limits and issues test certificates (not trusted by browsers, but functionally identical for testing).

sudo certbot certonly --standalone -d example.com --staging

Open Firewall Ports

Certbot needs port 80 open for HTTP-01 challenges, and your web server needs port 443 for HTTPS traffic.

Firewalld (Rocky / AlmaLinux / RHEL)

sudo firewall-cmd --add-service=http --permanent
sudo firewall-cmd --add-service=https --permanent
sudo firewall-cmd --reload

UFW (Ubuntu / Debian)

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw reload

Troubleshooting Common Issues

Error: “Could not bind TCP port 80 because it is already in use”

This means another service (usually Nginx or Apache) is listening on port 80, and the standalone plugin cannot start its own server. Either stop the web server first (systemctl stop nginx) or switch to the webroot or Nginx/Apache plugin method instead.

Error: “DNS problem: NXDOMAIN looking up A for example.com”

The domain does not resolve to any IP address. Verify your DNS A record is configured and has propagated:

dig +short example.com

If the domain resolves but Certbot still fails, the A record may point to a different server. Let’s Encrypt connects to whatever IP the domain resolves to, not necessarily your server.

Error: “Timeout during connect” for HTTP-01 challenge

Port 80 is blocked by a firewall between Let’s Encrypt and your server. Check your server firewall, cloud provider security groups, and any upstream network firewalls. Let’s Encrypt validates from multiple IP addresses, so you cannot whitelist specific IPs.

Error: “Certbot failed to authenticate some domains (dns-01)” with Cloudflare

The DNS TXT record did not propagate before validation. Increase the propagation delay:

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 120 \
  -d example.com

Also verify that your API token has Zone:DNS:Edit permission for the correct domain. A token scoped to the wrong zone will silently fail to create the TXT record.

Error: “Too many certificates already issued” (rate limit)

You have hit the 50 certificates per registered domain per week limit. Wait 7 days or use the staging environment (--staging) for testing. Renewals do not count toward this limit, so existing certificates will still renew normally.

Renewal fails silently

Check the renewal log for details:

sudo cat /var/log/letsencrypt/letsencrypt.log | tail -50

Common causes: the web server config was changed and port 80 is no longer accessible, the domain no longer points to this server, or the Cloudflare API token expired. Run certbot renew --dry-run to diagnose.

Frequently Asked Questions

How long are Let’s Encrypt certificates valid?

90 days. Certbot automatically renews certificates when less than one-third of the lifetime remains (around 60 days). Shorter 45-day certificates are planned for the near future, making automated renewal even more critical.

Can I get a wildcard certificate with HTTP-01?

No. Wildcard certificates require DNS-01 validation. You must use the --dns-cloudflare plugin (or another DNS plugin) because Let’s Encrypt needs to verify control over the entire zone, which only a DNS TXT record can prove.

Which Certbot method should I use?

Use the Nginx/Apache plugin for the simplest setup (it handles everything). Use webroot for zero-downtime on production servers where you manage configs manually. Use standalone when no web server is running yet. Use DNS-01 for wildcards or when the server has no public IP.

Do I need to open port 80 for HTTPS renewal?

Yes, if you used an HTTP-01 challenge method (standalone, webroot, Nginx/Apache plugin). The renewal uses the same challenge type as the original issuance. If you used DNS-01, port 80 is not needed for renewal.

Wrapping Up

You now have a complete reference for obtaining and managing Let’s Encrypt SSL certificates on Rocky Linux 10 and Ubuntu 24.04 using all five Certbot methods. The DNS-01 method with Cloudflare is particularly powerful for wildcard certificates and servers on private networks.

The most important takeaway: always set up a deploy hook to reload your web server on renewal, and always run certbot renew --dry-run after initial setup. A certificate that can’t renew automatically will expire in 90 days, and an expired certificate on a production site is worse than no certificate at all. For articles showing Let’s Encrypt in action with specific applications, see our guides on installing Grafana with Nginx and SSL or setting up an SVN server with HTTPS.

Related Articles

AlmaLinux Install Observium Monitoring Tool on Rocky 9 / AlmaLinux 9 AlmaLinux Install Android Studio on Rocky Linux 9|AlmaLinux 9|Oracle Linux 9 AlmaLinux Install Grafana Alloy on Rocky Linux 10 / AlmaLinux 10 AlmaLinux Install Docker and Compose on Rocky 8 or AlmaLinux 8

Leave a Comment

Press ESC to close