apisix/ssl_manager/HTTP_VERIFY_ISSUE_ANALYSIS.md

229 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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