强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

CA 证书详解:从原理到实践的完整教程 / 第 7 章:Let's Encrypt

第 7 章:Let’s Encrypt

Let’s Encrypt 是互联网安全的里程碑项目,通过 ACME 协议实现了免费、自动化、开放的证书签发。本章全面介绍 Let’s Encrypt 的工作原理和自动化实践。


7.1 Let’s Encrypt 简介

基本信息

项目说明
运营方Internet Security Research Group (ISRG)
成立时间2015 年
证书类型DV(Domain Validation)证书
费用完全免费
有效期90 天(鼓励自动化续期)
根 CAISRG Root X1
服务域名通过 HTTPS 接入
证书市场份额超过 30%(截至 2025 年)

信任链

ISRG Root X1 (自签名,预装在各平台信任存储中)
  └── R3 (中间 CA)
        └── 终端证书 (*.example.com)
  └── R4 (中间 CA,备用)
        └── 终端证书

旧设备兼容链(通过 DST Root CA X3 交叉签名):
DST Root CA X3 (已过期 2021-09-30)
  └── ISRG Root X1
        └── R3
              └── 终端证书

💡 提示:ISRG Root X1 已被所有主流操作系统和浏览器信任。对于不信任 ISRG Root X1 的旧设备(Android 7.1 以下),可以使用 Let’s Encrypt 提供的交叉签名链。


7.2 ACME 协议

ACME(Automatic Certificate Management Environment)是 Let’s Encrypt 使用的自动化证书管理协议,定义在 RFC 8555 中。

ACME 工作流程

1. 客户端生成密钥对和账户注册
   Client ──register──▶ ACME Server
   (创建账户,同意服务条款)

2. 客户端请求证书
   Client ──new-order──▶ ACME Server
   (指定需要的域名)

3. 服务器返回验证挑战
   ACME Server ──challenges──▶ Client
   (HTTP-01, DNS-01, TLS-ALPN-01)

4. 客户端完成验证
   Client ──完成挑战──▶ ACME Server
   (放置验证文件/添加 DNS 记录)

5. 服务器验证通过后签发证书
   ACME Server ──certificate──▶ Client
   (返回签发的证书)

ACME 验证方式

方式原理适用场景操作
HTTP-01/.well-known/acme-challenge/ 放置文件有 Web 服务器自动(certbot)
DNS-01添加 _acme-challenge TXT 记录通配符证书、无 Web 服务器需要 DNS API
TLS-ALPN-01通过 TLS 握手验证只有 443 端口需要专用工具

HTTP-01 验证详解

验证过程:
  1. ACME 服务器向客户端发送 token
  2. 客户端在 Web 服务器上放置文件:
     http://example.com/.well-known/acme-challenge/<token>
     文件内容: <token>.<thumbprint-of-account-key>
  3. ACME 服务器从 80 端口访问该 URL
  4. 验证文件内容正确 → 验证通过
# 手动模拟 HTTP-01 验证
# 创建验证目录
sudo mkdir -p /var/www/html/.well-known/acme-challenge/
echo "test-token.test-thumbprint" | sudo tee /var/www/html/.well-known/acme-challenge/test-token
curl http://example.com/.well-known/acme-challenge/test-token

DNS-01 验证详解

验证过程:
  1. ACME 服务器向客户端发送 token
  2. 客户端添加 DNS TXT 记录:
     _acme-challenge.example.com TXT "<token>.<thumbprint>"
  3. ACME 服务器查询该 TXT 记录
  4. 验证记录正确 → 验证通过
# 查看 DNS-01 验证记录
dig _acme-challenge.example.com TXT

7.3 使用 certbot

安装 certbot

# Debian/Ubuntu
sudo apt install certbot

# 或使用 snap 安装(推荐)
sudo snap install --classic certbot

# RHEL/CentOS/Fedora
sudo dnf install certbot

# 验证安装
certbot --version

基本使用

# Nginx 插件(自动配置)
sudo certbot --nginx -d example.com -d www.example.com

# Apache 插件
sudo certbot --apache -d example.com -d www.example.com

# Standalone 模式(临时启动 Web 服务器)
sudo certbot certonly --standalone -d example.com

# Webroot 模式(使用现有 Web 服务器)
sudo certbot certonly --webroot -w /var/www/html -d example.com

# 使用 DNS 验证(通配符证书需要)
sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com

通配符证书

# 通配符证书必须使用 DNS-01 验证
sudo certbot certonly \
  --manual \
  --preferred-challenges dns \
  -d "*.example.com" \
  -d "example.com"

