docs.sujiuovo.top

ACME 续期失败,多半是 80 端口的问题

2026-10-19acme.sh 3.x / Let's Encrypt

证书 90 天到期,自动续期配好之后就没人管了,直到某天浏览器报警告。翻日志发现续期任务每天都在跑,每天都在失败。最常见的原因只有一个:HTTP-01 校验拿不到文件。

校验到底发生了什么

CA 会向 http://你的域名/.well-known/acme-challenge/<token> 发一个 GET,期望拿到指定的字符串。这个请求:

先手工复现一次

# 在服务器上看 webroot 是否真的能取到文件
mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
echo ok > /var/www/letsencrypt/.well-known/acme-challenge/test
curl -s http://你的域名/.well-known/acme-challenge/test

# 从外部机器验证(这一步经常被跳过)
curl -sI http://你的域名/.well-known/acme-challenge/test

如果本机能取到、外部取不到,检查三件事:安全组有没有放行 80、Nginx 的 80 端口 server 是否把 /.well-known/ 之外的路径全部 301、以及前面有没有一层 CDN 在拦。

Nginx 的写法

server {
    listen 80;
    server_name example.com;

    # 校验路径必须能直接读到文件,且优先于跳转
    location ^~ /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
        default_type text/plain;
        try_files $uri =404;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}
顺序:用 ^~ 前缀匹配,确保它压过任何正则 location。只写 location /.well-known/ 时,如果另有正则规则命中,仍可能被抢走。

当 80 端口不能开

有些环境确实无法开放 80(比如只允许 443 出口的策略)。这时换 DNS-01:CA 让你在 DNS 里放一条 TXT 记录,验证通过后签发。Cloudflare 的 API token 配好后:

export CF_Token="..."
acme.sh --issue --dns dns_cf -d example.com -d '*.example.com'

DNS-01 的好处是能签通配符,坏处是验证要等 DNS 传播,失败重试的间隔更长。

续期任务要能看出失败

acme.sh --cron 放在 crontab 里,输出默认丢进 /dev/null,失败了也没人知道。至少把输出留下:

17 3 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" >> /var/log/acme.log 2>&1

再加一条到期检查,剩下 20 天还没续上就发通知——比等到浏览器报警告要早得多:

openssl x509 -in /etc/nginx/cert/example.com/fullchain.pem -noout -enddate

续期成功之后

证书换了文件,Nginx 不会自己重载。deploy hook 里加一句:

acme.sh --install-cert -d example.com \
  --key-file       /etc/nginx/cert/example.com/privkey.pem \
  --fullchain-file /etc/nginx/cert/example.com/fullchain.pem \
  --reloadcmd      "systemctl reload nginx"

少了 --reloadcmd,证书文件是新的,进程里还是旧的,这个坑每年都会有人踩。

← 返回归档