Skip to content
Published on

[DevOps] Let's Encrypt 与 Certbot 完全指南:免费 TLS 证书

分享
Authors

1. TLS/SSL 证书基本概念

1.1 为什么需要 TLS 证书

TLS(Transport Layer Security)证书为 Web 通信提供三个核心安全要素。

  • 加密(Encryption):加密客户端与服务器之间的数据,防止窃听
  • 认证(Authentication):证明服务器确实是该域名的所有者
  • 完整性(Integrity):保证数据在传输过程中未被篡改

1.2 证书链结构

┌───────────────────────┐
│   Root CA Certificate │  ← 浏览器/OS 内置的信任锚
│  (Self-Signed)        │
└───────────┬───────────┘
            │ 签名
┌───────────▼───────────┐
│ Intermediate CA Cert  │  ← 中间 CA 证书
│ (Signed by Root CA)   │
└───────────┬───────────┘
            │ 签名
┌───────────▼───────────┐
│  Server Certificate   │  ← 域名证书 (Leaf)
│ (Signed by Inter. CA) │
└───────────────────────┘

1.3 公钥/私钥对

┌──────────┐     CSR 生成     ┌──────────────┐
│ 私钥       │ ──────────────> │     CSR      │
│(Private  │                  │(证书签名      │
│  Key)    │                  │  请求)        │
└──────────┘                  └──────┬───────┘
     │                               │ 提交给 CA
     │                        ┌──────▼───────┐
     │                        │     CA       │
     │                        │  (签名发放)    │
     │                        └──────┬───────┘
     │                               │
     │                        ┌──────▼───────┐
     └───────── 一起使用 ──────>│   证书        │
                              │ (Certificate) │
                              └──────────────┘
  • 私钥(Private Key):仅服务器持有,绝对禁止外泄
  • CSR(Certificate Signing Request):公钥 + 域名信息,提交给 CA
  • 证书(Certificate):CA 签名后的公钥 + 域名信息

2. 什么是 Let's Encrypt

2.1 概述

Let's Encrypt 是由 ISRG(Internet Security Research Group)运营的免费、自动化、开放的证书颁发机构(CA)。

核心特点:

  • 免费:免费签发域名验证(DV)证书
  • 自动化:通过 ACME 协议完全自动化证书签发与续期
  • 开放:使用开源协议与工具
  • 有效期:90 天(用较短周期强化安全性)
  • 可信:所有主流浏览器与操作系统均信任

2.2 证书类型对比

类型验证级别签发时间费用Let's Encrypt
DV (Domain Validation)确认域名所有数分钟免费~低价支持
OV (Organization Validation)确认组织数日收费不支持
EV (Extended Validation)扩展验证数周高价不支持

3. ACME 协议工作原理

ACME(Automatic Certificate Management Environment)是将证书签发自动化的协议。

3.1 整体流程

┌────────┐                          ┌──────────────┐
│ Certbot│                          │ Let's Encrypt│
│(Client)│                          │   (CA/ACME)  │
└───┬────┘                          └──────┬───────┘
    │  1. Account Registration              │
    │──────────────────────────────────────>│
    │  2. Account Created                   │
    │<──────────────────────────────────────│
    │                                       │
    │  3. Order (domain list)               │
    │──────────────────────────────────────>│
    │  4. Authorizations + Challenges       │
    │<──────────────────────────────────────│
    │                                       │
    │  5. Respond to Challenge              │
    │  (HTTP-01 / DNS-01 / TLS-ALPN-01)    │
    │──────────────────────────────────────>│
    │                                       │
    │  6. Challenge Validated               │
    │<──────────────────────────────────────│
    │                                       │
    │  7. Finalize (send CSR)               │
    │──────────────────────────────────────>│
    │  8. Certificate issued                │
    │<──────────────────────────────────────│

3.2 HTTP-01 Challenge

最常见的挑战方式。通过端口 80 验证能否访问特定文件。

Let's Encrypt → http://yourdomain.com/.well-known/acme-challenge/TOKEN_VALUE

验证过程:
1. Certbot 将令牌文件放置到 Web 服务器
2. Let's Encrypt 通过 HTTP 访问该文件
3. 文件内容与期望值一致则确认域名所有权

