Let's Encrypt: Free TLS Certificate Automation

Let's Encrypt fundamentally changed how we approach TLS certificates. Before 2016, obtaining a certificate meant paying a certificate authority, dealing with manual verification processes, and...

Key Insights

  • Let’s Encrypt provides free, automated TLS certificates through the ACME protocol, eliminating the cost and manual overhead of traditional certificate authorities while issuing over 3 million certificates daily.
  • The HTTP-01 challenge is simplest for single domains, while DNS-01 enables wildcard certificates and works behind firewalls, making it essential for complex infrastructure deployments.
  • Production environments require automated renewal strategies using systemd timers or cert-manager in Kubernetes, with monitoring to prevent unexpected certificate expirations that cause outages.

Understanding Let’s Encrypt and ACME

Let’s Encrypt fundamentally changed how we approach TLS certificates. Before 2016, obtaining a certificate meant paying a certificate authority, dealing with manual verification processes, and remembering to renew before expiration. Let’s Encrypt solved this by providing free certificates through complete automation.

The secret sauce is the ACME (Automated Certificate Management Environment) protocol. ACME defines a standardized way for certificate authorities to verify domain ownership and issue certificates programmatically. The protocol uses challenge-response mechanisms where you prove control of a domain by completing tasks the CA can verify externally.

Let’s Encrypt certificates expire after 90 days, not because they’re free, but by design. Short-lived certificates reduce the impact of compromised keys and force automation—manual renewal every 90 days would be painful enough that you’ll build proper automation.

Domain Validation Methods

Let’s Encrypt offers three validation methods, each suited for different scenarios.

HTTP-01 Challenge is the most common. The ACME client places a specific file at http://yourdomain.com/.well-known/acme-challenge/TOKEN. Let’s Encrypt’s servers fetch this file to verify you control the domain. This requires port 80 to be accessible and doesn’t work for wildcard certificates.

DNS-01 Challenge requires adding a TXT record to your DNS zone. Let’s Encrypt queries _acme-challenge.yourdomain.com to verify the record. This method works for wildcard certificates and doesn’t require any open ports, making it perfect for internal services or servers behind firewalls.

TLS-ALPN-01 Challenge uses a special TLS handshake on port 443. It’s less common but useful when port 80 isn’t available but 443 is.

Here’s how the ACME flow works:

Client                          Let's Encrypt CA
  |                                    |
  |---(1) Certificate Request--------->|
  |<---(2) Challenge Options-----------|
  |                                    |
  |---(3) Challenge Response---------->|
  |                                    |
  |        (CA validates challenge)    |
  |                                    |
  |<---(4) Certificate Issued----------|

Be aware of rate limits: 50 certificates per registered domain per week, 5 duplicate certificates per week, and 300 new orders per account per 3 hours. These limits rarely affect normal usage but matter for large deployments.

Getting Started with Certbot

Certbot is the official ACME client from the Electronic Frontier Foundation. It’s battle-tested and handles the heavy lifting of certificate management.

Installation on Ubuntu/Debian:

sudo apt update
sudo apt install certbot python3-certbot-nginx

For RHEL/CentOS:

sudo yum install epel-release
sudo yum install certbot python3-certbot-nginx

To obtain your first certificate using the standalone method (requires stopping your web server temporarily):

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

The webroot method works while your server runs:

sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com

Certbot stores certificates in /etc/letsencrypt/live/example.com/. The key files are:

  • fullchain.pem - Your certificate plus intermediate certificates
  • privkey.pem - Your private key
  • cert.pem - Your certificate only
  • chain.pem - Intermediate certificates only

Configure Nginx to use your new certificate:

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 HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # Your application configuration
    location / {
        proxy_pass http://localhost:3000;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$server_name$request_uri;
}

For Apache:

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem

    # Your application configuration
</VirtualHost>

Automated Renewal

Manual renewal defeats the purpose of Let’s Encrypt. Certbot includes renewal logic—you just need to schedule it.

Test renewal first:

sudo certbot renew --dry-run

Create a cron job for automatic renewal:

