Let's Encrypt 证书自动续期实战:certbot 配置、hooks 与五个必踩的坑

<h2>为什么你的证书总是"莫名过期"</h2>
<p>几乎每个个人站长都经历过这样的场景:早上打开网站,浏览器弹出刺眼的红色警告"您的连接不是私密连接",一看日历才发现——SSL 证书昨天过期了。更尴尬的是,明明当初装过 certbot,也配过自动续期,为什么它还是没有续上?</p>
<p>这篇文章不讲空泛的概念,而是把 Let's Encrypt 证书自动续期这件事从头到尾拆开:ACME 协议到底在做什么、certbot 的续期机制怎么运作、nginx 环境下的 hooks 怎么写、以及那些真正会让续期悄悄失败的坑。</p>

<h2>一、先搞懂 ACME 协议在做三件事</h2>
<p>Let's Encrypt 使用的是 ACME 协议(Automatic Certificate Management Environment)。不管用 certbot、acme.sh 还是 lego,本质流程都是三步:</p>
<p><strong>1. 账户注册</strong>:客户端生成一对账户密钥,向 ACME 服务器注册。这个账户密钥和你的域名证书私钥是两回事,后面"账户被吊销"的坑就出在这里。</p>
<p><strong>2. 域名所有权验证</strong>:这是最关键的一步。ACME 服务器会给你一个随机 token,要求你在指定位置证明"这个域名是你的"。常用两种方式:</p>
<ul>
<li><strong>HTTP-01</strong>:在 <code>http://你的域名/.well-known/acme-challenge/&;lt;token&gt;</code> 放一个特定内容的文件,ACME 服务器从公网来抓。</li>
<li><strong>DNS-01</strong>:在 <code>_acme-challenge.你的域名</code> 加一条 TXT 记录。适合泛域名(wildcard)证书。</li>
</ul>
<p><strong>3. 签发证书</strong>:验证通过后,客户端生成证书私钥并提交 CSR,ACME 服务器签发证书返回。</p>
<p>理解了这三步,你就明白了一个核心事实:<strong>自动续期失败,90% 的概率是卡在第 2 步——域名验证</strong>。因为续期时 ACME 服务器依然要重新验证一次所有权,而不是因为你以前验证过就免检。</p>

<h2>二、certbot 的续期机制:两个 systemd 单元</h2>
<p>用 <code>apt install certbot python3-certbot-nginx</code> 装的 certbot,会在系统里注册两个东西:</p>
<pre><code>systemctl list-timers | grep certbot

通常你会看到:

certbot.timer ... next: 今天 08:23

certbot.service 静态</code></pre>