优点:

  • 最简单、最通用
  • 无需额外的 DNS 配置
  • 在大多数 Web 服务器上都容易配置

限制:

  • 端口 80 必须能从外部访问
  • 无法签发通配符证书
  • 位于负载均衡器之后时配置可能变复杂

3.3 DNS-01 Challenge

通过创建 DNS TXT 记录来证明域名所有权。签发通配符证书时必须使用此方式

Let's Encrypt → DNS 查询: _acme-challenge.yourdomain.com TXT

验证过程:
1. Certbot 计算令牌
2. 在 _acme-challenge.yourdomain.com TXT 记录中设置令牌值
3. Let's Encrypt 查询 DNS 确认该值
4. 一致则确认域名所有权

优点:

  • 可以签发通配符证书
  • 没有 Web 服务器也可以完成
  • 端口 80 不开放也没关系
  • 多台服务器可共用同一份证书

限制:

  • 需要 DNS Provider API(自动化时)
  • 可能存在 DNS 传播延迟
  • 手动配置比较繁琐

3.4 TLS-ALPN-01 Challenge

在端口 443 建立 TLS 连接时,通过 ALPN(Application-Layer Protocol Negotiation)扩展进行验证。

验证过程:
1. Let's Encrypt 尝试通过端口 443 建立 TLS 连接
2. 用 ALPN 协商 "acme-tls/1" 协议
3. 服务器用自签名证书响应(包含特定扩展)
4. 用证书的 acmeIdentifier 扩展值进行验证

优点:

  • 不需要端口 80
  • 仅凭端口 443 即可完成验证

限制:

  • 大多数 Web 服务器默认不支持
  • 需要额外的软件(例如 Caddy)

3.5 挑战方式对比表

功能HTTP-01DNS-01TLS-ALPN-01
端口80无 (DNS)443
通配符XOX
需要 Web 服务OXO
自动化难度中 (需要 DNS API)
需要 DNS 配置XOX
主要用途普通 Web 服务器通配符、内部服务器特殊环境

4. Certbot 安装

4.1 Ubuntu / Debian

# 使用 snap 安装(推荐)
sudo snap install core
sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

4.2 CentOS / RHEL / Rocky Linux

# 启用 EPEL 仓库
sudo dnf install epel-release
sudo dnf install certbot

# Nginx 插件
sudo dnf install python3-certbot-nginx

4.3 macOS

brew install certbot

4.4 Docker

docker run -it --rm \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/lib/letsencrypt:/var/lib/letsencrypt \
  certbot/certbot certonly --help

5. 证书签发方式

5.1 Standalone 模式

Certbot 启动自带的 Web 服务器来处理 HTTP-01 挑战。需要先停止现有的 Web 服务器。

# 因为要占用端口 80,需要先停止现有 Web 服务器
sudo systemctl stop nginx

sudo certbot certonly --standalone \
  -d example.com \
  -d www.example.com \
  --agree-tos \
  --email admin@example.com \
  --non-interactive

# 重启 Web 服务器
sudo systemctl start nginx

5.2 Webroot 模式

无需停止现有 Web 服务器即可签发证书。需要配置 Web 服务器对外提供特定目录。

Nginx 配置追加:

server {
    listen 80;
    server_name example.com www.example.com;

    # ACME 挑战用目录
    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

    # 其余请求重定向到 HTTPS
    location / {
        return 301 https://$host$request_uri;
    }
}
# 创建目录
sudo mkdir -p /var/www/certbot

# 以 Webroot 模式签发证书
sudo certbot certonly --webroot \
  -w /var/www/certbot \
  -d example.com \
  -d www.example.com \
  --agree-tos \
  --email admin@example.com

5.3 Nginx 插件

Certbot 会自动修改 Nginx 配置。

sudo certbot --nginx \
  -d example.com \
  -d www.example.com \
  --agree-tos \
  --email admin@example.com

Certbot 自动完成的工作:

  • 处理 HTTP-01 挑战
  • 在 Nginx 配置中添加 SSL 相关指令
  • 添加将 HTTP 重定向到 HTTPS 的配置

5.4 Apache 插件

sudo certbot --apache \
  -d example.com \
  -d www.example.com \
  --agree-tos \
  --email admin@example.com