sudo crontab -e

Add this line to check twice daily (certificates renew when they have 30 days or less remaining):

0 0,12 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

For systemd-based systems, use a timer instead. Create /etc/systemd/system/certbot-renewal.service:

[Unit]
Description=Let's Encrypt renewal
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

Create /etc/systemd/system/certbot-renewal.timer:

[Unit]
Description=Let's Encrypt renewal timer

[Timer]
OnCalendar=0/12:00:00
RandomizedDelaySec=1h
Persistent=true

[Install]
WantedBy=timers.target

Enable and start the timer:

sudo systemctl enable certbot-renewal.timer
sudo systemctl start certbot-renewal.timer

Pre and post-renewal hooks let you perform actions during renewal:

sudo certbot renew --pre-hook "systemctl stop nginx" \
                   --post-hook "systemctl start nginx" \
                   --deploy-hook "systemctl reload nginx"

Wildcard Certificates and DNS Automation

Wildcard certificates (*.example.com) require DNS-01 validation. You’ll need a DNS provider with API support.

Install the appropriate DNS plugin. For Cloudflare:

sudo apt install python3-certbot-dns-cloudflare

Create credentials file at /etc/letsencrypt/cloudflare.ini:

dns_cloudflare_api_token = your_api_token_here

Secure it:

sudo chmod 600 /etc/letsencrypt/cloudflare.ini

Request a wildcard certificate:

sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d example.com \
  -d '*.example.com'

For AWS Route53:

sudo apt install python3-certbot-dns-route53

Configure AWS credentials with appropriate IAM permissions, then:

sudo certbot certonly \
  --dns-route53 \
  -d example.com \
  -d '*.example.com'

The DNS plugins handle record creation and cleanup automatically. Renewal works the same as HTTP-01 certificates.

Production Deployment Patterns

For Docker environments, use a sidecar container pattern. Here’s a Docker Compose setup:

version: '3.8'

services:
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    depends_on:
      - certbot

  certbot:
    image: certbot/certbot
    volumes:
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"

volumes:
  certbot-etc:
  certbot-var:
  web-root:

In Kubernetes, cert-manager is the standard solution. Install it:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.0/cert-manager.yaml

Create a ClusterIssuer for Let’s Encrypt:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx

Request a certificate via Ingress annotation:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: example-ingress
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - example.com
    secretName: example-tls
  rules:
  - host: example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: example-service
            port:
              number: 80

cert-manager automatically obtains and renews certificates, storing them in Kubernetes secrets.

Monitoring and Troubleshooting

Monitor certificate expiration to avoid outages. Create a check script:

#!/bin/bash
DOMAIN="example.com"
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 14 ]; then
    echo "WARNING: Certificate expires in $DAYS_LEFT days"
    exit 1
fi

echo "Certificate valid for $DAYS_LEFT days"

For Prometheus monitoring, use blackbox_exporter:

- job_name: 'ssl-expiry'
  metrics_path: /probe
  params:
    module: [http_2xx]
  static_configs:
    - targets:
      - https://example.com
  relabel_configs:
    - source_labels: [__address__]
      target_label: __param_target
    - source_labels: [__param_target]
      target_label: instance
    - target_label: __address__
      replacement: blackbox-exporter:9115

Common troubleshooting commands:

# Check certificate details
sudo certbot certificates

# Verbose renewal test
sudo certbot renew --dry-run --verbose

# Check if port 80 is accessible
sudo netstat -tlnp | grep :80

# Verify DNS propagation
dig _acme-challenge.example.com TXT

# Check Let's Encrypt logs
sudo tail -f /var/log/letsencrypt/letsencrypt.log

The most common issues are firewall rules blocking port 80, DNS records not propagating, or web server misconfigurations preventing access to .well-known/acme-challenge/. Always test with --dry-run before making changes to production certificates to avoid hitting rate limits.

Let’s Encrypt transformed TLS from a paid, manual process into free, automated infrastructure. With proper automation and monitoring, certificate management becomes invisible—exactly how it should be.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.