首页能打开,Certbot的HTTP-01挑战地址却404。先别重装客户端:网页由谁响应、文件写到哪里,可能不是同一条路。本文聚焦Nginx配合webroot申请SSL证书的路径预检。
一、先分清三个"成功"
Certbot写入临时文件、HTTP入口返回正确内容、CA完成验证,是三个不同结果。webroot插件使用正在运行的Web服务器,不需要自己占用80端口,但也不会替你修好所有路由。certonly只取证书,不等于安装到Nginx。
本文实际运行的是独立Nginx回环HTTP实验:临时目录、随机高端口、固定测试内容。没有运行真实Certbot、访问CA或改生产配置;高端口仅为隔离实验,真实HTTP-01仍从公网80端口开始,不能把实验端口搬到申请参数里冒充替代方案。

图为webroot与Nginx root方案的预检顺序,非完整网络时序或CA实测。挑战路径指/.well-known/acme-challenge/下的具体文件;测试文本不是正式挑战的key authorization。还要核对域名命中的server及公网各入口。
二、-w填写根目录,不是挑战目录
Certbot官方说明:webroot会把文件放到${webroot-path}/.well-known/acme-challenge。假设-w是/var/www/acme,测试文件就应位于/var/www/acme/.well-known/acme-challenge/probe。若-w已经写到acme-challenge,插件还会再追加一层,文件就"住错楼"了。
Nginx的root按根路径加请求URI构造文件名。因此下面root也填/var/www/acme,而不是挑战子目录。alias是替换location对应路径的另一种语义,不能把root随手改名成alias后照搬。多域名使用多个-w时,每个-d采用它之前最近的-w,逐个核对,不只测第一个域名。
三、让挑战路径避开隐藏目录拦截
下面是供评审的最小server示意,不可覆盖整份生产配置;实际域名、监听、访问控制和现有include都要合并检查。普通业务保留HTTPS跳转,挑战路径独立返回文件,缺文件明确404。
nginx
server {
listen 80;
server_name example.com;
location ^~ /.well-known/acme-challenge/ {
root /var/www/acme;
default_type text/plain;
try_files $uri =404;
}
location ~ /\. { deny all; }
location / { return 301 https://example.com$request_uri; }
}
在这个同层、没有其他更精确匹配的示例里,最长前缀加^~会跳过正则location检查,避免通用的隐藏目录拦截把.well-known一起挡住。它不是"最高优先级万能开关":精确匹配、嵌套配置及server级重写仍要单独看。不要为验证关闭全部隐藏文件保护,更不要把私钥目录设为webroot。
四、server级return可能先于location生效
一个常见误区是:在server里保留统一return 301,再新增挑战location,以为后者可以例外。Nginx重写模块的server级指令先执行;return终止处理,本例挑战请求仍会跳走。将业务跳转放入location /,才能让独立挑战location处理自己的路径。
这不代表HTTP-01禁止所有跳转。Let's Encrypt当前允许最多10次、目标协议为HTTP或HTTPS且端口为80或443的跳转,转HTTPS时不验证其证书。本文选择直接200只是便于定位;不要把curl看到301立即判作CA失败,也不要把跳转规则推广到所有CA。
五、先做路径预检,再讨论申请
在受控测试目录放一个不含秘密的探测文件,内容固定为path-check且不加换行。用下面完整函数核对状态码和全部响应字节。它不自动跟随跳转,因此能把首跳301单独暴露;网络超时会抛异常,不伪装成检查通过。
python
import http.client
def probe(host, port, path, expected):
c = http.client.HTTPConnection(host, port, timeout=5)
try:
c.request("GET", path, headers={"Host": "example.com"})
r = c.getresponse()
body = r.read()
return r.status == 200 and body == expected
finally:
c.close()
# 仅针对自己控制的测试文件;此函数不跟随重定向。
# probe("127.0.0.1", 18080,
# "/.well-known/acme-challenge/probe", b"path-check")
这段函数只用于小型预检文件,不是通用下载器。连接地址、端口按实际测试环境填写,Host要改成自己获授权的域名;回环成功只证明该入口,不能替代外网DNS、A/AAAA、CDN、WAF和所有负载节点检查。返回200却是登录页或首页,也必须失败。
六、隔离实验看到了什么
| 受控条件 | 实际结果 | 说明 |
|---|---|---|
| 正确root与^~,文件存在 | 200,正文完全相同 | 本入口路径通过 |
| root错误或文件缺失 | 404 | 写入与读取位置需对齐 |
| 去掉^~,命中隐藏目录正则 | 403 | 语法通过不代表路由正确 |
| 加入server级return | 301 | 挑战location没能绕过跳转 |
| 状态200但期待内容不同 | 预检False | 状态码不能替代正文 |
这些是Nginx 1.26.3的回环响应,不是CA报错复现。实验还检查了普通业务路径仍跳转,以及进程退出和临时目录清理。真实403还可能来自权限、上游或WAF;真实404也不只一种原因,不能用这张表反推唯一根因。
七、申请模板仍有副作用边界
路径通了之后,才评估下面模板。先确认实际Certbot版本和插件、域名授权、系统及用户配置中的server、钩子和目录权限。--dry-run通常使用测试服务器,但server配置可覆盖它;挑战、账户、日志及部分钩子仍可能产生操作,不等于无副作用。
bash
certbot certonly --webroot \
-w /var/www/acme -d example.com \
--dry-run
本文对这块仅做Shell语法与参数捕获,没有执行Certbot。正式申请需按授权去掉测试选项并确认生产directory;测试证书不用于公众信任部署。签发后还要检查最终名称、链、有效期、密钥配对和真实HTTPS服务,不把申请退出码当成全链路验收。
八、收尾检查清单与资料
- -w、root及实际文件路径相互对应,没有重复追加挑战目录。
- 挑战请求命中正确server;隐藏目录规则和server级跳转已分别验证。
- 从外部逐入口检查状态与精确内容,关注IPv6和多节点一致性。
- 预检文件及时删除;不泄露私钥、不放宽全部目录权限。
- CA验证、签发、部署、后续续期分别记录,不互相替代。
配图由AI辅助生成并经技术与可读性复核;示例为隔离实验与配置模板。