ACME 续期失败,多半是 80 端口的问题
证书 90 天到期,自动续期配好之后就没人管了,直到某天浏览器报警告。翻日志发现续期任务每天都在跑,每天都在失败。最常见的原因只有一个:HTTP-01 校验拿不到文件。
校验到底发生了什么
CA 会向 http://你的域名/.well-known/acme-challenge/<token> 发一个 GET,期望拿到指定的字符串。这个请求:
- 只走 80 端口,不会跟随 301 到 443(部分 CA 会跟随,但不该依赖);
- 不认你的 CDN 缓存,缓存命中旧内容就会失败;
- 需要从公网可达,本机 curl 成功不代表公网成功。
先手工复现一次
# 在服务器上看 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,证书文件是新的,进程里还是旧的,这个坑每年都会有人踩。