Let's Encrypt SSL自动化:从踩坑到躺平
发布日期: 2026/07/20 阅读总量: 0

一、真实场景:凌晨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.0acme.sh 3.0.7
安装方式apt install / snapcurl脚本安装
验证方式HTTP-01, DNS-01HTTP-01, DNS-01, TLS-ALPN-01
DNS API支持有限(需插件)150+ DNS厂商
自动续签systemd timer / croncron
证书格式PEMPEM, PFX, P12等
内存占用(空闲)12MB8MB
首次配置耗时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(包含完整脚本和配置模板)。