证书基础与一次 Traefik “Not secure” 排查实录

TL;DR

  • 现象:访问 https://traefik.example.local/dashboard/,Chrome/Safari 提示 Not secure;Firefox 正常。
  • 根因:macOS Trust 验证器不接受通配符证书 *.example.local 对子域名 traefik.example.local 的匹配(OpenSSL 接受,Apple Trust 不接受)。
  • 修复:签发显式域名证书 traefik.example.local,替代通配符赌博。
  • 一句话:验证器规则决定一切,不是"证书坏了"。

这不是"证书坏了"的问题,而是"证书被验证的方式不同"。在 Linux/OpenSSL 眼里没问题,在 macOS/Apple Trust 眼里却不合格。

本文分两部分:证书的基础知识 + 今天这起排查的完整链路。

证书基础(够用就好)

先把最关键的概念拎出来:

  • 证书链:叶子证书 → 中间证书(可选)→ 根 CA。浏览器只信任它认识的根 CA。
  • SAN(Subject Alternative Name):域名匹配完全看这里,CN 已经退居二线。
  • 信任库:macOS 有系统 Keychain;Firefox 有自己一套;Chrome/Safari 走系统信任。
  • 验证结果:同一张证书,在不同验证器里可能得出不同结论。

一句话总结:证书“正确”不等于“被当前验证器接受”。

问题现象与影响范围

访问 https://traefik.example.local/dashboard/#/,Chrome/Safari 都提示 Not secure

影响范围:

  • ❌ Chrome(依赖 macOS System Keychain)
  • ❌ Safari(依赖 macOS System Keychain)
  • ✅ Firefox(使用自己的证书存储,不受影响)

可复现前提:

  • macOS 版本:Sonoma 14.x+(其他版本可能表现不同)
  • CA 已导入 macOS Keychain 并设置为"始终信任"
  • 证书由 mkcert 签发(版本 1.4.4)
  • 服务端在 10.0.0.100

我希望先回答三个问题:

  1. 域名是否解析到了正确的服务器?
  2. 证书链是否完整、域名是否匹配?
  3. 浏览器信任库是否真的认可这张证书?

假设清单(按排除顺序)

在开始排查前,先列出可能的根因假设:

  • H1:CA 未正确导入系统信任库

    • 验证方法:检查 macOS Keychain,确认 CA 证书存在且为"始终信任"
    • 预期:如果 CA 不在信任库,所有浏览器都会报错(但 Firefox 正常,所以可能性低)
  • H2:证书链不完整(缺少中间证书)

    • 验证方法:openssl s_client -showcerts 查看证书链深度
    • 预期:自签 CA 通常只有两层(leaf + root),mkcert 默认就是完整的
  • H3:SAN 不包含目标域名

    • 验证方法:openssl x509 -text 查看 Subject Alternative Name 字段
    • 预期:如果 SAN 缺失或不匹配,OpenSSL 也会报错(但 curl 通过了,所以可能性低)
  • H4:验证器规则差异(OpenSSL vs Apple Trust)

    • 验证方法:用 security verify-cert 模拟 macOS 系统验证
    • 预期:这是最可能的根因 —— 不同验证器对通配符的 hostname matching 规则不同

接下来逐条验证。

验证流程可视化

下面这个图展示了4层验证的递进关系(不执行任何操作,仅用于理解):

bash
#!/usr/bin/env bash
# 验证流程:4层检查,定位到底是哪一层不认
cat <<'EOF'
┌─────────────────────────────────────────────────────────┐
│ Layer 1: DNS Resolution │
│ dig traefik.example.local +short │
│ ✅ Result: 10.0.0.100 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Certificate Content (SAN) │
│ openssl s_client + grep "Subject Alternative Name" │
│ ✅ Result: DNS:*.example.local, DNS:example.local │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 3: OpenSSL Validator (Linux/curl) │
│ curl -Iv https://traefik.example.local/ │
│ ✅ Result: SSL certificate verify ok │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Layer 4: Apple Trust Validator (macOS) │
│ security verify-cert -n traefik.example.local │
│ ❌ Result: Host name mismatch │
└─────────────────────────────────────────────────────────┘
关键发现:Layer 3 ✅ but Layer 4 ❌
→ 不同验证器对通配符的 hostname matching 规则不同
→ Chrome/Safari 依赖 Apple Trust (Layer 4)
EOF

排查链路(按顺序)

1) DNS 是否正确

bash
dig traefik.example.local +short

预期输出:

text
10.0.0.100

观察:DNS 指向正确,网络层没问题。

结论:排除网络连通性问题,继续验证证书层。

2) 证书链与 SAN 是否正确