5.5 DNS 插件(通配符证书)

通配符证书必须使用 DNS-01 挑战。

Cloudflare DNS 插件:

# 安装插件
sudo snap install certbot-dns-cloudflare

# 创建 API 令牌文件
sudo mkdir -p /etc/letsencrypt
cat > /etc/letsencrypt/cloudflare.ini << 'CFEOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
CFEOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

# 签发通配符证书
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "example.com" \
  -d "*.example.com" \
  --agree-tos \
  --email admin@example.com

AWS Route 53 DNS 插件:

sudo snap install certbot-dns-route53

# 配置 AWS 凭证(IAM Role 或环境变量)
sudo certbot certonly \
  --dns-route53 \
  -d "example.com" \
  -d "*.example.com" \
  --agree-tos \
  --email admin@example.com

手动 DNS 挑战:

sudo certbot certonly --manual \
  --preferred-challenges dns \
  -d "example.com" \
  -d "*.example.com" \
  --agree-tos \
  --email admin@example.com

# 当 Certbot 提示 TXT 记录值时
# 在 DNS 管理页面手动添加 TXT 记录:
# _acme-challenge.example.com  TXT  "提示的_令牌_值"

6. 签发出的文件结构

# 证书文件路径
ls -la /etc/letsencrypt/live/example.com/

# 文件结构
/etc/letsencrypt/live/example.com/
  cert.pem       # 服务器证书(仅域名证书)
  chain.pem      # 中间 CA 证书链
  fullchain.pem  # cert.pem + chain.pem(服务器使用)
  privkey.pem    # 私钥

# 实际文件位于 archive 目录,live 是符号链接
/etc/letsencrypt/archive/example.com/
  cert1.pem
  chain1.pem
  fullchain1.pem
  privkey1.pem
文件内容用途
cert.pem服务器证书单独使用(很少用到)
chain.pem中间 CA 证书用于 OCSP Stapling
fullchain.pem服务器证书 + 中间 CA用于 Nginx ssl_certificate
privkey.pem私钥用于 Nginx ssl_certificate_key

应用到 Nginx:

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

7. 自动续期配置

7.1 续期测试

# 不实际续期,仅执行测试
sudo certbot renew --dry-run

7.2 systemd Timer(推荐)

通过 snap 安装 Certbot 时会自动创建 systemd timer。

# 确认 Timer 状态
sudo systemctl status snap.certbot.renew.timer

# Timer 列表
sudo systemctl list-timers | grep certbot

自行创建时:

# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Certbot renewal timer

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot renewal service

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renew.timer

7.3 Crontab

# crontab -e
# 每天凌晨 2 点、下午 2 点尝试续期
0 2,14 * * * certbot renew --quiet --deploy-hook "systemctl reload nginx"

7.4 续期钩子(Hook)配置

# 续期前钩子
sudo mkdir -p /etc/letsencrypt/renewal-hooks/pre
cat > /etc/letsencrypt/renewal-hooks/pre/stop-service.sh << 'HOOKEOF'
#!/bin/bash
# Standalone 模式用:停止 Web 服务器
systemctl stop nginx
HOOKEOF
chmod +x /etc/letsencrypt/renewal-hooks/pre/stop-service.sh

# 续期后钩子
sudo mkdir -p /etc/letsencrypt/renewal-hooks/post
cat > /etc/letsencrypt/renewal-hooks/post/start-service.sh << 'HOOKEOF'
#!/bin/bash
# 重启 Web 服务器
systemctl start nginx
HOOKEOF
chmod +x /etc/letsencrypt/renewal-hooks/post/start-service.sh

# 部署钩子(仅在续期成功时执行)
sudo mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh << 'HOOKEOF'
#!/bin/bash
systemctl reload nginx
echo "Certificate renewed and nginx reloaded at $(date)" >> /var/log/certbot-deploy.log
HOOKEOF
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

8. Rate Limits 注意事项

Let's Encrypt 为防止滥用,适用以下 Rate Limits。

限制说明
Certificates per Registered Domain50 / 周同一域名每周 50 个
Duplicate Certificate5 / 周同一域名集合 5 个
Failed Validations5 / 小时 / 账号 / 主机名验证失败限制
New Orders300 / 3 小时新订单限制
Accounts per IP10 / 3 小时每个 IP 的账号创建限制