# 输出:
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Please deploy a DNS TXT record under the name:
#
# _acme-challenge.example.com
#
# with the following value:
#
# abc123xyz789...
#
# Before continuing, verify the TXT record has been deployed.
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

DNS 插件自动化

# 使用 Cloudflare DNS 插件
sudo apt install python3-certbot-dns-cloudflare

# 创建凭证文件
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = your-api-token
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini

# 自动签发通配符证书
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.example.com" \
  -d "example.com"

# 支持的 DNS 插件
# python3-certbot-dns-cloudflare  (Cloudflare)
# python3-certbot-dns-route53     (AWS Route 53)
# python3-certbot-dns-google      (Google Cloud DNS)
# python3-certbot-dns-digitalocean (DigitalOcean)
# python3-certbot-dns-dnspod      (DNSPod)

7.4 证书续期

自动续期

# 测试续期
sudo certbot renew --dry-run

# 查看已安装的证书
sudo certbot certificates

# 手动续期
sudo certbot renew

# 设置自动续期(systemd timer,certbot 安装时自动配置)
sudo systemctl status certbot.timer

# 查看定时器详情
sudo systemctl list-timers certbot.timer
# 手动设置 cron 续期
# certbot 建议一天运行两次
cat > /etc/cron.d/certbot << 'EOF'
# 每 12 小时尝试续期
0 */12 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx" 2>&1
EOF

续期钩子

# 续期前钩子(停止服务释放 80 端口)
sudo certbot renew --pre-hook "systemctl stop nginx"

# 续期后钩子(重新加载服务)
sudo certbot renew --deploy-hook "systemctl reload nginx"

# 配置默认钩子
cat >> /etc/letsencrypt/cli.ini << 'EOF'
deploy-hook = systemctl reload nginx
pre-hook = systemctl stop nginx
post-hook = systemctl start nginx
EOF

续期最佳实践

               续期时间线
 ────────────────────────────────────────────▶
 │               │                │           │
 签发          60 天            80 天       90 天
 │                              │           │
 │       certbot renew         │         过期
 │       (建议在此区间续期)      │
 │                              │
 │                              ▼
 │                     最迟续期时间
策略说明
推荐间隔每 12 小时运行 certbot renew
dry-run首次配置后务必执行 --dry-run
监控监控证书过期时间,确保续期成功
降级策略如果自动续期失败,手动续期流程

7.5 Nginx / Apache 集成

Nginx 完整配置

server {
    listen 80;
    server_name example.com www.example.com;
    
    # ACME challenge
    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }
    
    # HTTP → HTTPS 重定向
    location / {
        return 301 https://$server_name$request_uri;
    }
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;
    
    # Let's Encrypt 证书路径
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    
    # SSL 优化
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    
    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    
    # Session 复用
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    
    # HSTS
    add_header Strict-Transport-Security "max-age=63072000" always;
    
    root /var/www/html;
    index index.html;
}

Apache 配置

<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    
    # ACME challenge
    Alias /.well-known/acme-challenge/ /var/www/html/.well-known/acme-challenge/
    
    # HTTP → HTTPS 重定向
    RewriteEngine On
    RewriteCond %{HTTPS} off
    RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    
    # HSTS
    Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>

7.6 速率限制

Let’s Encrypt 有严格的速率限制,了解它们可以避免被封禁。

主要限制

限制类型限制值说明
每个域名每周证书数50 张包括所有子域名
每个 IP 每 3 小时订单数10 个新订单请求
每个域名每小时验证失败5 次验证失败计数
重复证书5 张/周相同域名组合的证书
续期不限制-续期不计入速率限制
# 查看当前域名的证书数量
curl -s "https://crt.sh/?q=example.com&output=json" | jq 'length'

# 测试验证(不会消耗速率限制)
sudo certbot renew --dry-run

⚠️ 注意:在开发和测试环境中,务必使用 Let’s Encrypt 的 staging 环境,避免消耗生产环境的速率限制。

# 使用 staging 环境
sudo certbot certonly --staging --standalone -d test.example.com

# 清除 staging 证书
sudo certbot delete --cert-name test.example.com

# Nginx 配置中使用 staging 证书
# ssl_certificate     /etc/letsencrypt/live/test.example.com/fullchain.pem;  (staging 路径)

7.7 证书透明度(CT)

Let’s Encrypt 自动将所有签发的证书提交到 CT 日志。

查看 CT 记录

