rea1m

TLS 证书部署里几个反复踩的坑

给一台新机器配证书,流程本身只有一条命令,但失败率出奇地高。这篇把最近几次踩过的记一下,按踩到的概率排序。

一、域名有 AAAA 记录,机器却没有 IPv6

这是失败率最高的一条。CA 的验证节点通常会优先尝试 IPv6,如果域名同时有 A 和 AAAA,而服务器只监听 IPv4:

CA 查 AAAA → 拿到 IPv6 地址 → 连接超时 → 验证失败

排查只需要一条命令:

dig +short AAAA example.com

输出必须为空。很多 DNS 服务商(尤其是动态 DNS)会默认给 zone 加一条 IPv6 记录,如果机器上没配 v6,这条记录就是纯粹的负担。

顺便说,即使不用 CA 验证,AAAA 指向一个不通的地址也会让部分客户端在建立连接时多等一个超时周期。

二、80 端口先跳了 https,证书还没签出来

很常见的配置顺序错误:为了"强制 HTTPS",在所有站点的 80 上都写了 return 301 https://...,然后再去签证书。

验证请求是这样走的:

http://example.com/.well-known/acme-challenge/xxx
  → 301 → https://example.com/.well-known/acme-challenge/xxx
  → 证书不存在 → handshake failure → 验证失败

正确顺序是先让 80 正常返回内容,签完证书之后再改成:

location /.well-known/acme-challenge/ { root /var/www/html; }
location / { return 301 https://$host$request_uri; }

这样续期永远能过,因为 .well-known 被单独放行了。

三、证书签好了,但服务进程读不到

签发工具产出的私钥权限通常是 600,目录 700,属主 root。以 root 启动的 nginx 主进程没问题,但以普通用户运行的服务会直接报 permission denied。

三个办法:

第三种的做法大致是:建一个 root:服务组、权限 750 的目录,把 fullchain.pem 和 privkey.pem 用 install -m 640 复制过去,然后在续期钩子里重做一次,再 reload 服务。

四、证书有效期比你以为的短

免费 DV 证书常见的有效期是 90 天。如果签发是在某个控制台手动点的,而不是由自动化工具管理,那这件事会在三个月后静默失效 —— 不会有任何告警,直到你某天发现站点打不开。

检查一下机器上有没有续期定时器:

systemctl list-timers | grep -i cert

没有的话,要么补上,要么在日历里记一条。手动续期不是不行,但依赖人的记忆,历史上出问题的概率很高。

五、顺手记录:握手延迟的测量

排查证书问题的时候,经常需要单独看握手耗时。用 curl 的格式化输出比看浏览器开发者工具方便:

curl -o /dev/null -s -w 'dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n' https://example.com

如果 time_appconnect 明显大于 time_connect,说明 TLS 握手占了大头,通常是证书链不完整,客户端需要额外去抓中间证书。