# HTTP-01 验证失败根源分析 ## 问题现象 Let's Encrypt 在申请证书时返回: ``` Domain: jingrowtools.cn Type: unauthorized Detail: 125.89.136.224: Invalid response from http://jingrowtools.cn/.well-known/acme-challenge/xxx: 404 ``` ## 根本原因分析 ### 1. **路由匹配优先级问题** ⚠️ **核心问题** **问题描述:** - APISIX 路由匹配规则:`host` 匹配 > `uri` 匹配 > `priority` 优先级 - 当前配置: - `jingrowtools.cn` 路由:`host: "jingrowtools.cn"`, `uri: "/*"`, `priority: 0` - `certbot-webroot` 路由:`uri: "/.well-known/acme-challenge/*"`, `priority: 9999`, **没有 host 限制** **匹配流程:** 1. 请求 `http://jingrowtools.cn/.well-known/acme-challenge/xxx` 2. APISIX 先匹配 `host: "jingrowtools.cn"` → 找到 `jingrowtools.cn` 路由 3. 在该路由内匹配 `uri: "/*"` → **匹配成功** ✅ 4. 请求被转发到 upstream `192.168.10.2:8201` 5. **webroot 路由永远不会被匹配到** ❌ **根本原因:** - `host` 匹配优先级高于 `uri` 匹配 - 即使 webroot 路由的 `priority` 更高,但 `host` 匹配会先命中 `jingrowtools.cn` 路由 - webroot 路由没有设置 `host`,导致无法优先匹配 ### 2. **容器文件系统隔离问题** **问题描述:** - APISIX 运行在 Docker 容器中 - Certbot 在宿主机上运行 - 验证文件写入到 `/var/www/certbot`(宿主机) - 但 APISIX 容器内无法访问宿主机文件系统 **解决方案:** - 需要在 `docker-compose.yml` 中挂载卷: ```yaml volumes: - /var/www/certbot:/var/www/certbot:ro ``` ### 3. **Serverless 函数执行问题** **问题描述:** - 使用 `serverless-pre-function` 插件读取文件 - Lua 函数需要访问容器内的文件系统 - 如果文件路径不正确或权限不足,会返回 404 **当前函数:** ```lua local path = "/var/www/certbot/.well-known/acme-challenge/" .. string.match(ctx.var.uri, "/.well-known/acme-challenge/(.+)") local file = io.open(path, "r") ``` **潜在问题:** - 路径拼接可能不正确 - 文件权限问题 - 容器内文件不存在 ## 解决方案 ### 方案一:修复路由匹配(推荐)✅ **修改 webroot 路由,添加 host 匹配并提高优先级:** ```bash curl -X PUT 'http://localhost:9180/apisix/admin/routes/certbot-webroot' \ -H 'X-API-KEY: 8206e6e42b6b53243c52a767cc633137' \ -H 'Content-Type: application/json' \ -d '{ "uri": "/.well-known/acme-challenge/*", "name": "certbot-webroot", "priority": 10000, "host": "jingrowtools.cn", "plugins": { "serverless-pre-function": { "phase": "rewrite", "functions": [ "return function(conf, ctx) local token = string.match(ctx.var.uri, \"/.well-known/acme-challenge/(.+)\"); local path = \"/var/www/certbot/.well-known/acme-challenge/\" .. token; local file = io.open(path, \"r\"); if file then local content = file:read(\"*all\"); file:close(); ngx.header.content_type = \"text/plain\"; ngx.say(content); else ngx.status = 404; ngx.say(\"File not found: \" .. path); end end" ] } }, "status": 1 }' ``` **关键修改:** 1. ✅ 添加 `"host": "jingrowtools.cn"` - 确保只匹配该域名 2. ✅ 提高 `priority` 到 10000 - 确保优先匹配 3. ✅ 修复 Lua 函数路径处理 ### 方案二:修改主路由,排除验证路径 **在 `jingrowtools.cn` 路由中添加条件,排除验证路径:** ```json { "uri": "/*", "name": "jingrowtools.cn", "host": "jingrowtools.cn", "vars": [ ["uri", "!~", "^/.well-known/acme-challenge/"] ], ... } ``` 这样验证路径就不会被主路由匹配。 ### 方案三:使用 DNS-01 验证(最可靠)✅✅ **优点:** - 不依赖 HTTP 服务 - 不需要配置 webroot 路由 - 适合容器化环境 - 支持通配符证书 **缺点:** - 需要 DNS API 访问权限 - 配置稍复杂 **实现:** 需要修改 `ssl_manager.py`,支持 DNS-01 验证。 ### 方案四:使用独立的验证服务 **创建一个简单的 HTTP 服务专门处理验证:** ```python # 简单的验证服务 from http.server import HTTPServer, BaseHTTPRequestHandler import os class ACMEHandler(BaseHTTPRequestHandler): def do_GET(self): if self.path.startswith('/.well-known/acme-challenge/'): token = self.path.split('/')[-1] file_path = f'/var/www/certbot/.well-known/acme-challenge/{token}' if os.path.exists(file_path): with open(file_path, 'r') as f: self.send_response(200) self.send_header('Content-type', 'text/plain') self.end_headers() self.wfile.write(f.read().encode()) else: self.send_response(404) self.end_headers() else: self.send_response(404) self.end_headers() if __name__ == '__main__': server = HTTPServer(('0.0.0.0', 8080), ACMEHandler) server.serve_forever() ``` 然后在 APISIX 中创建一个路由指向这个服务。 ## 推荐解决步骤 ### 立即修复(方案一) 1. **确保容器挂载了 webroot 目录:** ```yaml # docker-compose.yml volumes: - /var/www/certbot:/var/www/certbot:ro ``` 2. **更新 webroot 路由配置(添加 host 和更高优先级):** ```bash # 使用上面的方案一配置 ``` 3. **测试验证路径:** ```bash echo "test-token" | sudo tee /var/www/certbot/.well-known/acme-challenge/test-token curl http://jingrowtools.cn/.well-known/acme-challenge/test-token # 应该返回: test-token ``` 4. **重新申请证书:** ```bash cd /home/jingrow/apisix/ssl_manager python3 ssl_manager.py request --domain jingrowtools.cn ``` ## 验证流程说明 Let's Encrypt HTTP-01 验证流程: 1. **Certbot 生成随机 token** - 例如:`yUXNhDPsvsjEwYjxishk_XZ6kGZFRqn22FSUYcfuZQY` 2. **写入验证文件** - 路径:`/var/www/certbot/.well-known/acme-challenge/{token}` - 内容:`{token}.{account_key_thumbprint}` 3. **Let's Encrypt 服务器访问** - URL: `http://jingrowtools.cn/.well-known/acme-challenge/{token}` - 期望:返回文件内容 4. **APISIX 路由匹配** - ❌ **当前问题**:匹配到 `jingrowtools.cn` 路由,转发到 upstream - ✅ **应该**:匹配到 `certbot-webroot` 路由,返回文件内容 5. **验证结果** - 成功:Let's Encrypt 收到正确内容 → 颁发证书 - 失败:收到 404 或其他错误 → 拒绝申请 ## 总结 **HTTP 验证失败的根本原因:** 1. **主要问题**:路由匹配优先级 - `host` 匹配优先于 `uri` 匹配,导致验证请求被错误路由 2. **次要问题**:容器文件系统隔离 - 需要正确挂载卷 3. **潜在问题**:Serverless 函数路径处理 - 需要确保路径正确 **最佳解决方案:** - 方案一(修复路由匹配)+ 容器卷挂载 = 最简单有效 - 方案三(DNS-01 验证)= 最可靠,适合生产环境