返回博客

macOS 浏览器拒绝内网 HTTPS,curl 却完全正常——397 天叶证书硬限制

搭一套内网 HTTPS:自建根 CA、安装到 macOS 系统钥匙串、配好 /etc/hosts,看似万事俱备。打开 Safari——「无法与服务器建立安全连接」。换 Chrome——ERR_CONNECTION_CLOSED。重启、重装证书、换浏览器,统统无效。

最诡异的地方在于,命令行工具一个都没报错:

curl --cacert rootCA.crt https://internal.dev/   # 200 OK
openssl s_client -CAfile rootCA.crt \
  -connect internal.dev:443 -servername internal.dev
# Verify return code: 0 (ok)
security find-certificate -c "My Root CA" \
  /Library/Keychains/System.keychain
# (证书存在)

链路合法、CA 信任、SAN 齐全——但浏览器就是不认。

现象

  • Safari:「Safari 浏览器无法打开页面 · 无法与服务器建立安全连接」
  • Chrome / Edge / Arc(macOS):ERR_CONNECTION_CLOSED
  • curl / wget / openssl s_client:全部正常
  • Linux 上的 Chrome / curl:正常
  • 证书包含正确的 v3 扩展(BasicConstraints CA:FALSE、EKU serverAuth、SAN DNS 条目)

根因

Apple 在 2020 年 9 月发布技术文档 HT211025,规则生效日期 2020-09-01:

TLS 服务器叶证书的有效期(notAfter - notBefore)不得超过 398 天。违反规则的证书在 Apple 操作系统上不被信任。

实际上,398 是宣传数字,脚本里安全上限是 397 天(留 1 天缓冲)。

这条规则影响范围:

  • macOS 11 Big Sur 及以上
  • iOS 14 及以上
  • Safari 14 及以上
  • macOS 上的 Chrome / Edge / Arc / Brave(它们在 macOS 上走 CertVerifyProcMac,底层调用 Apple 的评估器,同样受约束)
  • Tauri / Electron 内嵌 WKWebView 的 macOS 应用

不受影响的:

  • curl / wget / openssl s_client(自带 BoringSSL / OpenSSL,不走 NSURLSession)
  • Linux 上的任何浏览器
  • 服务端 TLS 验证(Node.js、Python、Go 等)

这就是为什么命令行全绿、浏览器全红——问题不在证书本身,在 Apple 的评估器

坑:网上「简易内网 CA」教程大多预设 2 年或 10 年叶证书

绝大多数 2020 年前的教程默认用 -days 3650(10 年)或 -days 730(2 年),都超过 397 天。直接复制这些命令,在 macOS 上会被浏览器无声地拒绝,报错信息完全看不出是「有效期太长」。

正解

只需重新签发叶证书,有效期改为 397 天以内。根 CA 完全不需要动(根 CA 可以保持 10 年甚至更长)。

# 复用现有根 CA,只重签叶证书
openssl x509 -req -in server.csr \
  -CA rootCA.crt -CAkey rootCA.key -CAcreateserial \
  -out server.crt -days 397 -sha256 \
  -extensions v3_req -extfile cert.cnf
 
# 重建 fullchain
cat server.crt rootCA.crt > fullchain.pem
 
# 热重载 nginx(不需要重启服务)
nginx -s reload

如果维护的是 gen-ca.sh 类脚本,把变量改掉:

# 改之前
LEAF_DAYS=$((365 * 2))   # ❌ 730 天,macOS 浏览器拒绝
 
# 改之后
LEAF_DAYS=397            # ✅ 恰好在 Apple 上限以内
正解:叶证书 ≤ 397 天,根 CA 不受限

重签叶证书后,Safari 和 Chrome 均无需重启,刷新页面即可恢复绿锁。根 CA 有效期不受此规则约束,无需重新安装信任。

运维后果:每年续签

397 天约等于 13 个月。上线后需要设日历提醒,在到期前 30 天重新执行签发脚本并热重载 TLS 终止器。如果想彻底自动化,可以把内网 CA 换成支持 ACME 协议的方案(如 step-ca),Caddy 等反向代理可以对接 ACME 自动续期,从此不再手动操心 397 天的窗口。

调试提示:直接检查叶证书日期
openssl x509 -in fullchain.pem -noout -dates
# notBefore=Jun  1 00:00:00 2025 GMT
# notAfter =Jun  2 00:00:00 2027 GMT  ← 超过 397 天,必挂

浏览器的报错(「无法建立安全连接」/ ERR_CONNECTION_CLOSED)没有任何提示说明是有效期问题,必须手动算 notAfter - notBefore

一句话外卖

macOS 上 curl 通而浏览器挂,先查叶证书有效期是否超过 397 天——Apple 的评估器悄悄执行了命令行工具从不理会的这条规则。