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

Certbot 证书自动化教程 / 第 12 章:最佳实践

第 12 章:最佳实践

12.1 部署架构最佳实践

推荐架构

                ┌─────────────────────────────┐
                │       负载均衡器 / CDN        │
                │    (SSL 终止层)              │
                └──────────┬──────────────────┘
                           │
              ┌────────────┼────────────┐
              │            │            │
         ┌────▼───┐   ┌───▼────┐   ┌───▼────┐
         │ Nginx  │   │ Nginx  │   │ Nginx  │
         │ Web 1  │   │ Web 2  │   │ Web 3  │
         └────────┘   └────────┘   └────────┘
              │            │            │
              └────────────┼────────────┘
                           │
                    ┌──────▼──────┐
                    │  证书同步层   │
                    │  (rsync/     │
                    │   Ansible)   │
                    └──────┬──────┘
                           │
                    ┌──────▼──────┐
                    │   Certbot   │
                    │  主控节点    │
                    └─────────────┘

分层部署策略

层级职责工具
证书管理层申请/续期证书Certbot
证书分发层同步证书到 Web 节点rsync / Ansible
SSL 终止层处理 HTTPS 请求Nginx / 负载均衡器
监控层证书到期监控Prometheus / Zabbix

12.2 安全加固

证书私钥安全

# 设置私钥权限
sudo chmod 600 /etc/letsencrypt/live/*/privkey.pem
sudo chown root:root /etc/letsencrypt/live/*/privkey.pem

# 确保 archive 目录权限
sudo chmod 700 /etc/letsencrypt/archive
sudo chmod 700 /etc/letsencrypt/live

凭证文件安全

# DNS 插件凭证权限
sudo chmod 600 /etc/letsencrypt/cloudflare/credentials.ini
sudo chown root:root /etc/letsencrypt/cloudflare/credentials.ini

# 定期轮换 API Token
# Cloudflare: Dashboard → API Tokens → Roll
# AWS: IAM → Access Keys → Create new / Delete old

SSL/TLS 安全配置

# 最大安全性的 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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

# 禁用不安全的协议和密码
# ❌ 已废弃的配置
# ssl_protocols SSLv3 TLSv1 TLSv1.1;
# ssl_ciphers RC4:DES:3DES;

安全响应头

# HSTS - 强制 HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# 防止点击劫持
add_header X-Frame-Options DENY always;

# 防止 MIME 类型嗅探
add_header X-Content-Type-Options nosniff always;

# XSS 保护
add_header X-XSS-Protection "1; mode=block" always;

# 引用策略
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# 内容安全策略
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline';" always;

# 权限策略
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

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;
resolver_timeout 5s;

12.3 监控与告警

证书到期监控脚本

#!/bin/bash
# /usr/local/bin/check-cert-expiry.sh
# 描述:检查所有证书的到期时间,发送告警

WARN_DAYS=30
CRITICAL_DAYS=7
LOG_FILE="/var/log/cert-monitor.log"
WEBHOOK_URL="${CERTBOT_WEBHOOK_URL:-}"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}

notify() {
    local level="$1"
    local domain="$2"
    local days="$3"
    local message="SSL 证书 ${level}: ${domain} 将在 ${days} 天后到期"

    log "$message"

    # 企业微信通知
    if [ -n "$WEBHOOK_URL" ]; then
        curl -s -X POST "$WEBHOOK_URL" \
          -H 'Content-Type: application/json' \
          -d "{
            \"msgtype\": \"markdown\",
            \"markdown\": {
              \"content\": \"## ${level} SSL 证书到期告警\n> 域名: ${domain}\n> 剩余天数: ${days}\n> 时间: $(date)\"
            }
          }" > /dev/null 2>&1
    fi
}

