Caddy SSL 证书自动验证集成
概述
本文档说明如何配置 Caddy 使用井云服务中心后端的域名验证接口来自动颁发 SSL 证书。
架构设计
Caddy → Gateway (HTTP) → Tenant Service (gRPC) → Database
验证流程
- Caddy 请求: 当有新域名请求 HTTPS 连接时,Caddy 会调用验证接口
- Gateway 转发: Gateway 接收 HTTP 请求并转发到 Tenant 服务
- Tenant 验证: Tenant 服务查询数据库验证域名
- 返回结果: 返回是否允许颁发证书
Caddyfile 配置
{
admin 0.0.0.0:2019
email admin@your.com
}
:80, :443 {
tls {
on_demand {
ask http://gateway:8000/internal/caddy/ask
}
}
reverse_proxy gateway:8000
}
配置说明
on_demand: 启用按需 TLS,只在首次访问时申请证书ask: 指定验证 URL,Caddy 会在颁发证书前调用此接口http://gateway:8000/internal/caddy/ask: 井云网关的验证端点
API 接口
请求
GET /internal/caddy/ask?domain=shop.example.com HTTP/1.1
Host: gateway:8000
响应
成功(允许颁发证书)
HTTP/1.1 200 OK
Content-Type: application/json
{
"allowed": true,
"tenant_id": 123
}
失败(拒绝颁发证书)
HTTP/1.1 200 OK
Content-Type: application/json
{
"allowed": false,
"reason": "domain not found",
"tenant_id": 0
}
注意: 即使拒绝,HTTP 状态码仍为 200,通过 allowed 字段判断是否允许。
验证逻辑
Tenant 服务按以下顺序验证域名:
1. 检查子域名
查询 tenants 表的 subdomain 字段:
SELECT * FROM tenants WHERE subdomain = 'shop.example.com';
2. 检查自定义域名
如果子域名未找到,查询 domain 字段:
SELECT * FROM tenants WHERE domain = 'shop.example.com';
3. 验证租户状态
找到租户后,检查:
- 状态:
status必须为active - 过期时间:
expires_at必须晚于当前时间
4. 返回结果
- ✅ 所有检查通过 →
allowed: true - ❌ 任何检查失败 →
allowed: false+ 失败原因
拒绝原因
| Reason | 说明 |
|---|---|
domain is empty | 请求中未提供域名参数 |
domain not found | 数据库中未找到该域名 |
tenant is not active | 租户状态不是 active |
tenant has expired | 租户已过期 |
internal error | 服务内部错误 |
数据库表结构
tenants 表
CREATE TABLE tenants (
id BIGINT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
subdomain VARCHAR(255) UNIQUE NOT NULL, -- 子域名
domain VARCHAR(255) UNIQUE, -- 自定义域名
user_id BIGINT NOT NULL,
status VARCHAR(50) DEFAULT 'trial', -- active, inactive, suspended, trial
expires_at TIMESTAMP, -- 过期时间
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 索引
CREATE INDEX idx_subdomain ON tenants(subdomain);
CREATE INDEX idx_domain ON tenants(domain);
CREATE INDEX idx_status ON tenants(status);
测试示例
1. 创建测试租户
# 通过 API 创建租户
curl -X POST http://gateway:8000/admin/tenants \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "测试商店",
"subdomain": "shop.example.com",
"domain": "www.myshop.com",
"user_id": 1,
"status": "active"
}'
2. 测试域名验证
# 测试子域名
curl "http://gateway:8000/internal/caddy/ask?domain=shop.example.com"
# 预期: {"allowed":true,"tenant_id":1}
# 测试自定义域名
curl "http://gateway:8000/internal/caddy/ask?domain=www.myshop.com"
# 预期: {"allowed":true,"tenant_id":1}
# 测试不存在的域名
curl "http://gateway:8000/internal/caddy/ask?domain=notexist.com"
# 预期: {"allowed":false,"reason":"domain not found"}
3. 测试 HTTPS 访问
# 首次访问会触发证书申请
curl -v https://shop.example.com
# 检查 Caddy 日志
docker logs caddy
安全考虑
1. 内部接口保护
虽然 /internal/caddy/ask 不需要用户认证,但建议:
- 仅允许 Caddy 容器访问(通过 Docker 网络隔离)
- 使用防火墙规则限制外部访问
- 考虑添加 IP 白名单或共享密钥验证
2. 速率限制
建议在 Caddy 或网关层添加速率限制,防止滥用:
:80, :443 {
rate_limit {
zone dynamic {
key {remote_host}
events 10
window 1m
}
}
# ... 其他配置
}
3. 日志审计
记录所有验证请求,便于审计:
s.log.Infof("Caddy domain validation: domain=%s, allowed=%v, tenant_id=%d, reason=%s",
req.Domain, reply.Allowed, reply.TenantId, reply.Reason)
故障排查
问题 1: Caddy 无法获取证书
症状: 访问域名时显示证书错误
排查步骤:
-
检查 Caddy 日志:
docker logs caddy | grep -i "certificate\|tls\|acme" -
测试验证接口:
curl "http://gateway:8000/internal/caddy/ask?domain=YOUR_DOMAIN" -
检查租户状态:
SELECT id, subdomain, domain, status, expires_at
FROM tenants
WHERE subdomain = 'YOUR_DOMAIN' OR domain = 'YOUR_DOMAIN';
问题 2: 验证接口返回 500 错误
可能原因:
- 数据库连接失败
- Tenant 服务未启动
- gRPC 通信失败
排查步骤:
-
检查 Gateway 日志:
docker logs gateway | grep -i "caddy\|tenant" -
检查 Tenant 服务状态:
docker ps | grep tenant
curl http://tenant:9000/health -
测试数据库连接:
docker exec -it postgres psql -U user -d database -c "SELECT 1;"
问题 3: 域名验证通过但仍无法访问
可能原因:
- DNS 未正确解析
- 防火墙阻止 80/443 端口
- Let's Encrypt 速率限制
排查步骤:
-
检查 DNS 解析:
nslookup YOUR_DOMAIN
dig YOUR_DOMAIN -
检查端口:
telnet YOUR_DOMAIN 80
telnet YOUR_DOMAIN 443 -
检查 Let's Encrypt 速率限制:
- 访问 https://crt.sh/?q=YOUR_DOMAIN
- 每周最多 50 个证书/域名
性能优化
1. 数据库索引
确保以下索引存在:
CREATE INDEX IF NOT EXISTS idx_tenants_subdomain ON tenants(subdomain);
CREATE INDEX IF NOT EXISTS idx_tenants_domain ON tenants(domain);
CREATE INDEX IF NOT EXISTS idx_tenants_status ON tenants(status);
2. 缓存策略
考虑在 Gateway 或 Tenant 服务中添加缓存:
// 缓存验证结果 5 分钟
cache.Set(fmt.Sprintf("caddy:domain:%s", domain), result, 5*time.Minute)
3. 连接池优化
调整 gRPC 连接池大小:
grpc.WithDefaultCallOptions(
grpc.MaxCallRecvMsgSize(10 * 1024 * 1024),
grpc.MaxCallSendMsgSize(10 * 1024 * 1024),
)
相关文档
- Caddy On-Demand TLS
- Let's Encrypt Rate Limits
- 🏢 多租户设计 - 多租户架构实现
- 🔐 安全设计 - 认证授权和安全机制
更新日志
- 2025-12-18: 初始版本,实现基础域名验证功能