跳到主要内容

Caddy SSL 证书自动验证集成

概述

本文档说明如何配置 Caddy 使用井云服务中心后端的域名验证接口来自动颁发 SSL 证书。

架构设计

Caddy → Gateway (HTTP) → Tenant Service (gRPC) → Database

验证流程

  1. Caddy 请求: 当有新域名请求 HTTPS 连接时,Caddy 会调用验证接口
  2. Gateway 转发: Gateway 接收 HTTP 请求并转发到 Tenant 服务
  3. Tenant 验证: Tenant 服务查询数据库验证域名
  4. 返回结果: 返回是否允许颁发证书

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 无法获取证书

症状: 访问域名时显示证书错误

排查步骤:

  1. 检查 Caddy 日志:

    docker logs caddy | grep -i "certificate\|tls\|acme"
  2. 测试验证接口:

    curl "http://gateway:8000/internal/caddy/ask?domain=YOUR_DOMAIN"
  3. 检查租户状态:

    SELECT id, subdomain, domain, status, expires_at 
    FROM tenants
    WHERE subdomain = 'YOUR_DOMAIN' OR domain = 'YOUR_DOMAIN';

问题 2: 验证接口返回 500 错误

可能原因:

  • 数据库连接失败
  • Tenant 服务未启动
  • gRPC 通信失败

排查步骤:

  1. 检查 Gateway 日志:

    docker logs gateway | grep -i "caddy\|tenant"
  2. 检查 Tenant 服务状态:

    docker ps | grep tenant
    curl http://tenant:9000/health
  3. 测试数据库连接:

    docker exec -it postgres psql -U user -d database -c "SELECT 1;"

问题 3: 域名验证通过但仍无法访问

可能原因:

  • DNS 未正确解析
  • 防火墙阻止 80/443 端口
  • Let's Encrypt 速率限制

排查步骤:

  1. 检查 DNS 解析:

    nslookup YOUR_DOMAIN
    dig YOUR_DOMAIN
  2. 检查端口:

    telnet YOUR_DOMAIN 80
    telnet YOUR_DOMAIN 443
  3. 检查 Let's Encrypt 速率限制:

性能优化

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),
)

相关文档

更新日志

  • 2025-12-18: 初始版本,实现基础域名验证功能