<p>certbot 自带一个 <code>certbot.timer</code>,默认<strong>每天跑两次</strong>——随机选一个时间(避免全世界的服务器同时冲击 Let's Encrypt 的接口),执行 <code>certbot renew</code>。</p>
<p>注意 <code>certbot renew</code> 和 <code>certbot certonly</code> 的区别:<code>renew</code> 会遍历 <code>/etc/letsencrypt/renewal/</code> 下每一个 <code>*.conf</code>,逐个判断"这张证书是否在 30 天内到期"。只有在 30 天内才真正去续。所以看到 <code>certbot renew</code> 输出 "not due for renewal" 是正常的,不代表出错。</p>
<p>手动模拟一次试运行,这是上线后必做的检查:</p>
<pre><code>certbot renew --dry-run</code></pre>
<p><code>--dry-run</code> 会使用 Let's Encrypt 的 staging 环境,完整走一遍验证和签发流程,但不消耗正式的速率配额。如果这一步能过,说明你的自动续期基本是健康的。</p>

<h2>三、坑一:HTTP-01 验证被 301 重定向吃掉了</h2>
<p>这是 nginx 站点最经典的失败场景。你的配置大概是这样的:</p>
<pre><code>server {

listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;

}</code></pre>
<p>看起来没问题,但 ACME 服务器访问 <code>http://example.com/.well-known/acme-challenge/xxx<;/code> 时,会被 301 跳到 <code>https://...<;/code>。<strong>ACME 规范允许跟随重定向,但如果跳转后的 HTTPS 证书恰好也要续期(或已经过期),就会形成死锁</strong>——要验证新证书,得先有有效的旧证书。</p>
<p>正确做法是显式放行 acme-challenge 路径,不要重定向:</p>
<pre><code>server {

listen 80;
server_name example.com www.example.com;

location ^~ /.well-known/acme-challenge/ {
    root /var/www/html;
    default_type "text/plain";
    allow all;
}

location / {
    return 301 https://$host$request_uri;
}

}</code></pre>
<p>注意 <code>^~</code> 前缀——它告诉 nginx"这个 location 一旦匹配,就不要再去做正则匹配了",可以防止某些通配 location 抢先拦截。</p>

<h2>四、坑二:webroot 路径和 nginx root 不一致</h2>
<p>很多人第一次签发证书时用的是 <code>certbot --nginx</code>(自动改配置),后面某次又手动用了 <code>--webroot</code> 指定一个目录,结果 renewal conf 里记录的 webroot 和 nginx 实际的 <code>root</code> 对不上,验证文件写到了错误的地方。</p>
<p>检查方法,直接看续期配置文件:</p>
<pre><code>cat /etc/letsencrypt/renewal/example.com.conf</code></pre>
<p>重点看这几行:</p>
<pre><code>[renewalparams]
authenticator = webroot
webroot_path = /var/www/html,
[[webroot_map]]
example.com = /var/www/html</code></pre>
<p>然后确认 nginx 里 <code>location /.well-known/acme-challenge/</code> 的 <code>root</code> 与 <code>webroot_path</code> 完全一致。<strong>路径不一致是那种"平时看不出问题、续期那天才炸"的隐形杀手</strong>。</p>

<h2>五、坑三:防火墙与 CDN 挡住验证请求</h2>
<p>如果你在服务器前面挂了 Cloudflare 之类的 CDN,并且开启了"强制 HTTPS"或者"Under Attack 模式",ACME 的 HTTP-01 请求会在 CDN 层就被处理掉,根本到不了你的 nginx。</p>
<p>两种解决方案:</p>
<ul>
<li><strong>暂停 CDN 代理</strong>(把 DNS 记录改成灰色云朵"DNS only"),验证完再打开。缺点是要手动操作,不适合全自动续期。</li>
<li><strong>改用 DNS-01 验证</strong>。这也是泛域名证书唯一的办法。以 Cloudflare 为例,用 acme.sh 加 dns_cf 插件后,续期时通过 API 自动写 TXT 记录,完全绕开 HTTP 层,稳定性极高。</li>
</ul>
<p>顺带说一句,如果你服务器上有 <code>fail2ban</code> 或者自研的 WAF 规则,注意别把 Let's Encrypt 的验证服务器 IP 段给封了。ACME 官方文档里有明确的 IP 段列表,可以直接加白名单。</p>

<h2>六、坑四:nginx reload 没有跟上</h2>
<p>证书续期成功了,但网站还是提示证书过期——因为 nginx 还没重新加载配置。<strong>证书文件被替换了,但 nginx 进程内存里缓存的还是旧证书</strong>。</p>
<p>certbot 的 <code>--deploy-hook</code> 就是干这个的。在续期配置里加上:</p>
<pre><code>certbot renew --deploy-hook "systemctl reload nginx"</code></pre>
<p>或者写进 renewal conf(推荐,一次写好永久生效):</p>
<pre><code>[renewalparams]
renew_hook = systemctl reload nginx</code></pre>
<p>更精细一点,如果一台机器上有多个 web 服务(比如 nginx 上还挂了 OpenResty、haproxy),可以用一个部署脚本统一处理:</p>
<pre><code>#!/bin/bash

/etc/letsencrypt/renewal-hooks/deploy/reload-services.sh

systemctl reload nginx 2>/dev/null
systemctl reload openresty 2>/dev/null
echo "$(date) certs deployed, services reloaded" >> /var/log/cert-deploy.log</code></pre>
<pre><code>chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh</code></pre>
<p>放在 <code>renewal-hooks/deploy/</code> 目录下的脚本,会在每次成功续期后自动执行,不需要改任何单个域名的 conf。这是最省心的做法。</p>

<h2>七、坑五:速率限制把你锁在门外</h2>
<p>Let's Encrypt 有严格的速率限制,最常撞上的两条:</p>
<ul>
<li><strong>同一注册域名每周 50 张证书</strong>(Registered Domain 维度,即 example.com 及其子域共享额度)</li>
<li><strong>同一账户每 3 小时 300 次新订单</strong></li>
</ul>
<p>调试阶段反复试错,很容易把额度耗光,然后要等一周。所以务必:</p>
<ol>
<li>调试一律用 <code>--dry-run</code>(走 staging 环境,独立配额且宽松得多);</li>
<li>实在要用 staging 签真证书,加 <code>--server https://acme-staging-v02.api.letsencrypt.org/directory<;/code>,但记得浏览器不认 staging 证书,仅用于连通性验证。</li>
</ol>

<h2>八、一套可靠的自检清单</h2>
<p>把下面这些做成一条命令,每周跑一次,基本能消灭"证书突然过期"事故:</p>
<pre><code>#!/bin/bash

cert-check.sh — 证书健康巡检

echo "=== 1. 当前系统所有证书到期日 ==="
for d in /etc/letsencrypt/live/*/; do

name=$(basename "$d")
if [ -f "$d/fullchain.pem" ]; then
    exp=$(openssl x509 -enddate -noout -in "$d/fullchain.pem" | cut -d= -f2)
    days=$(( ($(date -d "$exp" +%s) - $(date +%s)) / 86400 ))
    printf "%-30s %s (%s天后)\n" "$name" "$exp" "$days"
fi

done

echo ""
echo "=== 2. 续期定时器状态 ==="
systemctl is-active certbot.timer && systemctl list-timers certbot.timer --no-pager | head -2

echo ""
echo "=== 3. 域名线上实际证书 ==="
for dom in example.com www.example.com; do

echo | openssl s_client -servername "$dom" -connect "$dom:443" 2&gt;/dev/null \
    | openssl x509 -noout -subject -enddate 2&gt;/dev/null | sed "s/^/[$dom] /"

done

echo ""
echo "=== 4. 最近一次续期日志 ==="
tail -n 5 /var/log/letsencrypt/letsencrypt.log</code></pre>
<p>配合一个最简单的告警——当剩余天数小于 15 时发邮件或者推送到钉钉/飞书机器人——整套自动续期才算真正闭环。<strong>只要还有人需要"想起来去看一眼",这套系统就迟早会出事。</strong></p>

<h2>九、小结</h2>
<p>证书自动续期这件事,工具本身已经很成熟了,真正会出问题的永远是环境:<strong>重定向规则、webroot 路径、CDN 代理、reload 缺失、速率限制</strong>。把这五点逐一核对一遍,加上每周自动巡检和到期告警,你的网站就不会再出现那个红色的安全警告了。</p>
<p>对个人站长来说,这类"配置一次、长期稳定"的基础设施工作,投入产出比其实非常高——花两个小时把 certbot 彻底配好,往后几年都能省下每个月提心吊胆的成本。</p>

<div style="margin:20px 0;padding:10px 0;border-top:1px solid #eee;border-bottom:1px solid #eee">
<ins class="adsbygoogle" style="display:block;text-align:center" data-ad-layout="in-article" data-ad-format="fluid" data-ad-client="ca-pub-1561091167355374" data-ad-slot="8963568440"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>

Last modification:September 19th, 2026 at 12:22 pm

Leave a Comment