# 检查所有证书
check_all_certs() {
    local status=0

    for cert_dir in /etc/letsencrypt/live/*/; do
        [ -d "$cert_dir" ] || continue

        local domain=$(basename "$cert_dir")
        local cert_file="${cert_dir}cert.pem"

        [ -f "$cert_file" ] || continue

        # 获取证书到期时间
        local expiry_date=$(openssl x509 -in "$cert_file" -noout -enddate | cut -d= -f2)
        local expiry_epoch=$(date -d "$expiry_date" +%s)
        local now_epoch=$(date +%s)
        local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

        if [ "$days_left" -le 0 ]; then
            notify "已过期" "$domain" "$days_left"
            status=2
        elif [ "$days_left" -le "$CRITICAL_DAYS" ]; then
            notify "严重告警" "$domain" "$days_left"
            status=2
        elif [ "$days_left" -le "$WARN_DAYS" ]; then
            notify "警告" "$domain" "$days_left"
            [ "$status" -lt 1 ] && status=1
        else
            log "正常: ${domain} 还有 ${days_left} 天到期"
        fi
    done

    return $status
}

check_all_certs
exit $?

Prometheus 指标导出器

#!/bin/bash
# /usr/local/bin/certbot-metrics.sh
# 描述:导出证书指标供 Prometheus 抓取

METRICS_FILE="/var/lib/node_exporter/certbot.prom"

cat > "$METRICS_FILE" << 'HEADER'
# HELP certbot_certificate_expiry_days Days until certificate expiry
# TYPE certbot_certificate_expiry_days gauge
# HELP certbot_certificate_not_after Certificate expiry timestamp
# TYPE certbot_certificate_not_after gauge
HEADER

for cert_dir in /etc/letsencrypt/live/*/; do
    [ -d "$cert_dir" ] || continue
    local domain=$(basename "$cert_dir")
    local cert_file="${cert_dir}cert.pem"
    [ -f "$cert_file" ] || continue

    local expiry_date=$(openssl x509 -in "$cert_file" -noout -enddate | cut -d= -f2)
    local expiry_epoch=$(date -d "$expiry_date" +%s)
    local now_epoch=$(date +%s)
    local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    echo "certbot_certificate_expiry_days{domain=\"${domain}\"} ${days_left}" >> "$METRICS_FILE"
    echo "certbot_certificate_not_after{domain=\"${domain}\"} ${expiry_epoch}" >> "$METRICS_FILE"
done
# 每 5 分钟更新指标
*/5 * * * * /usr/local/bin/certbot-metrics.sh

Prometheus 告警规则

# prometheus-rules.yml
groups:
  - name: certbot_alerts
    rules:
      - alert: CertificateExpiringSoon
        expr: certbot_certificate_expiry_days < 30
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "SSL 证书即将过期"
          description: "域名 {{ $labels.domain }} 的证书将在 {{ $value }} 天后过期"

      - alert: CertificateExpiryCritical
        expr: certbot_certificate_expiry_days < 7
        for: 1h
        labels:
          severity: critical
        annotations:
          summary: "SSL 证书即将过期(严重)"
          description: "域名 {{ $labels.domain }} 的证书将在 {{ $value }} 天后过期"

      - alert: CertificateExpired
        expr: certbot_certificate_expiry_days < 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "SSL 证书已过期"
          description: "域名 {{ $labels.domain }} 的证书已过期 {{ $value }} 天"

Zabbix 监控

#!/bin/bash
# /usr/local/bin/zabbix-cert-check.sh
# 描述:Zabbix 自定义检查项

DOMAIN="$1"
CERT_FILE="/etc/letsencrypt/live/${DOMAIN}/cert.pem"

if [ ! -f "$CERT_FILE" ]; then
    echo "0"
    exit 1
fi

EXPIRY_DATE=$(openssl x509 -in "$CERT_FILE" -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

echo "$DAYS_LEFT"
# Zabbix Agent 配置
# UserParameter=certbot.expiry[*],/usr/local/bin/zabbix-cert-check.sh $1

12.4 备份与恢复

需要备份的文件

路径内容重要性
/etc/letsencrypt/live/当前有效的证书符号链接
/etc/letsencrypt/archive/所有证书历史文件
/etc/letsencrypt/renewal/续期配置文件
/etc/letsencrypt/accounts/ACME 账户信息关键
/etc/letsencrypt/cli.ini全局配置
DNS 凭证文件Cloudflare/Route53 凭证

警告: 如果丢失了 ACME 账户密钥(/etc/letsencrypt/accounts/),将无法续期已申请的证书,只能重新申请新证书。

备份脚本

#!/bin/bash
# /usr/local/bin/certbot-backup.sh
# 描述:备份 Certbot 证书和配置

BACKUP_DIR="/backup/certbot"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/certbot-backup-${BACKUP_DATE}.tar.gz"
KEEP_DAYS=90

mkdir -p "$BACKUP_DIR"

# 创建备份
tar -czf "$BACKUP_FILE" \
    /etc/letsencrypt/ \
    /var/log/letsencrypt/ \
    2>/dev/null

if [ $? -eq 0 ]; then
    echo "[$(date)] Backup created: ${BACKUP_FILE}"
    echo "[$(date)] Size: $(du -sh "$BACKUP_FILE" | cut -f1)"
else
    echo "[$(date)] ERROR: Backup failed" >&2
    exit 1
fi

# 清理旧备份
find "$BACKUP_DIR" -name "certbot-backup-*.tar.gz" -mtime +${KEEP_DAYS} -delete
echo "[$(date)] Cleaned up backups older than ${KEEP_DAYS} days"

# 验证备份完整性
echo "[$(date)] Verifying backup..."
tar -tzf "$BACKUP_FILE" > /dev/null 2>&1
if [ $? -eq 0 ]; then
    echo "[$(date)] Backup verification successful"
else
    echo "[$(date)] ERROR: Backup verification failed" >&2
    exit 1
fi

加密备份

#!/bin/bash
# /usr/local/bin/certbot-backup-encrypted.sh

BACKUP_DIR="/backup/certbot"
BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
GPG_RECIPIENT="admin@example.com"

# 创建并加密备份
tar -czf - /etc/letsencrypt/ | \
    gpg --encrypt --recipient "$GPG_RECIPIENT" \
    --output "${BACKUP_DIR}/certbot-backup-${BACKUP_DATE}.tar.gz.gpg"

echo "Encrypted backup: ${BACKUP_DIR}/certbot-backup-${BACKUP_DATE}.tar.gz.gpg"

恢复流程

#!/bin/bash
# /usr/local/bin/certbot-restore.sh
# 描述:从备份恢复 Certbot 配置

BACKUP_FILE="$1"

if [ -z "$BACKUP_FILE" ]; then
    echo "Usage: $0 <backup-file>"
    exit 1
fi

if [ ! -f "$BACKUP_FILE" ]; then
    echo "Error: Backup file not found: $BACKUP_FILE"
    exit 1
fi

echo "This will overwrite all current Certbot configuration!"
read -p "Continue? (y/N): " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
    exit 0
fi

# 停止 Web 服务器
echo "Stopping web server..."
systemctl stop nginx

# 备份当前配置
echo "Backing up current configuration..."
mv /etc/letsencrypt /etc/letsencrypt.bak.$(date +%s)

# 恢复备份
echo "Restoring from backup..."
tar -xzf "$BACKUP_FILE" -C /

# 验证证书
echo "Verifying certificates..."
certbot certificates

# 启动 Web 服务器
echo "Starting web server..."
systemctl start nginx

echo "Restore complete!"

备份到远程存储

#!/bin/bash
# 使用 rclone 备份到云存储

BACKUP_DATE=$(date +%Y%m%d)
TEMP_DIR="/tmp/certbot-backup-${BACKUP_DATE}"

mkdir -p "$TEMP_DIR"
tar -czf "${TEMP_DIR}/certbot-backup.tar.gz" /etc/letsencrypt/

# 上传到 S3
aws s3 cp "${TEMP_DIR}/certbot-backup.tar.gz" \
    "s3://my-backup-bucket/certbot/${BACKUP_DATE}/certbot-backup.tar.gz"

# 或使用 rclone
rclone copy "${TEMP_DIR}/certbot-backup.tar.gz" \
    remote:certbot-backups/${BACKUP_DATE}/

# 清理
rm -rf "$TEMP_DIR"

12.5 迁移策略

服务器迁移

场景一:迁移到新服务器

#!/bin/bash
# 源服务器操作
# 1. 备份 Certbot 配置
sudo tar -czf /tmp/certbot-migration.tar.gz /etc/letsencrypt/

# 2. 传输到新服务器
scp /tmp/certbot-migration.tar.gz root@new-server:/tmp/

# 目标服务器操作
# 3. 安装 Certbot
sudo snap install --classic certbot

# 4. 恢复配置
sudo tar -xzf /tmp/certbot-migration.tar.gz -C /

# 5. 验证
sudo certbot certificates

# 6. 配置 Web 服务器
# ...(根据实际情况配置 Nginx/Apache)

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

场景二:从其他 ACME 客户端迁移到 Certbot

# 如果原客户端是 acme.sh
# 1. 查看现有证书
~/.acme.sh/acme.sh --list

# 2. 将证书复制到 Certbot 目录结构
DOMAIN="example.com"
sudo mkdir -p /etc/letsencrypt/live/$DOMAIN/

sudo cp ~/.acme.sh/$DOMAIN/fullchain.cer /etc/letsencrypt/live/$DOMAIN/fullchain.pem
sudo cp ~/.acme.sh/$DOMAIN/$DOMAIN.key /etc/letsencrypt/live/$DOMAIN/privkey.pem
sudo cp ~/.acme.sh/$DOMAIN/ca.cer /etc/letsencrypt/live/$DOMAIN/chain.pem
sudo cp ~/.acme.sh/$DOMAIN/$DOMAIN.cer /etc/letsencrypt/live/$DOMAIN/cert.pem

# 3. 创建续期配置
# (手动或重新申请)

场景三:迁移域名到新的 DNS 服务商

# 1. 更新 DNS 插件凭证
# 如果从 Cloudflare 迁移到 Route53
pip install certbot-dns-route53
aws configure

# 2. 更新续期配置
sudo vim /etc/letsencrypt/renewal/example.com.conf
# 修改 authenticator 和相关参数

# 3. 测试新配置
sudo certbot renew --dry-run --cert-name example.com

证书迁移检查清单

步骤操作验证
1备份源服务器证书tar -czf 创建备份
2安装目标服务器 Certbotcertbot --version
3传输证书文件scp / rsync
4恢复证书目录结构certbot certificates
5配置 Web 服务器nginx -t / apache2ctl configtest
6更新 DNS(如需要)dig 验证解析
7测试 HTTPS 访问curl -I https://example.com
8测试续期certbot renew --dry-run
9配置自动续期systemctl status certbot.timer
10撤销源服务器证书(可选)certbot revoke

12.6 故障排除清单

证书申请失败

# 1. 检查域名解析
dig +short example.com A

# 2. 检查 80 端口
curl -I http://example.com/.well-known/acme-challenge/test

# 3. 检查防火墙
sudo ufw status
sudo iptables -L -n

# 4. 使用 staging 测试
sudo certbot certonly --staging -d example.com

# 5. 查看详细日志
sudo cat /var/log/letsencrypt/letsencrypt.log | tail -50

# 6. 检查速率限制
# https://crt.sh/?q=example.com

续期失败

# 1. 检查证书状态
sudo certbot certificates

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

# 3. 检查续期配置
cat /etc/letsencrypt/renewal/example.com.conf

# 4. 检查 Web 服务器
sudo systemctl status nginx
sudo nginx -t

# 5. 检查钩子脚本
sudo /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

# 6. 检查定时器
sudo systemctl status certbot.timer
sudo journalctl -u certbot.timer --since "1 week ago"

HTTPS 配置问题

# 1. 检查证书有效性
openssl s_client -connect example.com:443 -servername example.com

# 2. 检查证书链
openssl s_client -connect example.com:443 -servername example.com -showcerts

# 3. 检查证书域名
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -text | grep "DNS:"

# 4. 检查 Nginx SSL 配置
sudo nginx -T | grep ssl

# 5. 在线测试
# https://www.ssllabs.com/ssltest/
# https://www.sslshopper.com/ssl-checker.html

12.7 性能优化

TLS 握手优化

# 启用 TLS Session 缓存
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;  # 安全考虑建议关闭

# 启用 OCSP Stapling(减少客户端查询 OCSP 的延迟)
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;

# 使用 ECDSA 密钥(比 RSA 更快)
# Certbot 默认使用 ECDSA
# ssl_certificate_key 中的密钥类型
openssl ec -in /etc/letsencrypt/live/example.com/privkey.pem -noout -text | head -2

ECDSA vs RSA 性能对比

特性ECDSA (P-256)RSA (2048-bit)RSA (4096-bit)
密钥大小256 bit2048 bit4096 bit
TLS 握手速度更快基准更慢
安全性等效基准更高
浏览器兼容性现代浏览器所有所有
Certbot 默认✅ 是

启用 HTTP/2

server {
    listen 443 ssl http2;
    # ...
}

启用 HTTP/3 (QUIC)

server {
    listen 443 ssl;
    listen 443 quic reuseport;

    add_header Alt-Svc 'h3=":443"; ma=86400';
    # ...
}

12.8 运维文档模板

证书清单模板

| 域名 | 证书名 | 类型 | 验证方式 | 到期时间 | 负责人 |
|------|--------|------|----------|----------|--------|
| example.com | example.com | SAN | Nginx | 2025-08-10 | 运维A |
| *.blog.com | blog.com | Wildcard | DNS-Cloudflare | 2025-07-15 | 运维B |
| api.partner.com | partner-api | Single | Webroot | 2025-06-01 | 开发C |

故障处理 SOP

## SSL 证书过期处理流程

### 1. 确认证书状态
```bash
sudo certbot certificates

2. 手动续期

sudo certbot renew --force-renewal --cert-name example.com

3. 如果续期失败

  • 检查日志: /var/log/letsencrypt/letsencrypt.log
  • 检查 80 端口: curl http://example.com
  • 检查 DNS: dig example.com

4. 如果仍然失败

5. 确认修复

sudo certbot renew --dry-run
curl -I https://example.com

## 12.9 常见问题 FAQ

| 问题 | 解答 |
|------|------|
| Let's Encrypt 证书安全吗? | 是的,与商业 DV 证书使用相同的加密标准 |
| 可以用 Let's Encrypt 做商业网站吗? | 可以,但仅限 DV 验证,不支持 OV/EV |
| 证书过期后多久可以续期? | 随时可以续期,建议在到期前 30 天 |
| 一个域名可以有多少个证书? | 每周 50 个(主域名限制) |
| 证书可以在多台服务器使用吗? | 可以,复制证书文件即可 |
| 如何撤销已签发的证书? | `certbot revoke --cert-name example.com` |
| Certbot 支持 Windows 吗? | 官方不支持,推荐在 Linux 上使用 |
| 如何切换到其他 CA? | 重新申请证书并更新 Web 服务器配置 |

## 12.10 进阶资源

### Certbot 高级配置

```bash
# /etc/letsencrypt/cli.ini 全局配置示例
email = admin@example.com
agree-tos = true
non-interactive = true
max-log-backups = 30
preferred-challenges = http
key-type = ecdsa
elliptic-curve = secp384r1

学习资源

资源链接
Certbot 官方文档https://certbot.eff.org/docs
Let’s Encrypt 文档https://letsencrypt.org/docs
ACME 协议 (RFC 8555)https://tools.ietf.org/html/rfc8555
Mozilla SSL 配置生成器https://ssl-config.mozilla.org/
SSL Labs 测试https://www.ssllabs.com/ssltest/
Certificate Transparencyhttps://crt.sh/
Let’s Encrypt 社区https://community.letsencrypt.org/

推荐工具

工具用途链接
acme.sh纯 Shell ACME 客户端https://github.com/acmesh-official/acme.sh
legoGo 语言 ACME 客户端https://github.com/go-acme/lego
Caddy内置 HTTPS 的 Web 服务器https://caddyserver.com
Traefik云原生反向代理https://traefik.io
cert-managerKubernetes 证书管理https://cert-manager.io

总结

本教程涵盖了 Certbot 从入门到生产的完整知识体系:

  1. 基础概念: ACME 协议、Let’s Encrypt、证书类型
  2. 安装部署: Snap、包管理器、Docker 多种方式
  3. 验证方式: Standalone、Webroot、DNS 三种模式
  4. 服务器集成: Nginx、Apache 插件自动配置
  5. 自动化运维: 续期配置、钩子脚本、监控告警
  6. 高级主题: 多域名、通配符、Docker Compose、迁移策略

掌握这些知识,你就能够为任何规模的 Web 应用部署和管理 SSL/TLS 证书。