Let's Encrypt & ACME
Why This Matters
Before Let's Encrypt, getting an HTTPS certificate for your website meant paying a Certificate Authority $50-$300 per year, waiting for manual verification, and remembering to renew before it expired. The result? Huge swaths of the internet ran on unencrypted HTTP. Login pages, e-commerce sites, personal blogs -- all sending data in plain text.
Let's Encrypt changed everything. It is a free, automated, and open Certificate Authority that has issued billions of certificates. Combined with the ACME protocol (Automatic Certificate Management Environment), it allows you to obtain and renew TLS certificates without any human intervention. Today, there is no excuse for running an unencrypted website.
This chapter covers how Let's Encrypt works, how to use certbot to get
certificates, how to set up automatic renewal, and how to handle special cases like
wildcard certificates.
Try This Right Now
If you have a server with a public domain name pointing to it, you can get a certificate in under a minute:
# Install certbot (Debian/Ubuntu)
sudo apt install certbot
# Get a standalone certificate (stop any web server on port 80 first)
sudo certbot certonly --standalone -d yourdomain.com
If you do not have a public server yet, you can still explore certbot:
# Install certbot
sudo apt install certbot # Debian/Ubuntu
sudo dnf install certbot # RHEL/Fedora
# See what certbot can do
certbot --help all
# Check if you already have any certificates
sudo certbot certificates
What Is Let's Encrypt?
Let's Encrypt is a non-profit Certificate Authority run by the Internet Security Research Group (ISRG). It provides:
- Free domain-validated (DV) certificates
- Automated issuance and renewal via the ACME protocol
- Open -- all software and protocols are open source
- Trusted -- Let's Encrypt certificates are trusted by all major browsers and operating systems
Traditional CA Process:
+--------+ +----------+ +------+ +--------+
| Pay $$ | --> | Wait for | --> | Get | --> | Renew |
| | | approval | | cert | | yearly |
+--------+ +----------+ +------+ +--------+
Manual, slow, expensive, easy to forget renewal
Let's Encrypt Process:
+----------+ +----------+ +-------+
| Run | --> | Auto- | --> | Auto- |
| certbot | | verified | | renew |
+----------+ +----------+ +-------+
Automated, free, 90-day certs, auto-renewed
Why 90-Day Certificates?
Let's Encrypt certificates are valid for only 90 days (compared to the traditional 1 year). This seems like a hassle, but it is intentional:
- Encourages automation -- If you must renew every 90 days, you are forced to automate it. Automated renewal is more reliable than human memory.
- Limits damage -- If a key is compromised, the exposure window is shorter.
- Forces freshness -- Certificates and keys are regularly rotated.
The ACME Protocol
ACME (Automatic Certificate Management Environment, defined in RFC 8555) is the protocol that makes automation possible. Here is how it works:
ACME Client (certbot) ACME Server (Let's Encrypt)
| |
| 1. Request certificate for |
| example.com |
| -------------------------------------> |
| |
| 2. Here is a challenge: |
| Prove you control |
| example.com |
| <------------------------------------- |
| |
| 3. Complete the challenge |
| (place a file on web server |
| or create a DNS record) |
| |
| 4. Challenge completed |
| -------------------------------------> |
| |
| 5. Verify the challenge |
| (fetch the file or |
| query the DNS record) |
| |
| 6. Challenge passed! |
| Here is your certificate. |
| <------------------------------------- |
| |
The key insight is that Let's Encrypt never sees your private key. You generate the key locally, create a CSR, and only the CSR and challenge proof are sent to Let's Encrypt.
Challenge Types
Let's Encrypt needs to verify that you control the domain before issuing a certificate. There are two main challenge types.
HTTP-01 Challenge
The most common challenge. Let's Encrypt asks you to place a specific file at a specific URL on your web server.
Let's Encrypt says:
"Place a file with content 'abc123...' at:
http://example.com/.well-known/acme-challenge/TOKEN"
Certbot:
1. Places the file in the web server's document root
2. Tells Let's Encrypt to check
3. Let's Encrypt fetches the URL from their servers
4. If the file content matches, you control the domain
Requirements:
- Port 80 must be open and reachable from the internet
- The domain must point to your server's IP address
- Works for individual domain names only (not wildcards)
DNS-01 Challenge
Let's Encrypt asks you to create a specific DNS TXT record under your domain.
Let's Encrypt says:
"Create a DNS TXT record:
_acme-challenge.example.com TXT 'xyz789...'"
Certbot:
1. Creates the DNS record (manually or via DNS API)
2. Tells Let's Encrypt to check
3. Let's Encrypt queries DNS for the TXT record
4. If the record content matches, you control the domain
Requirements:
- Access to your domain's DNS management
- DNS propagation can take time (seconds to minutes)
- Required for wildcard certificates
- Works even if your server is not publicly accessible
Think About It: Why can't HTTP-01 challenges be used for wildcard certificates? (Hint: think about what
*.example.commeans and how many servers it could point to.)
Installing Certbot
Debian/Ubuntu
# Install certbot and the Nginx plugin
sudo apt update
sudo apt install certbot python3-certbot-nginx
# Or for Apache
sudo apt install certbot python3-certbot-apache
RHEL/Fedora
# Enable EPEL repository (RHEL/CentOS)
sudo dnf install epel-release
# Install certbot and plugins
sudo dnf install certbot python3-certbot-nginx
# Or
sudo dnf install certbot python3-certbot-apache
Using snap (Distribution-Independent)
The certbot team recommends snap for the latest version:
# Install snap if not present
sudo apt install snapd # Debian/Ubuntu
sudo dnf install snapd # RHEL/Fedora
# Install certbot via snap
sudo snap install --classic certbot
# Create a symlink so certbot is in the path
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Obtaining Certificates
Method 1: Standalone Mode
Certbot runs its own temporary web server on port 80 to answer the challenge. Use this when you do not have a web server running (or can stop it temporarily).
# Stop your web server first
sudo systemctl stop nginx # or apache2
# Get the certificate
sudo certbot certonly --standalone -d example.com -d www.example.com
# Start your web server again
sudo systemctl start nginx
Method 2: Webroot Mode
Certbot places challenge files in your existing web server's document root. Use this when your web server is running and you do not want to stop it.
# Your web server must serve files from the webroot
sudo certbot certonly --webroot \
-w /var/www/html \
-d example.com -d www.example.com
For this to work, your web server must serve files from
/var/www/html/.well-known/acme-challenge/. With Nginx, ensure you have:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/html;
}
# Redirect everything else to HTTPS
location / {
return 301 https://$host$request_uri;
}
}
Method 3: Nginx Plugin (Recommended)
The Nginx plugin handles everything -- it reads your Nginx config, gets the certificate, and configures Nginx to use it.
# Get certificate and automatically configure Nginx
sudo certbot --nginx -d example.com -d www.example.com
Certbot will:
- Read your existing Nginx configuration
- Obtain the certificate using webroot via the existing Nginx
- Modify the Nginx config to add SSL directives
- Reload Nginx
Method 4: Apache Plugin
# Get certificate and automatically configure Apache
sudo certbot --apache -d example.com -d www.example.com
What Certbot Creates
After successful certificate issuance:
sudo ls -la /etc/letsencrypt/live/example.com/
cert.pem -> ../../archive/example.com/cert1.pem # Leaf certificate
chain.pem -> ../../archive/example.com/chain1.pem # Intermediate chain
fullchain.pem -> ../../archive/example.com/fullchain1.pem # cert + chain
privkey.pem -> ../../archive/example.com/privkey1.pem # Private key
These are symlinks to the latest versions. When certificates are renewed, the symlinks are updated to point to the new files.
For Nginx, use:
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
For Apache, use:
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
Hands-On: Full Nginx + Let's Encrypt Setup
Here is a complete walkthrough for setting up a new site with HTTPS.
Step 1: Set Up the Basic Nginx Site
# Create a basic Nginx config for your domain
sudo tee /etc/nginx/sites-available/example.com << 'EOF'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
EOF
# Enable the site
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
# Create the document root
sudo mkdir -p /var/www/example.com
echo "<h1>Hello, HTTPS!</h1>" | sudo tee /var/www/example.com/index.html
# Test and reload Nginx
sudo nginx -t && sudo systemctl reload nginx
Step 2: Get the Certificate
# Use the Nginx plugin
sudo certbot --nginx -d example.com -d www.example.com
Certbot will ask a few questions:
- Email address for urgent notices (like expiration warnings)
- Agreement to terms of service
- Whether to redirect HTTP to HTTPS (say yes)
Step 3: Verify
# Test HTTPS
curl -I https://example.com
# Test that HTTP redirects to HTTPS
curl -I http://example.com
# Check the certificate details
echo | openssl s_client -connect example.com:443 -brief
# View certbot's view of the certificate
sudo certbot certificates
Auto-Renewal
Certificates expire every 90 days. Let's Encrypt recommends renewing at 60 days (30 days before expiry). Certbot handles this automatically.
How Auto-Renewal Works
When you install certbot, it creates either a systemd timer or a cron job that runs
certbot renew twice daily. The command checks all certificates and renews any
that are within 30 days of expiry.
# Check if the systemd timer is active
systemctl status certbot.timer
● certbot.timer - Run certbot twice daily
Loaded: loaded (/lib/systemd/system/certbot.timer; enabled)
Active: active (waiting)
Trigger: Fri 2026-02-21 14:23:00 UTC; 6h left
Triggers: ● certbot.service
# Or check for a cron job
cat /etc/cron.d/certbot
# Certbot automatic renewal
0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && \
perl -e 'sleep int(rand(43200))' && certbot -q renew
Testing Renewal
# Dry run -- test renewal without actually renewing
sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert not yet due for renewal
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
The following certificates are not due for renewal yet:
/etc/letsencrypt/live/example.com/fullchain.pem expires on 2026-05-22
No renewals were attempted.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewal Hooks
After a certificate is renewed, you typically need to reload your web server. Certbot supports hooks for this:
# Reload Nginx after successful renewal
sudo certbot renew --deploy-hook "systemctl reload nginx"
Or set it permanently in the renewal configuration:
sudo vi /etc/letsencrypt/renewal/example.com.conf
Add at the bottom under [renewalparams]:
[renewalparams]
# ... existing settings ...
[[ commands ]]
post_hook = systemctl reload nginx
You can also place hook scripts in dedicated directories:
# Scripts in these directories run automatically during renewal
ls /etc/letsencrypt/renewal-hooks/
# deploy/ - runs after successful renewal
# post/ - runs after every renewal attempt
# pre/ - runs before every renewal attempt
# Example: reload Nginx after renewal
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'EOF'
#!/bin/bash
systemctl reload nginx
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
Setting Up a systemd Timer Manually
If the certbot timer is not installed:
sudo tee /etc/systemd/system/certbot-renewal.service << 'EOF'
[Unit]
Description=Certbot Renewal
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
EOF
sudo tee /etc/systemd/system/certbot-renewal.timer << 'EOF'
[Unit]
Description=Run certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renewal.timer
Think About It: Why does certbot add a random delay before renewal? (Hint: what would happen if thousands of servers all tried to renew at exactly midnight?)
Wildcard Certificates
A wildcard certificate covers all subdomains of a domain: *.example.com matches
www.example.com, api.example.com, mail.example.com, etc.
Requirements
- Wildcard certificates require the DNS-01 challenge (HTTP-01 will not work)
- You need API access to your DNS provider, or you must create TXT records manually
Manual DNS Challenge
# Request a wildcard certificate
sudo certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d example.com
Certbot will show:
Please deploy a DNS TXT record under the name:
_acme-challenge.example.com
with the following value:
aB3dEfGhIjKlMnOpQrStUvWxYz1234567890abc
Before continuing, verify the TXT record has been deployed. Depending on the
DNS provider, this may take a few seconds to a few minutes.
Press Enter to Continue
You must log into your DNS provider and create this TXT record, then press Enter.
WARNING: Manual DNS challenges cannot be renewed automatically. You will need to repeat the manual process every 90 days. For production use, use a DNS plugin instead.
Automated DNS Challenge with DNS Plugins
For automated renewal, use a certbot DNS plugin for your DNS provider:
# Cloudflare example
sudo apt install python3-certbot-dns-cloudflare # Debian/Ubuntu
sudo dnf install python3-certbot-dns-cloudflare # RHEL/Fedora
# Create API credentials file
sudo mkdir -p /etc/letsencrypt
sudo tee /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini
# Get a wildcard certificate with automatic DNS verification
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.example.com" -d example.com
Available DNS plugins include:
certbot-dns-cloudflarecertbot-dns-route53(AWS)certbot-dns-google(Google Cloud DNS)certbot-dns-digitaloceancertbot-dns-linodecertbot-dns-ovh
Rate Limits
Let's Encrypt imposes rate limits to prevent abuse. Know these before you start testing in production:
+-----------------------------+------------------+-----------------------+
| Limit | Value | Reset |
+-----------------------------+------------------+-----------------------+
| Certificates per domain | 50 per week | Rolling 7-day window |
| Duplicate certificates | 5 per week | Rolling 7-day window |
| Failed validations | 5 per hour | Rolling 1-hour window |
| New registrations (accounts)| 10 per IP/3 hrs | Rolling 3-hour window |
| Pending authorizations | 300 per account | N/A |
+-----------------------------+------------------+-----------------------+
Using the Staging Environment
For testing, always use the staging environment. It has much higher rate limits and issues certificates signed by a fake CA (not trusted by browsers, but functionally identical).
# Use staging for testing
sudo certbot certonly --standalone \
--staging \
-d test.example.com
# The staging certificate will show issuer: "(STAGING) ..."
After confirming everything works with staging, remove the staging cert and run
again without --staging:
# Delete the staging certificate
sudo certbot delete --cert-name test.example.com
# Get the real certificate
sudo certbot certonly --standalone -d test.example.com
Alternative Client: acme.sh
While certbot is the most popular ACME client, acme.sh is a lightweight pure-bash
alternative. It has no dependencies beyond bash and curl.
Installation
# Install acme.sh (runs as your user, not root)
curl https://get.acme.sh | sh -s email=admin@example.com
# Reload your shell to get the acme.sh alias
source ~/.bashrc
Getting a Certificate
# Webroot mode
acme.sh --issue -d example.com -w /var/www/html
# Standalone mode
acme.sh --issue -d example.com --standalone
# DNS mode (Cloudflare)
export CF_Token="YOUR_CLOUDFLARE_API_TOKEN"
acme.sh --issue -d "*.example.com" -d example.com --dns dns_cf
Installing the Certificate
# Install certificate to the correct location and set up reload hook
acme.sh --install-cert -d example.com \
--key-file /etc/ssl/private/example.com.key \
--fullchain-file /etc/ssl/certs/example.com.fullchain.pem \
--reloadcmd "systemctl reload nginx"
Why Choose acme.sh?
- No root required (runs as regular user)
- No Python dependency (pure bash)
- Supports more DNS providers out of the box (over 100)
- Built-in cron job for renewal
- Lightweight and portable
Distro Note:
acme.shworks identically across all Linux distributions since it only requires bash and curl, which are universally available.
Managing Certificates
List All Certificates
sudo certbot certificates
Found the following certs:
Certificate Name: example.com
Serial Number: 03a4b5c6d7e8f9...
Key Type: RSA
Domains: example.com www.example.com
Expiry Date: 2026-05-22 10:30:00+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem
Revoke a Certificate
# Revoke using the certificate file
sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem
# Revoke and delete all related files
sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem --delete-after-revoke
Delete a Certificate (Without Revoking)
sudo certbot delete --cert-name example.com
Expand a Certificate (Add Domains)
# Add a new domain to an existing certificate
sudo certbot certonly --expand -d example.com -d www.example.com -d new.example.com
Debug This
A certbot renewal starts failing with this error:
Attempting to renew cert (example.com) from /etc/letsencrypt/renewal/example.com.conf
Cert is due for renewal, auto-renewing...
Could not choose appropriate plugin: The manual plugin is not working;
there may be problems with your existing configuration.
The certificate was originally obtained with --manual --preferred-challenges dns.
What is wrong?
Answer: Manual challenges cannot be automated. When certbot tries to auto-renew, it cannot complete the DNS challenge because there is no automated DNS plugin configured. The fix is to switch to an automated method:
# Delete the old cert
sudo certbot delete --cert-name example.com
# Re-obtain with a DNS plugin (e.g., Cloudflare)
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d example.com -d www.example.com
# Or switch to HTTP-01 with the Nginx plugin
sudo certbot --nginx -d example.com -d www.example.com
Now auto-renewal will work because certbot can complete the challenge without human intervention.
What Just Happened?
+------------------------------------------------------------------+
| LET'S ENCRYPT & ACME |
+------------------------------------------------------------------+
| |
| LET'S ENCRYPT: |
| Free, automated, open CA. No more excuses for no HTTPS. |
| 90-day certs by design -- forces automation. |
| |
| ACME PROTOCOL: |
| Client requests cert --> CA issues challenge --> |
| Client proves control --> CA issues certificate |
| |
| CHALLENGES: |
| HTTP-01: Place file at /.well-known/acme-challenge/ (port 80) |
| DNS-01: Create TXT record at _acme-challenge.domain |
| Wildcards require DNS-01. |
| |
| CERTBOT METHODS: |
| --standalone : certbot runs its own web server |
| --webroot : uses existing web server's doc root |
| --nginx : reads and modifies Nginx config |
| --apache : reads and modifies Apache config |
| |
| RENEWAL: |
| certbot renew (via systemd timer or cron) |
| Test with: certbot renew --dry-run |
| Deploy hooks reload the web server after renewal |
| |
| WILDCARDS: |
| certbot certonly --dns-PLUGIN -d "*.example.com" |
| Requires DNS API access for auto-renewal |
| |
| RATE LIMITS: |
| Use --staging for testing! |
| 50 certs/week per registered domain |
| |
+------------------------------------------------------------------+
Try This
Exercise 1: Local Staging Test
Set up a VM with a public IP and a domain name pointing to it. Use certbot with
--staging to practice the full certificate lifecycle: obtain, configure Nginx,
verify HTTPS works, test renewal with --dry-run, then delete and re-obtain with
the real CA.
Exercise 2: Multiple Sites
Configure two separate websites on the same Nginx server, each with their own
Let's Encrypt certificate. Verify that SNI is working correctly by testing both
sites with openssl s_client -servername.
Exercise 3: acme.sh Alternative
Install acme.sh alongside certbot and obtain a staging certificate using it.
Compare the experience -- the directory structure, the renewal mechanism, and the
configuration approach. Which do you prefer?
Bonus Challenge
Write a monitoring script that checks all certificates managed by certbot, reports their expiration dates, and sends an alert (to a log file or email) if any certificate will expire within 14 days. Combine this with the certificate expiration script from Chapter 40 to cover both Let's Encrypt and non-Let's Encrypt certificates.