Securing your website with HTTPS is no longer optional. Search engines penalize unencrypted sites, browsers display warning messages to visitors, and data transmitted without TLS is vulnerable to interception. Let’s Encrypt eliminated the cost barrier by providing free, automated, and open SSL/TLS certificates. Certbot is the recommended client for obtaining and managing those certificates on Linux servers.
This guide walks you through every step — from installation to production-hardened configuration — on Ubuntu 22.04 and 24.04.
Prerequisites
Before you begin, ensure you have the following in place:
- Ubuntu Server 22.04 LTS or 24.04 LTS with root or sudo access.
- A registered domain name (e.g.,
example.com) with DNS A/AAAA records pointing to your server’s public IP address. - Nginx or Apache installed and serving at least one virtual host (server block) for your domain.
- Port 80 open on your firewall. Let’s Encrypt uses HTTP-01 challenges on port 80 by default.
- Port 443 open for HTTPS traffic after the certificate is installed.
Verify your firewall allows the required ports:
sudo ufw status
sudo ufw allow 'Nginx Full' # Or 'Apache Full' for Apache
Confirm your domain resolves to the server:
dig +short A example.com
dig +short AAAA example.com
Both commands should return your server’s IP address.
How Let’s Encrypt Works
Let’s Encrypt is a free, automated, and open Certificate Authority (CA) operated by the Internet Security Research Group (ISRG). It issues Domain Validation (DV) certificates using the ACME protocol (Automatic Certificate Management Environment).
The ACME Protocol
The certificate issuance process follows these steps:
- Account Registration — Certbot creates an account with the Let’s Encrypt ACME server and agrees to the terms of service.
- Domain Authorization — The ACME server sends challenges to prove you control the domain. The two most common challenge types are:
- HTTP-01 — Certbot places a token file at
http://example.com/.well-known/acme-challenge/<TOKEN>. The ACME server fetches that URL to verify ownership. - DNS-01 — You create a
_acme-challenge.example.comTXT record with a specific value. Required for wildcard certificates.
- HTTP-01 — Certbot places a token file at
- Certificate Issuance — Once the challenge is validated, the ACME server signs your certificate and returns it.
- Renewal — Certificates are valid for 90 days. Certbot automates renewal via a systemd timer or cron job.
Why 90-Day Certificates?
The short validity period is an intentional design choice:
- Forces automation, reducing human error in certificate management.
- Limits exposure if a private key is compromised.
- Encourages modern security practices across the ecosystem.
Installing Certbot
On Ubuntu 22.04 and 24.04, Certbot is available directly from the default repositories. The snap package is the officially recommended installation method.
Method 1: Snap (Recommended)
# Remove any older OS-packaged certbot to avoid conflicts
sudo apt remove certbot -y
# Install Certbot via snap
sudo snap install --classic certbot
# Create a symlink so certbot is in your PATH
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Method 2: APT (Alternative)
If you prefer the system package manager:
sudo apt update
sudo apt install certbot -y
For web server integration, install the corresponding plugin:
# For Nginx
sudo apt install python3-certbot-nginx -y
# For Apache
sudo apt install python3-certbot-apache -y
Verify the installation:
certbot --version
Obtaining Certificates
Certbot supports multiple methods for obtaining certificates. Choose the one that matches your environment.
Option 1: Nginx Plugin
The Nginx plugin automatically modifies your Nginx configuration to serve the certificate and redirect HTTP to HTTPS.
sudo certbot --nginx -d example.com -d www.example.com
Certbot will:
- Verify your Nginx configuration and locate the matching server block.
- Perform the HTTP-01 challenge.
- Download the certificate and private key.
- Update the server block with
ssl_certificateandssl_certificate_keydirectives. - Optionally configure an HTTP-to-HTTPS redirect.
You will be prompted to enter an email address for renewal notifications and to agree to the terms of service.
Option 2: Apache Plugin
The Apache plugin works the same way for Apache virtual hosts:
sudo certbot --apache -d example.com -d www.example.com
Certbot locates the matching <VirtualHost> block, configures SSL, and enables the ssl module if it is not already active.
Option 3: Standalone Mode
If no web server is running (or you want to use Certbot independently), the standalone mode spins up a temporary server on port 80:
sudo certbot certonly --standalone -d example.com -d www.example.com
Important: Stop any service listening on port 80 before running this command. Certbot needs exclusive access to the port.
This mode only obtains the certificate — you must configure your web server manually afterward.
Option 4: Webroot Mode
If your web server is already running and you do not want to stop it, use webroot mode. Certbot places the challenge files in a directory your web server already serves:
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
Ensure your web server is configured to serve files from the specified webroot. For Nginx:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
}
Non-Interactive Mode
For scripting and automation, use the --non-interactive flag along with --agree-tos and --email:
sudo certbot --nginx --non-interactive --agree-tos \
--email admin@example.com \
-d example.com -d www.example.com
Wildcard Certificates with DNS Challenge
Wildcard certificates cover all subdomains at a single level (e.g., *.example.com). Let’s Encrypt requires the DNS-01 challenge for wildcards because the HTTP-01 challenge cannot validate arbitrary subdomains.
Manual DNS Challenge
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d "*.example.com"
Certbot will display a value and instruct you to create a TXT record:
Please deploy a DNS TXT record under the name:
_acme-challenge.example.com
with the following value:
gfj9Xq...Rg5nTzA
Log in to your DNS provider, create the record, and wait for propagation before pressing Enter.
Verify the TXT record before confirming:
dig +short TXT _acme-challenge.example.com
Automated DNS Challenge with Plugins
Manual DNS challenges do not support automatic renewal. For production, use a DNS plugin that modifies records via your provider’s API.
Cloudflare Example
sudo snap install certbot-dns-cloudflare
Create a credentials file:
sudo mkdir -p /etc/letsencrypt/cloudflare
sudo tee /etc/letsencrypt/cloudflare/credentials.ini > /dev/null <<EOF
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini
Obtain the wildcard certificate:
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com -d "*.example.com"
Other Supported DNS Plugins
| Plugin | Provider | Install Command |
|---|---|---|
certbot-dns-cloudflare | Cloudflare | sudo snap install certbot-dns-cloudflare |
certbot-dns-route53 | AWS Route 53 | sudo snap install certbot-dns-route53 |
certbot-dns-digitalocean | DigitalOcean | sudo snap install certbot-dns-digitalocean |
certbot-dns-google | Google Cloud DNS | sudo snap install certbot-dns-google |
certbot-dns-linode | Linode | sudo snap install certbot-dns-linode |
Each plugin follows the same pattern: install the plugin, create a credentials file with your API token, restrict permissions to 600, and run certbot certonly with the plugin flag.
Auto-Renewal Configuration
Certbot sets up automatic renewal when installed via snap or apt. A systemd timer checks for certificates nearing expiration twice a day.
Verify the Timer
sudo systemctl status certbot.timer
Expected output:
● certbot.timer - Run certbot twice daily
Loaded: loaded (/lib/systemd/system/certbot.timer; enabled)
Active: active (waiting)
Trigger: Mon 2026-02-16 03:22:00 UTC; 11h left
Test Renewal
Always test renewal with a dry run:
sudo certbot renew --dry-run
If the dry run succeeds, your renewal pipeline is working correctly.
Renewal Hooks
Certbot supports hooks that run before, during, or after renewal. Use deploy hooks to reload your web server after a new certificate is installed:
sudo certbot renew --deploy-hook "systemctl reload nginx"
To make the hook permanent, add it to the renewal configuration file:
sudo tee -a /etc/letsencrypt/renewal/example.com.conf > /dev/null <<EOF
[renewalparams]
deploy_hook = systemctl reload nginx
EOF
Or for Apache:
sudo tee -a /etc/letsencrypt/renewal/example.com.conf > /dev/null <<EOF
[renewalparams]
deploy_hook = systemctl reload apache2
EOF
Manual Cron Job (If Needed)
If the systemd timer is not active, set up a cron job as a fallback:
sudo crontab -e
Add the following line:
0 3,15 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"
This runs at 03:00 and 15:00 daily. The --quiet flag suppresses output unless there is an error.
Certificate Management Commands
Certbot provides several commands for managing your certificates.
List All Certificates
sudo certbot certificates
Output includes the certificate name, domains covered, expiry date, and file paths.
Revoke a Certificate
If a private key is compromised or you no longer need a certificate:
sudo certbot revoke --cert-name example.com
Add --delete-after-revoke to also remove the files:
sudo certbot revoke --cert-name example.com --delete-after-revoke
Delete Certificate Files (Without Revoking)
sudo certbot delete --cert-name example.com
Expand an Existing Certificate
To add domains to an existing certificate:
sudo certbot --nginx --expand -d example.com -d www.example.com -d blog.example.com
Update Certificate Email
sudo certbot update_account --email newemail@example.com
Certificate File Locations
After obtaining a certificate, the files are stored at:
| File | Path | Purpose |
|---|---|---|
| Certificate | /etc/letsencrypt/live/example.com/fullchain.pem | Public certificate chain |
| Private Key | /etc/letsencrypt/live/example.com/privkey.pem | Private key (keep secret) |
| Chain Only | /etc/letsencrypt/live/example.com/chain.pem | Intermediate CA chain |
| Certificate Only | /etc/letsencrypt/live/example.com/cert.pem | Domain certificate only |
These are symlinks to the current version in /etc/letsencrypt/archive/. Never reference the archive path directly — always use the live directory.
Nginx SSL Best Practices
After Certbot configures your certificate, harden the Nginx SSL settings for production.
Recommended Server Block
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# Certificate files managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# TLS Protocol Versions — disable TLS 1.0 and 1.1
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher Suites — strong ciphers only
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers on;
# OCSP Stapling — faster TLS handshakes
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
# HSTS — force HTTPS for 1 year, include subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Security Headers
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# DH Parameters for Forward Secrecy
ssl_dhparam /etc/ssl/certs/dhparam.pem;
# SSL Session Settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
root /var/www/example.com/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
# Redirect HTTP to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Generate DH Parameters
The ssl_dhparam directive requires a Diffie-Hellman parameter file. Generate one:
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
This takes a few minutes. A 2048-bit DH group is sufficient for most deployments.
Test Your Configuration
After modifying Nginx, always validate and reload:
sudo nginx -t
sudo systemctl reload nginx
Verify SSL Grade
Test your domain with the Qualys SSL Labs Server Test to confirm an A+ rating. An A+ requires HSTS to be enabled.
Multi-Domain Certificates (SAN)
A Subject Alternative Name (SAN) certificate covers multiple domains and subdomains within a single certificate. This is simpler to manage than individual certificates for each domain.
Obtain a SAN Certificate
sudo certbot --nginx \
-d example.com \
-d www.example.com \
-d api.example.com \
-d docs.example.com
Certbot bundles all listed domains into one certificate. Each domain must resolve to the server and pass the HTTP-01 or DNS-01 challenge.
Combining Wildcard and Specific Domains
You can combine a wildcard with the base domain in a single certificate:
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare/credentials.ini \
-d example.com \
-d "*.example.com"
This covers example.com itself plus any single-level subdomain like www.example.com, api.example.com, and so on.
Rate Limits
Let’s Encrypt enforces rate limits to prevent abuse:
| Limit | Value | Window |
|---|---|---|
| Certificates per Registered Domain | 50 | 7 days |
| Duplicate Certificates | 5 | 7 days |
| Failed Validations | 5 | 1 hour |
| New Registrations | 10 | 3 hours |
| Pending Authorizations | 300 | 7 days |
Use the staging environment for testing to avoid hitting production rate limits:
sudo certbot --nginx --staging -d example.com
Once your configuration works, remove the staging certificate and request a production one:
sudo certbot delete --cert-name example.com
sudo certbot --nginx -d example.com
Monitoring Certificate Expiry
Proactive monitoring prevents unexpected certificate expirations that result in browser warnings and service outages.
Check Expiry from the Command Line
sudo certbot certificates
Or check a specific domain using OpenSSL:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates
Output:
notBefore=Feb 14 00:00:00 2026 GMT
notAfter=May 15 00:00:00 2026 GMT
Script-Based Monitoring
Create a simple monitoring script that alerts you if a certificate expires within 14 days:
#!/usr/bin/env bash
# check-cert-expiry.sh
DOMAIN="example.com"
DAYS_THRESHOLD=14
EXPIRY_DATE=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -lt "$DAYS_THRESHOLD" ]; then
echo "WARNING: Certificate for $DOMAIN expires in $DAYS_LEFT days ($EXPIRY_DATE)"
# Add email or webhook notification here
exit 1
fi
echo "OK: Certificate for $DOMAIN expires in $DAYS_LEFT days"
exit 0
Make it executable and schedule it with cron:
chmod +x /usr/local/bin/check-cert-expiry.sh
0 9 * * * /usr/local/bin/check-cert-expiry.sh >> /var/log/cert-monitor.log 2>&1
External Monitoring Services
For critical production environments, consider external certificate monitoring services that check from outside your network and alert via email, Slack, or PagerDuty if a certificate is nearing expiry or has configuration issues.
Troubleshooting Common Errors
Challenge Failed — Connection Refused or Timeout
Symptom: Certbot reports that the ACME server could not reach your domain on port 80.
Causes and Fixes:
-
Firewall blocking port 80:
sudo ufw allow 80/tcp sudo ufw reload -
DNS not pointing to the correct server:
dig +short A example.comThe output must match your server’s public IP.
-
Web server not running:
sudo systemctl status nginx # or apache2 sudo systemctl start nginx -
Reverse proxy or CDN intercepting requests: If you use Cloudflare or another CDN with proxying enabled, temporarily set the DNS record to “DNS only” (grey cloud in Cloudflare) during certificate issuance, or use a DNS challenge instead.
Too Many Certificates Already Issued (Rate Limit)
Symptom: Certbot returns Error creating new order :: too many certificates already issued.
Fixes:
- Wait for the rate limit window to expire (7 days for most limits).
- Use the staging environment for testing: add
--stagingto your Certbot command. - Combine domains into a single SAN certificate instead of requesting separate certificates for each domain.
Check your current certificate issuance history:
# View certificates issued for your domain via crt.sh
curl -s "https://crt.sh/?q=example.com&output=json" | python3 -m json.tool | head -50
DNS Resolution Errors
Symptom: DNS problem: NXDOMAIN looking up A for example.com or similar.
Fixes:
-
Verify the A record exists and has propagated:
dig +trace A example.com -
Check from a public DNS resolver:
dig @8.8.8.8 A example.com -
For DNS-01 challenges, ensure the TXT record is published at
_acme-challenge.example.com:dig +short TXT _acme-challenge.example.com -
Wait 2–5 minutes for DNS propagation before pressing Enter in Certbot.
Renewal Failures
Symptom: certbot renew fails and the systemd timer logs show errors.
Diagnosis:
# Check Certbot logs
sudo tail -100 /var/log/letsencrypt/letsencrypt.log
# Test renewal
sudo certbot renew --dry-run
# Check the renewal configuration
sudo cat /etc/letsencrypt/renewal/example.com.conf
Common causes:
- Port 80 is blocked — A firewall rule or another service is occupying port 80. Ensure your web server is running and port 80 is open.
- Web server configuration changed — If you modified the Nginx or Apache config and removed the ACME challenge location block, renewal will fail.
- Server block deleted — Certbot cannot find the server block referenced in the renewal config. Recreate it or update the renewal config.
- Permissions — Ensure the Certbot systemd timer runs as root:
sudo systemctl cat certbot.timer.
Nginx Configuration Errors After Certbot
Symptom: nginx -t fails after Certbot modifies the configuration.
Fix: Inspect the Nginx config for syntax issues:
sudo nginx -t 2>&1
Common issues include duplicate listen 443 ssl directives or conflicting server blocks. Review the affected file, fix the conflict, and reload:
sudo nginx -t && sudo systemctl reload nginx
Certificate Chain Issues
Symptom: Browsers on older devices or specific clients report “certificate not trusted.”
Fix: Ensure you are using fullchain.pem (not cert.pem) in your web server configuration. The fullchain.pem file includes both your certificate and the intermediate CA certificate:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
Verify the chain is complete:
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \
| openssl x509 -noout -issuer -subject
Summary
Let’s Encrypt and Certbot have fundamentally changed how we manage SSL/TLS certificates. What previously required annual fees and manual processes is now free and fully automated. The key steps are:
- Install Certbot via snap (recommended) or apt.
- Obtain certificates using the Nginx/Apache plugin, standalone, or webroot method.
- Harden your TLS configuration with modern protocols, strong ciphers, OCSP stapling, and HSTS.
- Automate renewal with the built-in systemd timer and deploy hooks.
- Monitor expiration with scripts or external services to catch failures before they impact users.
With these practices in place, your server will maintain a strong security posture with minimal ongoing effort.