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:

  1. Encourages automation -- If you must renew every 90 days, you are forced to automate it. Automated renewal is more reliable than human memory.
  2. Limits damage -- If a key is compromised, the exposure window is shorter.
  3. 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.com means 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;
    }
}

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:

  1. Read your existing Nginx configuration
  2. Obtain the certificate using webroot via the existing Nginx
  3. Modify the Nginx config to add SSL directives
  4. 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-cloudflare
  • certbot-dns-route53 (AWS)
  • certbot-dns-google (Google Cloud DNS)
  • certbot-dns-digitalocean
  • certbot-dns-linode
  • certbot-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.sh works 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.