Caddy 选择性 mTLS:内网 API 强制双向认证,公网访问零影响
【免费下载链接】caddyFast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS项目地址: https://gitcode.com/GitHub_Trending/ca/caddy
安全团队要求 API 域名启用强制双向认证,但同一个 Caddy 上跑着的公开文档站不能受影响——普通用户不应该被要求提交客户端证书。Caddy 选择性 mTLS 正是为这类"一部分流量必须验证书、另一部分照常放行"的需求设计的:它按 TLS 连接策略(connection_policy)匹配流量,对每个连接应用不同的客户端证书验证规则。
- 用
connection_policy+match把强制认证限定在指定 IP 段或域名上 - 先跑通基线双向认证,再叠加 remote_ip / sni 等匹配条件
- 拿到一张 mTLS 握手排障表,覆盖 CA 路径、证书链、匹配顺序等高频坑
为什么不是"一刀切"
| 方案 | 安全性 | 可用性 | 配置复杂度 |
|---|---|---|---|
| 不启用 mTLS | 仅验证服务端,无客户端身份 | 所有客户端零摩擦 | 低 |
| 全量 mTLS | 每个连接都验证客户端证书 | 没有证书的连接全部被拒 | 低,但牺牲可用 |
| 选择性 mTLS | 敏感流量强制验证,其余照常 | 普通访问不受影响 | 中,需写匹配条件 |
Caddy 的机制核心在 TLS 连接策略模块(ConnectionPolicy):每个 TLS 握手开始时,Caddy 拿 ClientHello 依次匹配已配置的连接策略,第一条命中的策略决定该连接的客户端认证行为。client_auth块(模式 + 信任池)就挂在策略上,而不是写死在服务器上。
支持的匹配维度(均作用于握手阶段,早于任何 HTTP 路由):
- SNI:按请求域名匹配,MatchServerName
- 远端 IP:按客户端 IP/网段匹配,MatchRemoteIP
- 本地 IP:按服务器本机 IP 匹配,适合多网卡多入口,MatchLocalIP
- 正则:SNI 通配,MatchServerNameRE
先想清楚:你的匹配条件是什么
上配置前先把"什么流量需要强制证书"说清楚,匹配条件选错了,后面全白调。
| 典型场景 | 适用条件 | 推荐 matcher |
|---|---|---|
| 内网服务网段 | 客户端固定来自办公网/机房 IP 段,如 192.168.1.0/24 | match remote_ip |
| 特定子域名 | 内部 API 独占一个子域名,如 api.internal.example.com | match sni |
| 正则通配 | 一批子域名都算内部服务,如*.corp.example.com | match sni_regexp |
证书文件只需准备好客户端 CA 的根证书,放到 Caddy 可读取的路径:
cp client-ca.crt /etc/caddy/caddy.ca.cer版本要求:trust_pool子块语法需要较新的 Caddy(v2.7+),升级或安装参见 官方安装文档。⚠️ 老版本只认已废弃的trusted_ca_cert_file字段,混用会在caddy adapt时报错。
配置落地:从最小可用到按需加条件
基线双向认证配置:tls 块 + client_auth + trust_pool
{ srv0 { listen :443 routes { @internal { header Authorization "Bearer sk-internal-..." } handle /api/* { reverse_proxy 10.0.0.20:8080 } } } }上面是示意路由结构,核心在下面的 TLS 块(取自仓库测试用例 tls_client_auth_cert_file.caddyfiletest):
:443 { respond "OK" tls { client_auth { mode require_and_verify # 关键行:强制提交并完整验证 trust_pool file { pem_file /etc/caddy/caddy.ca.cer # CA 根证书路径 } } } }这一段的意思是:该 server 上的所有连接都必须提交由这个 CA 签发的有效客户端证书,否则握手直接失败。跑通这个基线再谈选择性——如果基线都不通,加条件只会把排查范围放大。换成你的场景时只改pem_file路径和 CA 来源即可。
加条件:用 connection_policy + match remote_ip 限定强制范围
:443 { respond "OK" tls { # 默认策略:只请求、不强制,普通客户端零摩擦 client_auth { mode request trust_pool file { pem_file /etc/caddy/caddy.ca.cer } } # 内网网段命中后强制验证 connection_policy { match remote_ip 192.168.1.0/24 client_auth { mode require_and_verify trust_pool file { pem_file /etc/caddy/caddy.ca.cer } } } } }匹配规则要记死:策略按在 Caddyfile 中出现的顺序评估,先命中先生效,后面的策略不参与。⚠️ 如果你把remote_ip策略写在前面、默认策略写在后面,顺序反了就会让 192.168.1.0/24 之外的流量落到不该落的位置——按"越具体越靠前"排。改网段就改match remote_ip后面的 CIDR;要按域名强制,把它换成match sni api.internal.example.com即可。
验收与排障:curl 双场景 + mTLS 握手排障表
快速验收:无证书被拒、带证书通过
# 预期结果:握手失败(tls: certificate required) curl -v https://api.internal.example.com --cacert /etc/caddy/caddy.ca.cer # 预期结果:HTTP 200,返回 "OK" curl https://api.internal.example.com --cacert /etc/caddy/caddy.ca.cer \ --cert client.crt --key client.key两条命令一拒一通过,说明选择性策略按预期生效。注意--cert/--key指向由该 CA 签发的客户端证书,测试阶段可以让 Caddy PKI 模块 的内部 CA 顺手签发一张。
配置不生效?按这张 mTLS 握手排障表查
| 现象 | 最可能原因 | 排查命令或操作 | 相关源码或文档 |
|---|---|---|---|
handshake failure且所有客户端全拒 | pem_file路径错误或文件不可读 | ls -l /etc/caddy/caddy.ca.cer,确认 Caddy 进程用户可读 | ClientAuthentication 解析 |
| 证书正确但仍被拒 | 客户端证书链断裂:提交的是中间证书,未带全链 | openssl verify -CAfile /etc/caddy/caddy.ca.cer client.crt | trust_pool 模块解析 |
| 昨天正常今天全拒 | 服务器时间漂移,证书被判定过期 | chronyc tracking校时;openssl x509 -enddate -noout -in client.crt | 同上 |
| 内网流量没走强制策略 | 匹配器顺序问题:更宽的策略写在了前面 | 检查 Caddyfile 中connection_policy顺序,具体策略靠前 | 策略评估顺序 |
| 改完配置起不来或行为怪异 | Caddyfile 语法/字段错误 | caddy adapt --config Caddyfile --pretty查看展开后的 JSON | Caddyfile 适配测试用例 |
生产环境 mTLS 上线 checklist
- 服务端证书接入 Caddy 自动签发与轮换,客户端 CA 到期纳入日历提醒(Caddy PKI 模块)
- 客户端证书吊销走 CRL/OCSP 或缩短证书有效期,别等泄露后才发现
- 内网高频服务启用 TLS 会话复用,减少重复完整握手
- 打开握手日志,认证失败率异常时能按时间点回溯(日志模块)
- 把 mTLS 失败率纳入指标监控并设告警阈值(HTTP 指标)
- 条件逻辑超过两三个维度时改用 CEL 表达式匹配,避免 matcher 叠罗汉(CEL 匹配器)
- 变更走
caddy adapt预校验 + 灰度发布,不直接热加载生产配置
选择性 mTLS 的价值就一句话:敏感服务被双向认证保护,普通访问零摩擦——完整能力参见 Caddy TLS 模块。
【免费下载链接】caddyFast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS项目地址: https://gitcode.com/GitHub_Trending/ca/caddy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考