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.
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:
| File | Path | Used For |
|---|---|---|
fullchain.pem | /etc/letsencrypt/live/domain/fullchain.pem | Your certificate + intermediate CA chain. Use this in ssl_certificate (Nginx) or SSLCertificateFile (Apache) |
privkey.pem | /etc/letsencrypt/live/domain/privkey.pem | Private key. Use in ssl_certificate_key (Nginx) or SSLCertificateKeyFile (Apache) |
cert.pem | /etc/letsencrypt/live/domain/cert.pem | Server certificate only (no chain). Rarely needed directly |
chain.pem | /etc/letsencrypt/live/domain/chain.pem | Intermediate 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_LINEAGEcontains the path to the renewed cert (e.g.,/etc/letsencrypt/live/example.com)$RENEWED_DOMAINScontains 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
| Task | Command |
|---|---|
| Standalone certificate | certbot certonly --standalone -d example.com |
| Webroot certificate | certbot certonly --webroot -w /var/www/html -d example.com |
| Nginx auto-config | certbot --nginx -d example.com |
| Apache auto-config | certbot --apache -d example.com |
| DNS-01 Cloudflare | certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d example.com |
| Wildcard + apex | certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d "*.example.com" -d example.com |
| Test renewal | certbot renew --dry-run |
| Force renewal | certbot renew --force-renewal --cert-name example.com |
| List certificates | certbot certificates |
| Revoke + delete | certbot revoke --cert-name example.com --delete-after-revoke |
| Expand cert (add domain) | certbot certonly --expand -d example.com -d new.example.com |
| View cert details | openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -text |
| Check expiry | openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates |
| Test SSL from outside | openssl s_client -connect example.com:443 -servername example.com |
| Use staging (testing) | Add --staging to any certbot command |
OS-Specific Differences
| Item | Rocky / AlmaLinux / RHEL 10 | Ubuntu 24.04 |
|---|---|---|
| Certbot version in repos | 4.2.0 (EPEL) | 2.9.0 (apt) |
| Install command | dnf install certbot | apt install certbot |
| Requires EPEL | Yes (dnf install epel-release) | No |
| DNS Cloudflare plugin | python3-certbot-dns-cloudflare | python3-certbot-dns-cloudflare |
| Nginx plugin | python3-certbot-nginx | python3-certbot-nginx |
| Apache plugin | python3-certbot-apache + mod_ssl | python3-certbot-apache |
| Renewal timer name | certbot-renew.timer | certbot.timer |
| Timer schedule | Every 12 hours + random delay | Every 12 hours + random delay |
| Apache service name | httpd | apache2 |
| Firewall | firewall-cmd | ufw |
Let’s Encrypt Rate Limits
Let’s Encrypt enforces rate limits to prevent abuse. These are the key limits to be aware of:
| Limit | Value | Window |
|---|---|---|
| Certificates per registered domain | 50 | 7 days |
| Duplicate certificates (same name set) | 5 | 7 days |
| New orders per account | 300 | 3 hours |
| Failed authorizations per hostname | 5 | 1 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.