Staging 环境的利用:

# 测试时使用 Staging 服务器(Rate Limit 宽松得多)
sudo certbot certonly --standalone \
  --staging \
  -d example.com \
  --agree-tos \
  --email admin@example.com

9. 替代工具

9.1 acme.sh

用纯 Shell 脚本编写的 ACME 客户端。

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

# 签发证书
acme.sh --issue -d example.com -w /var/www/html

# 通配符(使用 DNS API)
acme.sh --issue \
  -d example.com \
  -d "*.example.com" \
  --dns dns_cf \
  --dnssleep 120

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

9.2 Caddy(自动 HTTPS)

Caddy 在 Web 服务器自身就自动处理 HTTPS。

# Caddyfile
example.com {
    root * /var/www/html
    file_server
}
# 仅此而已就能自动签发并续期 Let's Encrypt 证书!

9.3 lego

用 Go 编写的 ACME 客户端。

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

# HTTP 挑战
lego --email admin@example.com \
  --domains example.com \
  --http \
  run

# DNS 挑战(Cloudflare)
CLOUDFLARE_DNS_API_TOKEN=xxx \
lego --email admin@example.com \
  --domains "*.example.com" \
  --dns cloudflare \
  run

9.4 cert-manager(Kubernetes)

在 Kubernetes 环境中自动管理证书。

# ClusterIssuer 定义
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-key
    solvers:
      - http01:
          ingress:
            class: nginx
# 在 Ingress 中申请证书
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-app
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - app.example.com
      secretName: app-tls-secret
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: my-app
                port:
                  number: 80
# 直接创建 Certificate 资源
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: app-certificate
  namespace: default
spec:
  secretName: app-tls-secret
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - app.example.com
    - api.example.com
  renewBefore: 720h # 提前 30 天续期

10. 故障排查

10.1 常见错误与解决方法

Challenge 失败:

# 确认 HTTP-01 挑战文件是否可访问
curl -v http://yourdomain.com/.well-known/acme-challenge/test

# 确认防火墙(端口 80)
sudo ufw status
sudo iptables -L -n | grep 80

# 确认 DNS
dig +short yourdomain.com
nslookup yourdomain.com

证书续期失败:

# 确认续期状态
sudo certbot certificates

# 确认详细日志
sudo certbot renew --dry-run -v

# 确认日志文件
sudo cat /var/log/letsencrypt/letsencrypt.log

确认证书信息:

# 确认证书内容
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -text -noout

# 确认到期日
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -enddate -noout

# 确认远程服务器证书
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  openssl x509 -text -noout

# 验证证书链
openssl verify -CAfile /etc/letsencrypt/live/example.com/chain.pem \
  /etc/letsencrypt/live/example.com/cert.pem

10.2 证书吊销(Revoke)

# 吊销证书
sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem

# 删除证书(清理文件)
sudo certbot delete --cert-name example.com

11. 安全最佳实践

  1. 保护私钥:将 privkey.pem 文件权限设为 600,仅 root 可读
  2. 监控自动续期:续期失败时配置告警(邮件、Slack 等)
  3. 先用 Staging:新配置务必先在 Staging 环境测试
  4. 应用 HSTS:签发证书后配置 HSTS 头
  5. 监控证书透明度:在 crt.sh 等站点监控域名证书签发记录
  6. 备份:定期备份 /etc/letsencrypt 目录
# 确认私钥权限
ls -la /etc/letsencrypt/live/example.com/privkey.pem
# -rw------- 1 root root ... privkey.pem

# 查询证书透明度日志
# https://crt.sh/?q=example.com

12. 结论

Let's Encrypt 与 Certbot 已经成为免费 TLS 证书的标准。核心要点:

  • HTTP-01 挑战最简单,但通配符证书必须用 DNS-01
  • Certbot 的 Webroot 或 Nginx 插件最为实用
  • 自动续期用 systemd timercrontab 配置
  • Kubernetes 环境中 cert-manager 是标准方案
  • 理解 Rate Limits,测试时充分利用 Staging 服务器
  • 签发证书后务必优化 Nginx TLS 安全配置