第 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="[email protected]"
# 创建并加密备份
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 | 安装目标服务器 Certbot | certbot --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 bit | 2048 bit | 4096 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 = [email protected]
agree-tos = true
non-interactive = true
max-log-backups = 30
preferred-challenges = http
key-type = ecdsa
elliptic-curve = secp384r1
学习资源
推荐工具
总结
本教程涵盖了 Certbot 从入门到生产的完整知识体系:
- 基础概念: ACME 协议、Let’s Encrypt、证书类型
- 安装部署: Snap、包管理器、Docker 多种方式
- 验证方式: Standalone、Webroot、DNS 三种模式
- 服务器集成: Nginx、Apache 插件自动配置
- 自动化运维: 续期配置、钩子脚本、监控告警
- 高级主题: 多域名、通配符、Docker Compose、迁移策略
掌握这些知识,你就能够为任何规模的 Web 应用部署和管理 SSL/TLS 证书。