Certbot 续签失败排查:我踩过的几个 SSL 证书自动续期坑
上个月服务器迁移,旧机器上的证书到期了才想起来新机器上 cron 没配。等发现的时候网站已经跳不安全提示两天了。
后来花了几天把 certbot 的自动续期理顺,过程中踩了几个比较隐蔽的坑,记下来。
先搞明白 certbot renew 到底在干嘛
很多人以为 certbot 是个"设好就不管"的东西。实际上它干的事很简单:定时跑 certbot renew,检查证书还有三十天不到期的就重新签。就这么一件事。
问题出在执行环境上。
我第一次配的时候直接在 cron 里写了一行:
0 3 * * * certbot renew --quiet
看着没问题,第二天一看日志,certbot 命令找不到。环境变量没加载,cron 跑的 shell 和用户交互时用的 shell 根本不是一套东西。PATH 里根本没有 certbot 的安装位置。
后来改成绝对路径:
0 3 * * * /usr/bin/certbot renew --quiet
这步解决了"命令找不到"的问题,但还有更隐蔽的坑在后面。
坑一:Nginx 没重启,证书续了但网站还是旧的
certbot renew 默认只下载新证书,不会自动重载 Nginx。也就是说证书文件确实更新了,但 Nginx 还握着旧的文件描述符,用户看到的还是即将过期的那张。
解决办法有两个。一个是加 --deploy-hook:
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
另一个是在 certbot 的 Nginx 插件模式下,它会自动处理 reload。但前提是你是用 certbot --nginx 申请的证书,不是手动 copy 那种。
我一开始用的是手动 copy 的方式——把证书文件复制到指定目录,Nginx 配置里指向那个路径。这种方式 certbot 不知道你的文件在哪,自然也不会帮你 reload。后来干脆改用 certbot --nginx 插件模式,让 certbot 直接改 Nginx 配置,省心不少。
坑二:Webroot 模式和 Standalone 模式选错了
certbot 验证域名所有权有两种常见方式:webroot 和 standalone。
webroot 模式要求你的服务器上已经有一个能正常访问的 Nginx,certbot 会在 .well-known/acme-challenge/ 目录下放一个验证文件,Let's Encrypt 的服务器来请求这个文件确认你拥有这个域名。
standalone 模式则是 certbot 自己起一个临时 HTTP 服务器(默认 80 端口)来接收验证。
问题在于 standalone 需要独占 80 端口。如果你的 Nginx 已经在跑 80 端口了,standalone 起不来,续签就失败。
我第一次在 VPS 上测试的时候,Nginx 没开,用的 standalone,一切顺利。后来上了生产环境,Nginx 开着,certbot 还是按 standalone 的方式配的,续签直接报错:
Could not bind TCP port 80
改成 webroot 模式就好了:
certbot certonly --webroot -w /var/www/html -d example.com
webroot 模式下,certbot 往 Nginx 已有的站点根目录里放验证文件,不抢端口,不影响现有服务。
还有一个细节:Nginx 配置里得放行 .well-known 路径。如果你之前为了安全把所有隐藏目录都 deny 了,certbot 的验证请求会被 403 挡在外面,证书照样续不上。
我在 Nginx 配置里加了这么一段:
location ~ /\.well-known {
allow all;
}
别嫌短,这一段救过我两次续签失败。
坑三:多域名证书里漏了一个
certbot 支持一次性申请包含多个域名的证书(SAN 证书)。我一开始图省事,把主域名和 www 子域名一起申请:
certbot --nginx -d example.com -d www.example.com
后来加了个 API 域名 api.example.com,想着反正都在一台机器上,顺手加进去得了。结果续签的时候发现 certbot 只认最初申请时的那两个域名,新加的不在证书里。
这时候如果只续签,新域名拿不到有效证书。解决办法是重新生成一个包含所有域名的新证书:
certbot --nginx -d example.com -d www.example.com -d api.example.com
重新生成后会替换旧证书。关键是 cron 里的配置也得跟着改——如果 cron 跑的是固定参数的 renew,它可能不会自动包含新域名。
后来我干脆把域名列表单独写到配置文件里,cron 只跑 certbot renew,具体的域名参数放在 /etc/letsencrypt/cli.ini 里:
domains = example.com, www.example.com, api.example.com
这样加域名的时候只需要改 ini 文件,不用动 cron。
坑四:DNS-01 验证的自动化问题
对于某些场景,HTTP 验证(webroot/standalone)不够用。比如你的服务不在 80/443 端口,或者你想申请通配符证书 *.example.com。这时候得用 DNS-01 验证——在域名 DNS 里加一条 TXT 记录证明自己拥有这个域名。
问题在于 DNS-01 验证需要修改 DNS 记录,而大多数 DNS 服务商不提供命令行直接修改的接口。你得用 API,而且得提前拿到 API Token。
我用的是 Cloudflare。配起来也不难,装一个 cloudflare-dns 插件,把 API Token 写在配置文件里:
certbot certonly --dns-cloudflare --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini -d "*.example.com" -d example.com
但这里有个坑:API Token 的权限范围。Cloudflare 的 Token 可以限制只读、只写特定 zone,或者完全访问。我第一次随便生成了一个 Token,结果 certbot 提示权限不足,改不了 DNS 记录。
正确的做法是创建一个只针对目标 Zone 有 DNS 编辑权限的 Token,别给多余的权限。安全起见,权限最小化总是对的。
另外,通配符证书的有效期也是 90 天,和普通证书一样。很多人以为通配符证书能管一年,其实 Let's Encrypt 的政策对所有证书都是 90 天。
坑五:certbot 升级导致插件不兼容
这个坑比较玄学。有一段时间 certbot 自动升级到了新版本,而我用的 Nginx 插件版本还是旧的。升级后跑 certbot renew,提示插件版本不匹配,续签直接失败。
日志里能看到类似这样的报错:
The nginx plugin is not working; there may be problems with your existing configuration.
Error: nginx version mismatch
解决办法是定期更新 certbot 本身:
apt update && apt install --only-upgrade certbot
或者更稳妥的做法,把 certbot 的更新也写进 cron,和续签分开:
0 0 1 * * /usr/bin/apt update && /usr/bin/apt install --only-upgrade certbot -y
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
每月一号更新 certbot,每天凌晨三点续签。这样基本不会碰到版本不兼容的问题。
不过要注意,apt 更新可能会引入新的依赖或者改变行为。更新之后最好跑一次 dry-run 验证一下:
certbot renew --dry-run
dry-run 不会真的续签,但会模拟整个过程,告诉你哪里有问题。这个命令值得加到 cron 里每周跑一次,比等到证书快过期了才发现续不上要好得多。
我现在的配置
把上面这些坑踩完之后,我现在用的配置大概是这样的:
cron 任务:
# 每月更新 certbot
0 0 1 * * /usr/bin/apt update && /usr/bin/apt install --only-upgrade certbot -y 2>/dev/null
# 每周试运行续签,检查是否有问题
0 5 * * 0 /usr/bin/certbot renew --dry-run --quiet
# 每天凌晨三点实际续签
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
Nginx 配置里放行 challenge 路径:
location ~ /\.well-known {
allow all;
}
certbot 参数放在 ini 文件里:
/etc/letsencrypt/cli.ini:
webroot-path = /var/www/html
deploy-hook = systemctl reload nginx
email = admin@example.com
agree-tos = y
这样加域名、改路径都只需要改 ini 文件,不用动 cron。
最后说两句
certbot 本身不难用,难的是把它放到一个稳定的运行环境里。环境变量、Nginx 配置、插件版本、域名变更——任何一个环节出问题都会导致续签失败。
我的经验是:先用 dry-run 验证每一步,然后把所有配置集中到 ini 文件里管理,cron 只负责定时触发。出了问题先看日志,日志里没有就去 /var/log/letsencrypt/ 翻。
证书过期这件事,越早发现越好。别等到用户看到"不安全"的提示了才去查。
评论一下?