三个站点建好了,HTTP 全通:首页 200,接口返回 JSON,健康检查 status:ok。同机另外 6 个站点也没受影响。
然后去签证书:面板,证书,新建,Let’s Encrypt,HTTP 验证,提交。我当时提交完就卡住了,反复失败。
我一开始以为,要么是 80 端口没放行,要么是域名没解析过来,要么是面板的 ACME 客户端有问题。于是去检查安全组、查 DNS、重装插件。
这些都是瞎猜。有一个更便宜的动作应该先做。
先说清楚 CA 到底在验什么
证书颁发机构(CA)必须确认「申请证书的人确实管着这个域名」。HTTP-01 是最常用的一种确认方式:它给你一个随机字符串,也就是 token,要求你在该域名的 80 端口上放一个文件,路径是固定的。
http://<域名>/.well-known/acme-challenge/<随机token>放好之后,CA 自己来取一次。取到内容而且内容对得上,就证明你管着这个域名,可以签发。
问题就在这个「自己来取一次」上。
我们的站点是反向代理站,所有路径都被一条 location / 反代给了后端容器。CA 的请求打进来,走的是这条链。
CA 服务器 └─▶ http://h5.example.com/.well-known/acme-challenge/<token> └─▶ 面板 OpenResty(80) └─▶ location / → proxy_pass 127.0.0.1:18091 └─▶ 业务容器:我没有这条路由 → 404CA 拿到 404,判定「无法证明控制权」,签发失败。
注意失败的性质。不是证书算错了,不是 ACME 客户端崩了,也不是 80 端口不通。80 端口通得很,请求进来了,只是被送去了错误的地方。
所以该排查的不是证书,而是那条路径的归属权。.well-known/acme-challenge/ 这个前缀,到底该由面板直接回答,还是该转发给后端。答案是前者。
签之前先做三步预检
第一步,在站点根目录放一个探针文件。
# 三个站点各放一个,内容就叫 probe-okfordinh5 api admin;domkdir-p/opt/1panel/www/sites/$d.example.com/index/.well-known/acme-challengeecho"probe-ok">/opt/1panel/www/sites/$d.example.com/index/.well-known/acme-challenge/probedone第二步,带 Host 头请求,看能不能拿到内容。
fordinh5 api admin;doprintf"%-20s ""$d.example.com"curl-s--max-time5-H"Host:$d.example.com"http://127.0.0.1/.well-known/acme-challenge/probeecho""done第三步,判读。三个域名都输出 probe-ok,说明路径可穿透,面板会自己回答,直接去签,一定能过。某个域名输出 404,说明被反代吃掉了,先给该站点加 location,重跑第二步再签。输出是空或者别的内容,说明根目录位置不对,用 ls /opt/1panel/www/sites/<域名>/ 确认真实根目录。
这条判读的价值在于,它把「证书签不下来」这个模糊故障翻译成了一个有确定答案的二选一:这条路径现在归谁回答。
把验证路径从反代里抢回来
对返回 404 的站点,在配置文件的 location / 之前插入这一段。
# ACME HTTP-01 校验路径,不进后端 location ^~ /.well-known/acme-challenge/ { root <站点根目录>; default_type text/plain; try_files $uri =404; }三个细节。
放在 location / 之前这一条,其实 nginx 的前缀匹配取「最长命中」,.well-known/acme-challenge/ 比 / 长,所以位置前后不影响结果。但写成习惯动作,能让你在加其它精确路径时不再思考一次排序问题。
^~ 是主动利用优先级。它让这个前缀命中后直接跳过正则阶段,等于给验证路径上了一道「任何规则都别来抢」的保险。
default_type text/plain 保证 CA 看到的是纯文本,而不是被猜成别的 MIME 类型。
改完保存,重跑第二步,确认能拿到 probe-ok,再去签。签完记得清理探针。
rm-rf/opt/1panel/www/sites/{h5,api,admin}.example.com/index/.well-known一个会让补丁失效的坑,root 要写容器内路径
面板的 OpenResty 是一个容器,宿主目录和容器内目录是映射关系,不是同一串字符。
宿主 /opt/1panel/www ──▶ 容器内 /www所以,在宿主机上建文件,写的是 /opt/1panel/www/sites/…;在 nginx 配置里写 root,要写 /www/sites/…,写成 /opt/1panel/www/sites/… 是错的。
写成宿主路径的后果极其隐蔽。nginx 在容器里找不到这个目录,请求变成 404,而 nginx -t 语法检查照样通过。你会以为补丁加上了,其实什么都没发生。
验证方法只有一条,用容器内的视角去看。
OR=$(dockerps--format'{{.Names}}'|grep-iopenresty|head-1)dockerexec"$OR"cat/www/sites/<站点>/index/.well-known/acme-challenge/probe能打印出 probe-ok,路径映射才算对。
DNS 还没切过来的时候怎么办
如果 HTTP-01 这条路因为时间窗口走不通,比如 DNS 还指向旧机,切过去之前 CA 根本访问不到本机,还有另外两条路。
从旧机导出 fullchain.pem 加 privkey.pem,在新机导入,这是搬运旧证书,明文窗口是零,代价是搬来的证书不会自动续签,必须重签一次。在域名解析处加一条 TXT 记录,不经过 80 端口,这是 DNS-01,明文窗口也是零,代价是需要能改 DNS,还依赖面板的 DNS 插件。先把解析指到新机再立刻签发,这是先切 A 记录再走 HTTP-01,明文窗口是数分钟,但能拿到自动续签。
搬运那条的坑值得单独说。它是零窗口的,很诱人,但搬过来的证书是一份「死」的文件。面板不知道它从哪来,也就不会替你续。切完 DNS、服务稳定之后,必须回到面板重新申请一次,此时域名已指向本机,HTTP-01 能过,换成带自动续签的那一份。否则到期那天全站证书静默失效,而且是几个月后,你已经忘了这件事的时候。
先切 A 记录那条的窗口,可以用前面那两条手法压到几乎为零。先把 .well-known 的 location 配好、把证书申请单准备好,切完解析立刻提交。明文期就是几十秒的验证往返。