# 使用 crt.sh 查询
curl -s "https://crt.sh/?q=example.com&output=json&exclude=expired" | \
  jq '[.[] | select(.issuer_name_id > 0)] | 
    group_by(.issuer_name) | 
    map({issuer: .[0].issuer_name, count: length})'

# 查看 Let's Encrypt 签发的证书
curl -s "https://crt.sh/?q=example.com&issuer_name_id=183267&output=json" | \
  jq '.[] | {id, not_before, not_after, name_value}'

CT 监控脚本

#!/usr/bin/env bash
# ct-monitor-le.sh - 监控 Let's Encrypt 为你的域名签发的证书
DOMAIN="${1:?用法: $0 <domain>}"

echo "检查 ${DOMAIN} 的 Let's Encrypt CT 记录..."
echo ""

# Let's Encrypt 的 issuer_name_id (crt.sh)
# R3: 183267, R10: 3334554, R11: 3334555
LE_CERTS=$(curl -s "https://crt.sh/?q=${DOMAIN}&output=json" 2>/dev/null | \
  jq '[.[] | select(.issuer_name | contains("Let'"'"'s Encrypt") or contains("ISRG"))]')

COUNT=$(echo "$LE_CERTS" | jq 'length')
echo "共找到 ${COUNT} 张 Let's Encrypt 证书"
echo ""

echo "$LE_CERTS" | jq -r '.[-5:] | .[] | 
  "  ID: \(.id)\n  域名: \(.name_value)\n  签发: \(.not_before)\n  过期: \(.not_after)\n"'

7.8 替代客户端

certbot 替代方案

工具语言特点
certbotPython官方推荐,插件丰富
acme.shShell纯 Shell 实现,轻量
legoGo库/CLI,DNS 插件丰富
Certify The WebWindowsGUI 工具,适合 Windows 管理

acme.sh 使用

# 安装
curl https://get.acme.sh | sh

# 签发证书(standalone 模式)
acme.sh --issue -d example.com --standalone

# 使用 webroot 模式
acme.sh --issue -d example.com -w /var/www/html

# 通配符证书(Cloudflare DNS)
acme.sh --issue -d "*.example.com" --dns dns_cf

# 安装到 Nginx
acme.sh --install-cert -d example.com \
  --key-file /etc/nginx/ssl/key.pem \
  --fullchain-file /etc/nginx/ssl/fullchain.pem \
  --reloadcmd "systemctl reload nginx"

# 自动续期(acme.sh 安装时自动设置 cron)
crontab -l | grep acme
# 0 0 * * * "/home/user/.acme.sh/acme.sh" --cron

lego 使用

# 安装
go install github.com/go-acme/lego/v4/cmd/lego@latest

# HTTP 验证
lego --http --domains example.com --email admin@example.com run

# DNS 验证(Cloudflare)
CLOUDFLARE_EMAIL=admin@example.com \
CLOUDFLARE_API_KEY=xxx \
lego --dns cloudflare --domains "*.example.com" --email admin@example.com run

# 证书存储路径
ls .lego/certs/example.com/

7.9 高级场景

多域名证书

# certbot 多域名
sudo certbot certonly --nginx \
  -d example.com -d www.example.com \
  -d api.example.com -d admin.example.com

# 查看证书包含的域名
sudo certbot certificates

泛域名 + 裸域名

# 同时申请泛域名和裸域名
sudo certbot certonly \
  --manual --preferred-challenges dns \
  -d "example.com" \
  -d "*.example.com"

# 注意:需要为两条 DNS 记录添加 TXT 值
# _acme-challenge.example.com (两条 TXT 记录)

Docker 环境

# docker-compose.yml
version: '3'
services:
  nginx:
    image: nginx:latest
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
      - certbot-etc:/etc/letsencrypt
      - certbot-var:/var/lib/letsencrypt
      - web-root:/var/www/html

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

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

7.10 本章小结

主题关键要点
Let’s Encrypt免费 DV 证书,90 天有效期
ACME 协议HTTP-01 / DNS-01 / TLS-ALPN-01 三种验证方式
certbot官方推荐客户端,支持 Nginx/Apache 插件
通配符证书必须使用 DNS-01 验证
续期推荐每 12 小时自动续期
速率限制开发测试使用 staging 环境
CTLet’s Encrypt 自动提交到 CT 日志

📚 执展阅读


上一章第 6 章:OpenSSL 工具 下一章第 8 章:搭建私有 CA — 学习使用 CFSSL、easy-rsa 等工具搭建私有 CA。