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