Back to skills
SKILL.md
Lets Encrypt
FSecurityExpert agent for Let's Encrypt free TLS certificates via ACME. Covers certbot, acme.sh, HTTP-01/DNS-01/TLS-ALPN-01 challenges, 90-day and 6-day certificate lifetimes, wildcard certificates, rate limits, staging environment, and automation best practices. WHEN: \"Let's Encrypt\", \"certbot\", \"acme.sh\", \"ACME\", \"free TLS\", \"LEGO\", \"Certify The Web\", \"short-lived certificate\", \"6-day certificate\", \"ACME challenge\", \"DNS-01 challenge\".
- 4 stars
- 0 votes
- 0 copies
- 0 views
- Added September 24, 2026
Works with
Security analysis
32/100- Pipes output to a shell interpreter
- Uses curl or wget to download content
- Creates or modifies cron jobs for persistent execution
- Creates or modifies cron jobs for persistent execution
- Modifies startup scripts or system services for persistence
- Downloads and executes remote scripts — classic supply chain attack
- Installs packages at runtime which could introduce malicious dependencies
npx -y skills add chrishuffman5/domain-expert --skill lets-encrypt --agent claude-codeAre you the author of Lets Encrypt?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/chrishuffman5-lets-encrypt)---
name: lets-encrypt
description: "Expert agent for Let's Encrypt free TLS certificates via ACME. Covers certbot, acme.sh, HTTP-01/DNS-01/TLS-ALPN-01 challenges, 90-day and 6-day certificate lifetimes, wildcard certificates, rate limits, staging environment, and automation best practices. WHEN: \"Let's Encrypt\", \"certbot\", \"acme.sh\", \"ACME\", \"free TLS\", \"LEGO\", \"Certify The Web\", \"short-lived certificate\", \"6-day certificate\", \"ACME challenge\", \"DNS-01 challenge\"."
license: MIT
---
# Let's Encrypt
This skill covers Let's Encrypt and the ACME protocol ecosystem. It has deep knowledge of certificate issuance, renewal automation, challenge types, rate limits, and integration patterns.
## How to Approach Tasks
1. **Identify the deployment context**: web server (nginx/Apache/Caddy), container (K8s), serverless, load balancer, or custom application.
2. **Determine challenge type needed**: HTTP-01 (single servers with port 80), DNS-01 (wildcard, no port 80 access), TLS-ALPN-01 (only port 443).
3. **Identify ACME client**: certbot (most common), acme.sh (scriptable), LEGO (Go), step CLI, or built-in (Caddy, Traefik).
4. **Consider lifetime**: 90-day (current default), 6-day (available, requires renewal every ~4 days), 45-day opt-in (May 2026).
## Certificate Lifetimes
| Profile | Lifetime | Renewal Frequency | Launched |
|---|---|---|---|
| Default (90-day) | 90 days | Every ~60 days (at 2/3) | Always |
| Short-lived (6-day) | 6 days | Every ~4 days | March 2025 |
| 45-day opt-in | 45 days | Every ~30 days | May 2026 |
**6-day certificates**: Require fully automated renewal. No OCSP needed (cert expires before compromise can be acted upon). Ideal for fully automated infrastructure.
**Recommendation**: Use 90-day unless you have full automation and want to eliminate revocation dependency.
## certbot
### Installation
```bash
# Ubuntu/Debian
sudo apt install certbot python3-certbot-nginx # or python3-certbot-apache
# macOS
brew install certbot
# pip (any platform)
pip install certbot certbot-nginx certbot-apache
# snap (recommended for Ubuntu)
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
```
### Obtaining Certificates
```bash
# Nginx — automatically edits nginx config
sudo certbot --nginx -d example.com -d www.example.com
# Apache — automatically edits Apache config
sudo certbot --apache -d example.com -d www.example.com
# Standalone (temporary HTTP server on port 80)
# Use when nginx/Apache is not running
sudo certbot certonly --standalone -d example.com
# Webroot (place challenge files in existing webroot)
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com
# DNS-01 (for wildcard, requires DNS plugin)
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
-d example.com -d "*.example.com"
```
### DNS-01 Plugins
certbot has plugins for major DNS providers:
- `certbot-dns-cloudflare` — Cloudflare
- `certbot-dns-route53` — AWS Route 53
- `certbot-dns-google` — Google Cloud DNS
- `certbot-dns-azure` — Azure DNS
- `certbot-dns-digitalocean` — DigitalOcean
- `certbot-dns-ovh` — OVH
```bash
# Install Cloudflare plugin
pip install certbot-dns-cloudflare
# Cloudflare credentials file
cat > ~/.secrets/certbot/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 ~/.secrets/certbot/cloudflare.ini
# Issue wildcard cert
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \
--dns-cloudflare-propagation-seconds 30 \
-d example.com \
-d "*.example.com"
```
### Certificate Files
Certbot stores certificates in `/etc/letsencrypt/live/<domain>/`:
```
/etc/letsencrypt/live/example.com/
cert.pem → Certificate only (not for nginx/Apache — use fullchain)
chain.pem → Intermediate certificate(s) only
fullchain.pem → cert.pem + chain.pem (use this for most servers)
privkey.pem → Private key (readable only by root)
```
nginx config:
```nginx
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
```
### Renewal
```bash
# Test renewal (dry run, no actual cert change)
sudo certbot renew --dry-run
# Force renewal (even if not yet due)
sudo certbot renew --force-renewal
# Renew specific certificate
sudo certbot renew --cert-name example.com
# Check renewal timer (systemd)
sudo systemctl status snap.certbot.renew.timer
# Or cron:
0 0,12 * * * root certbot renew --quiet
```
### Renewal Hooks
```bash
# Deploy hook — runs after successful renewal
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/bash
systemctl reload nginx
# Pre-hook — runs before renewal attempt
# /etc/letsencrypt/renewal-hooks/pre/stop-haproxy.sh
#!/bin/bash
systemctl stop haproxy
# Post-hook — runs after renewal attempt (success or failure)
# /etc/letsencrypt/renewal-hooks/post/start-haproxy.sh
#!/bin/bash
systemctl start haproxy
```
Or configure in `/etc/letsencrypt/renewal/example.com.conf`:
```ini
[renewalparams]
deploy_hook = systemctl reload nginx
```
## acme.sh
A pure Bash ACME client with broad DNS provider support (150+ providers).
```bash
# Install
curl https://get.acme.sh | sh -s email=admin@example.com
# Issue certificate (HTTP-01, webroot mode)
acme.sh --issue -d example.com -w /var/www/html
# Issue certificate (standalone)
acme.sh --issue -d example.com --standalone
# Issue wildcard (DNS-01, Cloudflare example)
export CF_Token="your-cloudflare-api-token"
export CF_Account_ID="your-account-id"
acme.sh --issue -d example.com -d "*.example.com" --dns dns_cf
# Install certificate to nginx
acme.sh --install-cert -d example.com \
--cert-file /etc/nginx/ssl/cert.pem \
--key-file /etc/nginx/ssl/privkey.pem \
--fullchain-file /etc/nginx/ssl/fullchain.pem \
--reloadcmd "systemctl reload nginx"
# List all certificates
acme.sh --list
# Force renew
acme.sh --renew -d example.com --force
# Auto-renewal is handled by cron (installed automatically)
# Check with: crontab -l | grep acme
```
### acme.sh with Let's Encrypt Staging
```bash
# Use staging to test without hitting rate limits
acme.sh --issue -d example.com --standalone --staging
# Switch from staging to production
acme.sh --issue -d example.com --standalone --server letsencrypt
```
## Built-in ACME Servers
### Caddy
Caddy automatically obtains and renews Let's Encrypt certificates:
```caddyfile
# Caddyfile
example.com {
root * /var/www/html
file_server
# TLS is automatic — Caddy handles ACME
}
# With custom email
{
email admin@example.com
}
```
For internal CAs:
```caddyfile
example.com {
tls {
ca https://step-ca.internal/acme/acme/directory
}
}
```
### Traefik
```yaml
# traefik.yml
certificatesResolvers:
letsencrypt:
acme:
email: admin@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web # use HTTP-01 challenge
# OR:
dnsChallenge:
provider: cloudflare # use DNS-01 challenge
delayBeforeCheck: 30
---
# Docker label on service
labels:
- "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
- "traefik.http.routers.myapp.rule=Host(`example.com`)"
```
## Rate Limit Management
### Avoiding Limits
1. **Use staging for testing**: `--staging` flag; staging uses separate rate limit pool
2. **Reuse certificates**: Don't re-issue when cert is still valid; use `--force-renewal` sparingly
3. **Batch issuance**: Include all SANs in one certificate rather than separate certificates per domain
4. **Monitor usage**: `https://crt.sh/?q=example.com` shows issued certificates
### When You Hit Rate Limits
Let's Encrypt returns a `429 Too Many Requests` or specific error message indicating which limit was hit:
- "too many certificates already issued for exact set of domains" — Duplicate limit (5/week)
- "too many certificates already issued for this domain" — Domain limit (50/week)
**Only solution**: Wait until the sliding window resets (7 days from oldest cert in that group).
### Rate Limit Exceptions
Apply for rate limit increases: `https://issuance-limit-requests.letsencrypt.org` for legitimate high-volume needs. Alternatives:
- ZeroSSL (same ACME protocol, separate rate limits)
- Google Trust Services (separate rate limits, requires Google Cloud project)
- AWS Certificate Manager (ACM) for AWS-hosted workloads (no rate limits, free in ACM)
## Common Issues and Fixes
### "Connection refused" on HTTP-01 challenge
1. Check port 80 is open in firewall/security group
2. Check web server is listening on port 80
3. Check no redirect loop before challenge token is served: challenge path must serve token on HTTP (not redirect to HTTPS)
4. If behind load balancer: LB must forward `.well-known/acme-challenge/` to the server running certbot
```nginx
# Ensure challenge path is served before HTTPS redirect
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
# Served BEFORE redirect
}
location / {
return 301 https://$host$request_uri;
}
}
```
### "DNS problem" on DNS-01 challenge
1. Check DNS propagation: `dig TXT _acme-challenge.example.com @8.8.8.8`
2. Increase `--dns-cloudflare-propagation-seconds` (or equivalent) to 60-120s
3. Verify API token has DNS:Edit permission for the zone
4. Check TTL on TXT record (short TTL helps during validation)
### Certificate Not Renewed After Expiry
1. Check certbot timer: `systemctl status snap.certbot.renew.timer`
2. Check cron: `crontab -l | grep certbot` or `/etc/cron.d/certbot`
3. Check logs: `/var/log/letsencrypt/letsencrypt.log`
4. Verify certificate location: `certbot certificates`
5. Check if server is reloaded after renewal (missing deploy hook)
### CAA Record Blocking Issuance
If you have CAA records that don't include `letsencrypt.org`:
```
# DNS: Allow Let's Encrypt
example.com. CAA 0 issue "letsencrypt.org"
example.com. CAA 0 issuewild "letsencrypt.org" # for wildcard
```
## Production Checklist
- [ ] Test in staging environment first
- [ ] Configure automatic renewal (cron or systemd timer)
- [ ] Set up reload hook (nginx/Apache reload after renewal)
- [ ] Monitor certificate expiry independently (external monitoring, not just certbot)
- [ ] Set up CAA DNS records
- [ ] Configure OCSP stapling in web server
- [ ] Test HTTPS with `https://www.ssllabs.com/ssltest/`
- [ ] Set up alerts for renewal failures (email or monitoring integration)
Attribution
Comments
Loading comments…