bash
openssl s_client -connect traefik.example.local:443 -servername traefik.example.local -showcerts 2>/dev/null | openssl x509 -text -noout | grep -A2 "Subject Alternative Name"

预期输出(关键部分):

text
X509v3 Subject Alternative Name:
DNS:*.example.local, DNS:example.local

观察:SAN 包含通配符 *.example.local,按 RFC 6125 理论应该匹配 traefik.example.local

结论:证书字段本身没问题,排除 H3(SAN 缺失)

3) 用 OpenSSL 验证(通过)

bash
curl -Iv https://traefik.example.local/ 2>&1 | grep -E "SSL certificate verify|subject:"

预期输出:

text
* SSL certificate verify ok.
* subject: CN=*.example.local

观察:OpenSSL 验证器认为证书有效,hostname 匹配通过。

结论OpenSSL 认为没问题,排除 H2(证书链不完整)

4) 用 macOS 系统验证(失败)

bash
# 先导出证书到临时文件(如果还没导出)
echo | openssl s_client -connect traefik.example.local:443 -servername traefik.example.local 2>/dev/null | \
openssl x509 > /tmp/traefik-leaf.pem
# macOS 系统级验证
security verify-cert -c /tmp/traefik-leaf.pem -p ssl -n traefik.example.local -L -v

预期输出(关键错误):

text
...certificate verification failed
...Host name mismatch

观察:Apple Trust 明确拒绝了这个通配符对 traefik.example.local 的匹配。

结论Apple Trust 认为这个 SAN 不能匹配目标域名。这是关键转折:OpenSSL OK,但 Apple Trust 不 OK。Chrome/Safari 都依赖后者。命中 H4(验证器规则差异)

根因判断

根因不是"CA 没安装",也不是"证书链缺失",而是:

macOS 的主机名校验没有接受通配符证书对 traefik.example.local 的匹配。

这不是理论推测,是系统验证器的实际结果。

最小可行修复

不要赌通配符,直接签发显式域名证书。

bash
mkcert -cert-file traefik.example.local.pem -key-file traefik.example.local-key.pem "traefik.example.local"

如果你希望保留通配符与根域:

bash
mkcert -cert-file traefik.example.local.pem -key-file traefik.example.local-key.pem \
"traefik.example.local" "*.example.local" "example.local"

然后在 Traefik TLS 配置里替换证书,重启服务即可。

一张排查清单(以后直接复用)

  • DNS 指向是否正确(dig
  • SAN 是否包含目标域名(openssl x509 -text
  • OpenSSL 验证结果(curl -Iv
  • macOS 验证结果(security verify-cert
  • 浏览器是否使用系统信任库

复盘与预防

一句话教训

这次最值钱的证据: security verify-certHost name mismatch 输出 —— 它直接证明了问题不在"CA 信任"而在"hostname 验证规则"。

如果只能记住一件事: 不要用"在我电脑上能打开"来判断证书是否正确。不同验证器(OpenSSL / Apple Trust / Firefox NSS)对同一张证书可能得出不同结论。


预防性清单(下次签发证书时直接照做)

对于内网自签 CA(mkcert/自建 CA):

  • 优先签发显式域名,不要赌通配符的跨平台兼容性
  • 如果必须用通配符,用 security verify-cert 在 macOS 上提前验证
  • 保留签发命令记录(包含完整 SAN 列表),方便后续重新签发

对于公网证书(Let's Encrypt 等):

  • 申请时同时包含 example.com*.example.com(如果需要覆盖根域和子域)
  • 验证时至少在 3 个平台测试:Linux (OpenSSL) / macOS (Apple Trust) / Windows (Schannel)

验证模板(可复用脚本):

bash
#!/usr/bin/env bash
# 快速验证证书在多个验证器下的表现
DOMAIN="traefik.example.local"
echo "==> OpenSSL 验证"
curl -Iv https://${DOMAIN}/ 2>&1 | grep -E "SSL certificate verify|subject:"
echo "==> macOS 验证 (需要在 macOS 上运行)"
# 先导出证书
echo | openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} 2>/dev/null | \
openssl x509 > /tmp/cert-to-verify.pem
# 系统验证
security verify-cert -c /tmp/cert-to-verify.pem -p ssl -n ${DOMAIN} -L -v
echo "==> 检查 SAN"
openssl s_client -connect ${DOMAIN}:443 -servername ${DOMAIN} </dev/null 2>/dev/null | \
openssl x509 -text -noout | grep -A2 "Subject Alternative Name"

最终结论

证书问题最容易被"我已经导入 CA 了"这句话带偏。 真正决定一切的,是验证器的规则,而不是你的主观印象。

这一次的结论非常清晰: 用显式域名证书替代通配符,Chrome/Safari 立刻恢复安全状态。


Comments