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
我希望先回答三个问题:
- 域名是否解析到了正确的服务器?
- 证书链是否完整、域名是否匹配?
- 浏览器信任库是否真的认可这张证书?
假设清单(按排除顺序)
在开始排查前,先列出可能的根因假设:
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 是否正确
bashdig traefik.example.local +short
预期输出:
text10.0.0.100
观察:DNS 指向正确,网络层没问题。
结论:排除网络连通性问题,继续验证证书层。
2) 证书链与 SAN 是否正确
bashopenssl s_client -connect traefik.example.local:443 -servername traefik.example.local -showcerts 2>/dev/null | openssl x509 -text -noout | grep -A2 "Subject Alternative Name"
预期输出(关键部分):
textX509v3 Subject Alternative Name:DNS:*.example.local, DNS:example.local
观察:SAN 包含通配符 *.example.local,按 RFC 6125 理论应该匹配 traefik.example.local。
结论:证书字段本身没问题,排除 H3(SAN 缺失)。
3) 用 OpenSSL 验证(通过)
bashcurl -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 的匹配。
这不是理论推测,是系统验证器的实际结果。
最小可行修复
不要赌通配符,直接签发显式域名证书。
bashmkcert -cert-file traefik.example.local.pem -key-file traefik.example.local-key.pem "traefik.example.local"
如果你希望保留通配符与根域:
bashmkcert -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-cert 的 Host 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 -vecho "==> 检查 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