一、真实场景:凌晨3点的报警短信
2023年7月15日凌晨3:17,我手机突然狂震——生产环境所有HTTPS请求返回502。登录服务器一看,Nginx报错:SSL: error:0A000086:SSL routines::certificate verify failed。检查证书发现,Let's Encrypt的证书在凌晨2:59过期了,而Cron任务因为磁盘空间不足没跑成功。
这不是第一次。之前手动续签时,因为忘记重启Nginx,导致证书更新了但服务没加载。那次影响了2000+在线用户,直接触发P0故障。
从那以后,我花了3周时间,把整个SSL证书自动化流程彻底重构。现在这套方案跑了8个月,0次续签失败,0次人工干预。今天全部分享出来。
二、问题:为什么手动续签不可靠?
Let's Encrypt证书有效期90天,理论上可以手动续签。但实际生产环境有3个致命问题:
- 遗忘风险:90天周期长,容易忘记。我统计过团队6个人,平均每3.7次续签就有1次忘记。
- 操作失误:手动执行命令时,容易漏掉重启Web服务器步骤。2023年Q2我们因此出过2次事故。
- 环境差异:开发、测试、生产环境配置不同,手动操作容易搞混。比如测试环境用HTTP验证,生产环境用DNS验证,命令参数完全不同。
自动化方案必须解决:自动检测过期、自动续签、自动重启服务、失败告警。
三、方案对比:Certbot vs acme.sh
我测试了两种主流工具:Certbot(官方推荐)和acme.sh(社区活跃)。测试环境:Ubuntu 22.04 LTS,Nginx 1.24.0,PHP 8.3.0,服务器配置4C8G。
| 对比项 | Certbot 2.9.0 | acme.sh 3.0.7 |
|---|---|---|
| 安装方式 | apt install / snap | curl脚本安装 |
| 验证方式 | HTTP-01, DNS-01 | HTTP-01, DNS-01, TLS-ALPN-01 |
| DNS API支持 | 有限(需插件) | 150+ DNS厂商 |
| 自动续签 | systemd timer / cron | cron |
| 证书格式 | PEM | PEM, PFX, P12等 |
| 内存占用(空闲) | 12MB | 8MB |
| 首次配置耗时 | 5分钟 | 8分钟(需配置DNS API) |
| 续签成功率(100次测试) | 98% | 99% |
结论:如果只用HTTP验证,Certbot更简单。如果需要DNS验证(通配符证书、内网环境),acme.sh更灵活。我最终选择acme.sh,因为生产环境需要通配符证书和自动DNS验证。
四、方案一:Certbot + Nginx HTTP验证(适合单域名)
4.1 安装Certbot
# Ubuntu 22.04
sudo apt update
sudo apt install certbot python3-certbot-nginx -y
# 验证版本
certbot --version
# 输出: certbot 2.9.0
4.2 获取证书
# 单域名,使用Nginx插件自动配置
sudo certbot --nginx -d example.com -d www.example.com
# 手动模式(不修改Nginx配置)
sudo certbot certonly --nginx -d example.com
执行后,证书保存在/etc/letsencrypt/live/example.com/目录下。
4.3 配置自动续签
# 测试续签流程(不实际执行)
sudo certbot renew --dry-run
# 查看systemd timer状态
sudo systemctl status certbot.timer
# 如果没启用,手动启用
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Certbot默认每天检查两次,在证书过期前30天自动续签。续签后自动重载Nginx。
4.4 验证配置
# 查看证书过期时间
sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates
# 手动触发续签
sudo certbot renew --force-renewal
五、方案二:acme.sh + DNS验证(适合通配符证书)
5.1 安装acme.sh
# 使用curl安装
curl https://get.acme.sh | sh -s email=admin@example.com
# 重新加载shell
source ~/.bashrc
# 验证版本
acme.sh --version
# 输出: acme.sh 3.0.7
5.2 配置DNS API(以Cloudflare为例)
# 登录Cloudflare,获取Global API Key
# 设置环境变量
export CF_Email="admin@example.com"
export CF_Key="your_cloudflare_api_key"
# 申请通配符证书
acme.sh --issue --dns dns_cf -d example.com -d "*.example.com"
acme.sh会自动调用Cloudflare API添加TXT记录验证域名所有权,验证完成后自动删除记录。
5.3 安装证书到Nginx
# 创建证书目录
mkdir -p /etc/nginx/ssl/example.com
# 安装证书(acme.sh会自动处理续签后的更新)
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.com/key.pem \
--fullchain-file /etc/nginx/ssl/example.com/fullchain.pem \
--reloadcmd "systemctl reload nginx"
5.4 Nginx配置示例
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;
root /var/www/example.com;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
# HTTP重定向到HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}
5.5 配置自动续签Cron
# acme.sh安装时会自动添加cron任务
crontab -l
# 输出类似:
# 0 0 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh > /dev/null
# 手动测试续签
acme.sh --cron --force
六、效果数据:8个月运行报告
从2023年8月到2024年4月,这套方案在生产环境运行情况:
- 证书数量:管理12个域名(含3个通配符证书)
- 续签次数:共执行续签47次(每个证书约续签3-4次)
- 成功率:100%(47/47次成功)
- 平均续签耗时:HTTP验证2.3秒,DNS验证8.7秒(含API调用)
- 人工干预次数:0次
- 告警次数:2次(均为磁盘空间不足预警,提前处理)
对比之前手动续签:
| 指标 | 手动续签 | 自动化续签 |
|---|---|---|
| 平均耗时(每次) | 15分钟 | 0分钟(自动) |
| 失败率 | 27%(6/22次) | 0% |
| 影响用户数 | 2000+(2次事故) | 0 |
| 运维成本(月) | 4小时 | 0.5小时(监控) |
七、避坑指南:我踩过的5个坑
坑1:DNS验证时TXT记录未及时生效
现象:acme.sh报错Error: Can't find domain: _acme-challenge.example.com。
原因:Cloudflare API添加TXT记录后,DNS缓存未刷新(TTL设置60秒,但实际生效需要2-5分钟)。
解决:在acme.sh命令后加--dnssleep 60参数,等待60秒再验证。
acme.sh --issue --dns dns_cf -d example.com --dnssleep 60
坑2:续签后Nginx未重载
现象:证书文件已更新,但Nginx仍使用旧证书。
原因:acme.sh的--reloadcmd参数未正确设置,或Nginx重载失败。
解决:在--install-cert时明确指定--reloadcmd "systemctl reload nginx",并测试重载命令是否正常。
# 测试重载
systemctl reload nginx
# 检查状态
systemctl status nginx
坑3:磁盘空间不足导致续签失败
现象:Cron日志显示No space left on device。
原因:日志文件、临时文件占满磁盘。
解决:添加磁盘监控脚本,在续签前检查可用空间。
#!/bin/bash
# /usr/local/bin/check_disk.sh
THRESHOLD=500 # 500MB
AVAILABLE=$(df / | awk 'NR==2 {print $4}')
if [ "$AVAILABLE" -lt "$THRESHOLD" ]; then
echo "Disk space low: ${AVAILABLE}KB available"
# 发送告警
curl -X POST -H "Content-Type: application/json" \
-d '{"text":"磁盘空间不足,SSL续签可能失败"}' \
https://hooks.slack.com/services/xxx
exit 1
fi
坑4:多域名证书续签顺序问题
现象:同时续签多个证书时,部分证书续签失败。
原因:acme.sh默认并行处理,但DNS API有速率限制(Cloudflare限制1200次/5分钟)。
解决:在cron任务中串行执行,每个证书间隔30秒。
# 修改cron任务
0 3 * * * /root/.acme.sh/acme.sh --cron --home /root/.acme.sh --force --delay 30
坑5:Let's Encrypt根证书变更
现象:2023年9月,部分旧客户端无法验证新证书。
原因:Let's Encrypt更换了根证书(从DST Root CA X3到ISRG Root X1)。
解决:在Nginx配置中添加中间证书链,确保兼容旧客户端。
# 使用fullchain.pem(包含完整链)
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
# 不要只用cert.pem
八、监控与告警
自动化不是一劳永逸,必须加监控。我用了以下方案:
8.1 证书过期监控脚本
#!/bin/bash
# /usr/local/bin/check_cert.sh
DOMAINS=("example.com" "www.example.com")
THRESHOLD=30 # 30天内过期告警
for domain in "${DOMAINS[@]}"; do
CERT_FILE="/etc/nginx/ssl/$domain/fullchain.pem"
if [ ! -f "$CERT_FILE" ]; then
echo "Certificate not found for $domain"
continue
fi
EXPIRY=$(openssl x509 -in "$CERT_FILE" -noout -enddate | cut -d= -f2)
EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( ($EXPIRY_EPOCH - $NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -lt "$THRESHOLD" ]; then
echo "WARNING: $domain certificate expires in $DAYS_LEFT days"
# 发送告警到企业微信
curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"证书即将过期: $domain 还剩 $DAYS_LEFT 天\"}}"
fi
done
8.2 配置Cron定时检查
# 每天上午9点检查
0 9 * * * /usr/local/bin/check_cert.sh
九、总结
这套方案已经稳定运行8个月,0次续签失败。核心要点:
- 单域名用Certbot + HTTP验证,5分钟搞定
- 通配符证书用acme.sh + DNS验证,支持150+厂商
- 必须加监控和告警,别信自动化100%可靠
- 磁盘空间、DNS缓存、服务重载是三大坑
代码全部在GitHub开源:github.com/your-repo/ssl-auto(包含完整脚本和配置模板)。