ACME 客户端与自动化
Certbot 续期钩子:续期后自动重载服务
Certbot 续期后要靠钩子重载服务:deploy hook 只在证书续期成功后运行,适合放重载命令;钩子可以保存在单张证书的续期配置里,也可以放进 /etc/letsencrypt/renewal-hooks/ 公共目录。
适用于 Linux 上的 Certbot 4.1 及以上,钩子机制在很多版本里都没有变化。
前提条件
配置钩子需要一张已有的 Certbot 证书和 root 权限。
- 用 snap 或 pip 安装的 Certbot,至少有一张证书(
sudo certbot certificates)。 - 清楚哪些服务在用这张证书,以及让它加载新文件的命令(
systemctl reload nginx、systemctl restart postfix等)。 - 用 standalone 方式的证书,要知道续期时哪个服务占着 80 端口。
第一步:选对钩子类型
重载服务用 deploy hook;pre 和 post hook 用来在 standalone 模式下停止和启动占用 80 端口的服务。三种钩子的运行时机:
| 钩子 | 运行时机 | 常见用途 |
|---|---|---|
--pre-hook |
一次 renew 中,尝试续期前运行一次 |
standalone 模式下先停掉占用 80 端口的服务 |
--deploy-hook |
每张续期成功的证书各运行一次 | 重载 Nginx、复制文件、重启邮件服务 |
--post-hook |
一次 renew 中,所有续期尝试结束后运行一次 |
启动 pre-hook 停掉的服务 |
没有证书到期时,三种钩子都不会运行。deploy hook 能读取两个环境变量:RENEWED_LINEAGE(如 /etc/letsencrypt/live/example.com)和 RENEWED_DOMAINS(空格分隔的域名)。
第二步:给单张证书保存钩子
给已有证书加 deploy hook,不用重新签发:
sudo certbot reconfigure --cert-name example.com --deploy-hook "systemctl reload nginx"
grep -E 'hook' /etc/letsencrypt/renewal/example.com.conf
续期配置文件里会出现 renew_hook = systemctl reload nginx。也可以在第一次申请时直接带上,例如 sudo certbot certonly --webroot -w /var/www/html -d example.com --deploy-hook "systemctl reload nginx"。
standalone 方式、80 端口平时被 Nginx 占用的证书:
sudo certbot reconfigure --cert-name example.com \
--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"
这样续期时网站会短暂中断;Web 服务器已经在运行时,改用 webroot 方式更好。
第三步:用钩子目录放公共逻辑
下面三个目录里的脚本对所有证书生效,按文件名字母顺序执行。Certbot 3.2.0 之前只在 certbot renew 时运行,3.2.0 起用 certonly 申请新证书时也会运行:
/etc/letsencrypt/renewal-hooks/pre/
/etc/letsencrypt/renewal-hooks/deploy/
/etc/letsencrypt/renewal-hooks/post/
按续期的域名决定重载哪些服务:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
case "$RENEWED_DOMAINS" in
*mail.example.com*) systemctl restart postfix dovecot ;;
esac
systemctl reload nginx
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh
脚本必须有执行权限,失败时返回非零退出码,错误才会记录到 /var/log/letsencrypt/letsencrypt.log。
第四步:给读不到 /etc/letsencrypt 的服务复制证书
以普通用户运行的服务读不了 /etc/letsencrypt/live,在 deploy hook 里按正确的属主和权限复制,先写临时文件再替换:
sudo tee /etc/letsencrypt/renewal-hooks/deploy/20-myapp.sh > /dev/null <<'EOF'
#!/bin/sh
set -e
[ "$(basename "$RENEWED_LINEAGE")" = "example.com" ] || exit 0
dest=/etc/myapp/tls
install -d -o myapp -g myapp -m 700 "$dest"
install -o myapp -g myapp -m 644 "$RENEWED_LINEAGE/fullchain.pem" "$dest/fullchain.pem.new"
install -o myapp -g myapp -m 600 "$RENEWED_LINEAGE/privkey.pem" "$dest/privkey.pem.new"
mv "$dest/fullchain.pem.new" "$dest/fullchain.pem"
mv "$dest/privkey.pem.new" "$dest/privkey.pem"
systemctl reload myapp
EOF
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/20-myapp.sh
验证
certbot renew --dry-run --run-deploy-hooks 会在测试环境把续期流程完整跑一遍,包括 deploy hook:
sudo certbot renew --dry-run --run-deploy-hooks
sudo tail -n 50 /var/log/letsencrypt/letsencrypt.log
再用 SSL 证书检查 确认客户端实际看到的证书,或在终端运行:
curl ssl.sh/example.com
下一次续期后,这里显示的到期时间应该往后推。
自动续期
钩子只有在续期运行时才起作用。snap 版自带 snap.certbot.renew.timer,每天运行两次 certbot renew;pip 安装的要按官方说明加 cron。Certbot 4.1 及以上按 Let's Encrypt 的 ARI 窗口续期,续期时间可能早于常规的剩余三分之一有效期,所以钩子要能在任何时候安全运行。
systemctl list-timers | grep certbot
Let's Encrypt 有效期 2027 年 2 月 10 日缩短到 64 天、2028 年 2 月 16 日缩短到 45 天,钩子运行会越来越频繁,必须保证可以重复执行。把域名加到 ssl.sh 监控(开发中),钩子失效时也能及时发现。
常见问题排查
- Hook command returned error code 1:用 root 带上相同的环境变量手动运行,例如
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/example.com /etc/letsencrypt/renewal-hooks/deploy/10-reload.sh。 - 目录里的钩子没运行:文件没有执行权限,或文件名含有会被跳过的字符,用
chmod 755和普通文件名。 - 钩子运行了,线上还是旧证书:服务在复制文件之前就重载了,或者读的是另一个路径,用
curl ssl.sh/example.com检查线上证书。 - standalone 续期报 80 端口被占用:加 pre/post 钩子或改用 webroot,其他 80 端口原因见 certbot 连接超时。
- 续期后 nginx -t 报 key values mismatch:钩子复制了不配对的文件,见 nginx key values mismatch。
常见问题
renew_hook 和 deploy_hook 有什么区别?
是同一个东西。老版本 Certbot 的参数叫 --renew-hook,后来改名为 --deploy-hook,但续期配置文件里仍然用 renew_hook 这个键保存。
用 certbot --nginx 申请的证书还需要钩子吗?
不需要。Nginx/Apache 安装器插件续期后会自动重载 Web 服务器。用 certonly 申请,或者邮件服务、HAProxy、Java 应用也用这张证书时,才需要 deploy hook。
钩子能把证书同步到其他服务器或云上吗?
可以。deploy hook 里用专用 SSH 密钥 rsync/scp 到其他服务器再重载远端服务;要上传到阿里云、腾讯云,可以调用云厂商的命令行工具或 API。注意先写临时文件再移动,失败时返回非零退出码,错误才会记进 Certbot 日志。