Certbot webroot申请SSL证书总是404:Nginx挑战目录与路径映射检查

首页能打开,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辅助生成并经技术与可读性复核;示例为隔离实验与配置模板。

相关推荐
如若1231 小时前
Windows SSH + Headscale/Tailscale 组网及免密登录完整配置教程
运维·windows·ubuntu·ssh·内网穿透
开开心心就好1 小时前
音频降噪剪辑工具,音量裁剪一键处理
运维·服务器·前端·spring·ocr·音视频·1024程序员节
李纲明2 小时前
WordPress 白屏或提示「出现了严重错误」:先开 WP_DEBUG 抓致命错误,再二分插件与主题
运维·php·debug·wordpress·白屏·插件冲突·devdiy
Ruiery3 小时前
Linux 6.6内核并发与同步(五):mutex 可睡眠锁
linux·运维·服务器
莫晓燚3 小时前
双机部署若依 (RuoYi‑Vue) 传统环境部署---实战
linux·运维·数据库·redis·mysql·nginx·ruoyi
开开心心就好3 小时前
竖式除法出题工具,整除余数混合模式
运维·服务器·前端·javascript·ocr·perl·1024程序员节
领麦微红外4 小时前
红外测温传感器 赋能充电桩热安全与运维智能化
运维·安全·产品经理·智能硬件
余额瞒着我当琳5 小时前
Linux初识:Linux的历史,计算机的发展脉络,发行版本,选购云服务器,安装Xshell,第一次连接,基础命令指南
linux·运维·服务器