apisix/ssl_manager/HTTP_VERIFY_ISSUE_ANALYSIS.md

6.9 KiB
Raw Blame History

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 中挂载卷:
    volumes:
      - /var/www/certbot:/var/www/certbot:ro
    

3. Serverless 函数执行问题

问题描述:

  • 使用 serverless-pre-function 插件读取文件
  • Lua 函数需要访问容器内的文件系统
  • 如果文件路径不正确或权限不足,会返回 404

当前函数:

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 匹配并提高优先级:

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 路由中添加条件,排除验证路径:

{
  "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 服务专门处理验证:

# 简单的验证服务
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 目录:

    # docker-compose.yml
    volumes:
      - /var/www/certbot:/var/www/certbot:ro
    
  2. 更新 webroot 路由配置(添加 host 和更高优先级):

    # 使用上面的方案一配置
    
  3. 测试验证路径:

    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. 重新申请证书:

    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 验证)= 最可靠,适合生产环境