背景
短域和正式域都建好之后,用户的第一批反馈里很快出现一类:浏览器打开 https://api.zhangsan.user.xx,证书报错。
这是内网私有域名的宿命问题:公共 CA 不可能给你签证书。这一篇把这个问题从原理到解法讲清楚——它是个支线话题,但坑的密度超出我预期,值得一篇。
公共 CA 的两道墙
“用 Let’s Encrypt 签一个不就行了?"——不行,而且不行有两层原因,一层比一层硬。
第一层:技术墙。申请证书时 CA 要验证你拥有这个域名。即使是 DNS-01 验证(在 TXT 记录里放 challenge),也要求 CA 从公网能查到这条 TXT。而 xx 是私有 TLD,公网上根本不存在——CA 的解析器查不到,验证无从谈起。
第二层:政策墙,更硬。CA/Browser Forum 从 2015 年起就禁止公共 CA 为 Internal Name(内网名称、私有 TLD、IP 地址)签发证书。这是合规红线,写在全球 CA 的基线要求(Baseline Requirements)里——就算某个 CA 技术上能验证,它也不敢签,签了会被浏览器踢出信任库。
两层墙的结论一致:私有域名的证书,公共 CA 这条路是死的。
自救:自建 ACME CA
墙堵死了正门,但 ACME 给了我一个启发:它是开放协议,不是 Let’s Encrypt 的专利。既然协议公开,就可以在内网自建一个 ACME CA——我用的是 smallstep 的 step-ca。
step-ca 部署很轻,而且 ACME 客户端生态零改造:certbot、acme.sh、Traefik、Caddy 全都天然支持自定义 ACME 目录,把目录指向内网 step-ca 就能自动签发。签发这个问题,一晚上就解决了。
然后我撞到了真正的问题。
私有 CA 的真正成本:信任分发
签出来的证书,浏览器不认、curl 报错、Java 应用直接抛异常。原因很简单:step-ca 的根证书不在任何客户端的信任库里。
step-ca 只解决签发,完全不解决信任。
而"让内网所有客户端信任一个根证书”,是个比签发大一个数量级的工程。它的碎坑在于信任库根本不是一个东西,而是一堆各自为政的实现:
| 运行时 | 信任库位置 | 坑 |
|---|
| 操作系统 | /etc/ssl/certs | 相对标准,好办 |
| Java | cacerts(JRE 自带) | 不读系统信任库,容器镜像里更难办 |
| Python | certifi(pip 包) | 不读系统信任库,升级会被覆盖 |
| Node.js | 编译进二进制 | 需要 NODE_EXTRA_CA_CERTS 环境变量 |
| Go | 读系统信任库 | 但 scratch 镜像里什么都没有 |
| Firefox | 独立信任库 | 不读系统信任库 |
每个 JDK 容器、每个 Python 镜像、每个 Firefox 用户,都是一次单独的分发动作。
更清醒的认识来自一个现成案例:Let’s Encrypt 自己铺信任铺了六年。2015 年成立时没有设备信任它,它借道 IdenTrust 的交叉签名才被接受;2021 年借道的根(DST Root X3)到期独立,又直接导致老 Android 设备大面积瘫痪——即使六年之后,它仍然要为历史遗留付账。
信任库是"时间 × 渠道"的产物。 Let’s Encrypt 有全球浏览器厂商配合尚且如此,一个内网私有 CA 想把根证书铺满所有运行时,成本和收益完全不成比例。
务实突围:泛域名证书双轨
自建 CA 的信任分发走不通,就得回到公共 CA——但公共 CA 只给公网域名签。解法因此变成一句话:
给公网域名签泛证书,让内网解析把它指向内网 IP。
具体做法:给 *.devops.example.com 在公网 CA 签一张泛域名证书(公司有公网 DNS,DNS-01 验证没问题),然后在内网权威 DNS 里把 payment-api.devops.example.com 解析到内网 IP。浏览器访问时,域名是真的、证书是真的,天然信任——HTTPS 问题在没有触碰任何信任库的情况下消失了。
这个方案还有两个意外的好处:
- 隐私:证书透明度(CT)日志会公开所有签发的证书。泛证书在 CT 日志里只暴露
*.devops.example.com 一条,具体有多少个子域、叫什么,外界看不到。 - split-horizon:公网 DNS 里根本不放这些名字的 A 记录,外界连"这个名字存在"都不知道;内网权威才持有真实解析。
但有三个坑必须说清楚:
- 私钥扩散:所有服务共用一张泛证书,私钥要分发到每个用它的服务上。任何一处泄露,全部服务受影响。私钥的分发和轮换要有纪律。
- 续期必须自动化:泛证书全公司共用,过期是大面积故障。ACME 自动续期是唯一选项,而且要监控续期本身。
- 泛证书只盖一级子域:
*.devops.example.com 能盖 api.devops.example.com,盖不了 api.test.devops.example.com(那是两级)。这条限制会反过来决定你的命名设计——层级不能随心所欲地深,要服务名字的深度服从证书的覆盖范围。
最终格局
方案落定后,两个域名轨的 HTTPS 策略分道扬镳:
xx 短域:保留服务间 HTTPS(内网机器之间可以用 step-ca,信任分发到受控的机器上是可行的),放弃浏览器场景。反正个人空间的临时试验,浏览器报错点一下也就过去了。devops.example.com:公网泛证书 + 内网解析,全场景 HTTPS,浏览器、客户端、服务间通吃。
这个分工又和空间模型自洽了:正式的、需要被好好访问的,本来就该走正式域;短域承载的临时试验,容忍证书的粗糙。
最后
证书这一篇的三个结论:
ACME 是协议,不是 Let’s Encrypt 的专利——自建 CA 解决签发很容易。
私有 CA 的真正成本是信任分发,信任库是时间和渠道的产物,内网铺不起。
泛证书只盖一级子域——这条技术限制会反过来参与你的命名设计。
域名有了,HTTPS 通了。下一个问题是这些域名指向的 IP 本身:谁在用哪个段、这个 IP 原来是谁的——这是 IPAM 的故事,下一篇。