运维实践
CAA 记录详解:限制哪些 CA 能为你的域名签发证书
CAA(Certification Authority Authorization,证书颁发机构授权)是一条 DNS 记录,告诉各家证书颁发机构:谁可以为这个域名签发证书。每个公开信任的 CA 在签发 TLS 证书前都必须查询 CAA,记录不允许就必须拒绝。现行规范是 RFC 8659,RFC 8657 又增加了把签发限定到某个 ACME 账户或某种验证方式的参数。
CAA 能做什么、不能做什么
CAA 是 CA 在签发时对自己执行的约束。它把“任何一家受信任 CA 的验证漏洞都可能为你的域名签出证书”缩小为“只有你列出的几家”。
它的边界也很清楚:
- 浏览器不看 CAA:浏览器从不检查 CAA,某家 CA 无视你的记录签出的证书,在被吊销之前照样被信任。要发现这类签发,请监控 证书透明度。
- 只在签发时生效:增加或修改记录不影响已有证书。
- 不是身份验证:CAA 只限定由谁签发,CA 仍然要做 域名控制验证。
CAA 检查自 2017 年 9 月 8 日起通过 CA/Browser Forum 第 187 号投票成为强制要求。现在 Baseline Requirements 要求对证书里的每个域名都检查 CAA,而且自 SC-067 投票起,这项查询还要从多个网络视角交叉验证。
记录格式
每条 CAA 记录由三部分组成:标志(flags)、标签(tag)和值(value)。
example.com. 3600 IN CAA 0 issue "letsencrypt.org"
; | | |
; 标志 标签 值:CA 域名 [; 参数]
| 标签 | 作用 | 定义 |
|---|---|---|
issue |
授权某个 CA 签发该域名的任何证书;没有 issuewild 时也管通配符 |
RFC 8659 |
issuewild |
授权签发通配符证书;一旦存在,就取代 issue 对通配符的效力 |
RFC 8659 |
iodef |
CA 拒绝签发时可以向哪里报告,mailto: 或 https: 地址 |
RFC 8659 |
issuemail |
授权 CA 为该域名下的邮箱签发 S/MIME 证书 | RFC 9495 |
标志几乎总是 0。设为 128 表示这条记录是关键的(critical):不认识这个标签的 CA 必须拒绝签发。只有当一个未知标签确实必须阻止签发时才这么用。
CA 域名留空表示禁止签发。0 issue ";" 意味着任何 CA 都不能为该域名签发证书,适合停放域名或只收邮件的子域名。
CA 如何找到“相关记录集”
CA 从证书里的确切域名开始查 CAA,没有就往上一级找,遇到第一个有 CAA 记录的名字就停下。这叫“向上查找”(tree climbing):
申请:shop.eu.example.com
1. shop.eu.example.com CAA? 无
2. eu.example.com CAA? 无
3. example.com CAA? 0 issue "letsencrypt.org" <- 相关记录集,停止
(找到后不会再查 com 和根)
由此得出三点。第一,在主域名上放一条记录就能覆盖所有子域名。第二,子域名上的记录会完全覆盖父域名的记录,两者不会合并。第三,CAA 查询和普通 DNS 查询一样会跟随 CNAME:如果 blog.example.com CNAME 到某个托管服务商,那么适用的是服务商那边的 CAA 记录。
通配符从基础域名开始查:申请 *.example.com 时,CA 查看的是 example.com。
issue 与 issuewild 的组合
issuewild 的存在,是为了让普通证书和通配符证书可以授权给不同的 CA。这里很容易搞错,下表是 CA 的判断方式:
| 相关名字上的记录 | 普通证书 | 通配符证书 |
|---|---|---|
只有 issue "ca-a.example" |
ca-a 可签 | ca-a 可签 |
issue "ca-a.example" + issuewild "ca-b.example" |
只有 ca-a | 只有 ca-b |
issue "ca-a.example" + issuewild ";" |
ca-a 可签 | 谁都不行 |
只有 issuewild "ca-b.example" |
任何 CA | 只有 ca-b |
只有 issue ";" |
谁都不行 | 谁都不行 |
同一标签的多条记录是叠加关系:两条 issue 记录分别写两家 CA,两家都被授权。
accounturi 与 validationmethods
RFC 8657 让你把授权从“这家 CA”收窄到“这家 CA 的这个账户、用这些验证方式”。两个参数都写在 CA 域名之后,用分号分隔,Let's Encrypt 都支持。
; 只允许我的 Let's Encrypt 账户签发,且只能用 dns-01
example.com. IN CAA 0 issue "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890; validationmethods=dns-01"
- accounturi 把签发绑定到一个 ACME 账户 URL。即使有人暂时劫持了你的 Web 服务器或 DNS,只要拿不到 ACME 账户私钥,就签不出证书。Let's Encrypt 的账户 URI 形如
https://acme-v02.api.letsencrypt.org/acme/acct/后接数字账户 ID,ACME 客户端注册后会显示。 - validationmethods 限制 CA 可用的验证方式。ACME 下可用的值就是挑战名称:
http-01、dns-01、tls-alpn-01。只允许dns-01,就能挡住只控制了你 Web 服务器的攻击者。
这两个参数威力大,也很容易误伤。测试环境和生产环境的账户 URL 不同,重装服务器可能注册了新账户,不同团队也可能各用各的账户。正确做法是为每个合法账户各写一条 issue 记录,而不是替换掉唯一的那条。账户和挑战的原理见 ACME 协议。
常见 CA 的 CAA 标识
issue 记录里填的是 CA 自己的标识域名,而不是你购买时看到的品牌或经销商。国内云厂商和代理商销售的证书实际来自少数几家签发 CA,请查看现有证书的 Issuer 或 CA 官方文档。ACME CA 还会在目录的 caaIdentities 中公布这个值。
| CA | CAA 值 |
|---|---|
| Let's Encrypt | letsencrypt.org |
| Google Trust Services | pki.goog |
| DigiCert | digicert.com |
| Sectigo | sectigo.com |
| GlobalSign | globalsign.com |
| SSL.com | ssl.com |
| Amazon | amazon.com |
有些 CA 还接受历史品牌的标识,ZeroSSL 的证书由 Sectigo 签发。没把握时先向 CA 确认,再添加限制性记录。
配置示例与查询
典型配置是授权一家主 CA、一家备用 CA,再加上报告地址。BIND 风格的区文件写法:
$ORIGIN example.com.
@ 3600 IN CAA 0 issue "letsencrypt.org"
@ 3600 IN CAA 0 issue "pki.goog"
@ 3600 IN CAA 0 issuewild "letsencrypt.org"
@ 3600 IN CAA 0 iodef "mailto:security@example.com"
; api 托管在云平台上,证书由 Amazon 签发
api 3600 IN CAA 0 issue "amazon.com"
; 这个名字永远不该有证书
legacy 3600 IN CAA 0 issue ";"
在 DNSPod(腾讯云 DNS 解析)或阿里云云解析的控制台里,添加记录时“记录类型”选 CAA,“主机记录”填 @ 或子域名,“记录值”填 0 issue "letsencrypt.org" 这样的完整内容。不同面板对引号的处理略有差异,保存后一定用 dig 核对。
dig +short CAA example.com
0 issue "letsencrypt.org"
0 issue "pki.goog"
0 issuewild "letsencrypt.org"
0 iodef "mailto:security@example.com"
对 api.example.com,CA 在 api 自己的记录集处就停下,不会再看 example.com。想知道 CA 对某个名字实际会用哪组记录,可以自己逐级往上查,或者用 CAA 检测,它执行同样的向上查找。CAA 生成器 可以为常见 DNS 服务商生成引号正确的记录。
在记录里保留一家备用 CA 是很实用的做法:主 CA 出故障或政策变化时,你可以直接切换,不用等 DNS 修改生效。
DNS 错误与 DNSSEC
CAA 查询“失败”和“查到没有记录”是两回事。权威服务器返回 SERVFAIL、超时,或者对 CAA 查询回 NOTIMP 时,CA 无法判断是否存在禁止签发的记录,通常会拒绝。Baseline Requirements 只在三个条件同时满足时允许 CA 把查询失败视为允许:失败发生在 CA 自身基础设施之外、至少重试过一次、该域名没有通往根的 DNSSEC 验证链。
Let's Encrypt 文档列出的常见原因:
- DNSSEC 签名有缺陷,只在“空响应”时出错,而 CAA 查询的答案恰恰常常是空的。
- 名称服务器对不认识的查询类型返回 NOTIMP,而不是空的 NOERROR,违反 RFC 1035。
- 名称服务器前面的防火墙丢弃了陌生类型的查询。
DNS 服务商不需要提供 CAA 编辑功能也能兼容,只要能对 CAA 查询正确返回空响应即可。一些老旧的自建 DNS 或小服务商在这点上出过问题,遇到持续的 CAA 报错可以换用主流解析服务。
CAA 复查与时间
CA 在接近签发的时刻检查 CAA,而不只是在验证域名时查一次。Baseline Requirements 要求 CA 在 CAA 检查后 8 小时内(或记录 TTL 更长时在 TTL 内)完成签发,这也是 Let's Encrypt 在 tlsserver 和 shortlived 配置下把授权复用期限制为 7 小时的原因,可以省去单独的复查。实际效果是:修改 CAA 后,等 DNS 缓存过期,下一次订单就会按新记录执行;迁移前先把 TTL 调低。
安全上线 CAA 的步骤
正确的顺序是先盘点,后收紧。漏掉一个合法签发方的记录,会在几周后对方续期时突然出错,而那时已经没人记得改过 DNS。
- 盘点:在证书透明度日志里搜索过去一年为主域名和子域名签发的所有证书,例如用 CT 查询,记下每家签发 CA,包括 CDN、企业邮箱、状态页等 SaaS 服务用到的。
- 先宽后严:用
issue列出找到的所有 CA,再加上iodef,方便支持报告的 CA 告诉你被拒绝的请求。 - 按需覆盖:第三方托管的子域名单独设置 CAA 记录集,而不是放宽父域名。
- 逐步收紧:一次只加一项
issuewild、accounturi或validationmethods,每次修改后在测试环境触发一次续期。 - 定期复查:新服务会不断接入,每个季度重新盘点一次 CT 记录。
常见错误
大多数 CAA 故障都源于忘了记录会作用于所有子域名、所有证书来源。发布限制性记录前对照检查:
| 错误 | 后果 | 解决 |
|---|---|---|
| 填了经销商或品牌名而不是签发 CA | 商业证书在签发环节失败 | 改成签发 CA 的标识 |
| 忘了某个为子域名签证书的 CDN 或 SaaS | 该平台续期失败 | 加上它的 CA,或在该子域名上单独设 CAA |
没有通配符需求却加了 issuewild |
通配符续期被挡 | 删掉,或改成正确的 CA |
| accounturi 只写了测试或旧账户 | 生产环境续期失败 | 每个账户各写一条 |
| 引号、分号写错 | 记录被忽略或面板拒绝保存 | 用工具生成并用 dig 核对 |
因 CAA 导致签发失败时,ACME 错误类型是 caa,消息里会带上域名,排查步骤见 CAA 记录导致无法签发。
常见问题
一定要加 CAA 记录吗?
不是必须的。没有 CAA 记录时,任何公开信任的 CA 只要验证了域名控制权都可以签发。CAA 是一道可选的保险:万一攻击者能在别的 CA 那里骗过域名验证,或者公司其他团队从你不认可的 CA 买了证书,它能挡住签发。
加了 CAA 会影响已经签发的证书吗?
不会。CA 只在签发时检查 CAA,浏览器从不看 CAA。加上严格的记录后,现有证书照常使用,但下次从未列入的 CA 续期时会失败。
Let's Encrypt 应该填什么?
填 0 issue "letsencrypt.org"。只有当通配符和普通证书要用不同规则时,才需要另加 0 issuewild "letsencrypt.org"。每个 ACME CA 也会在目录的 caaIdentities 里公布这个值。
阿里云、腾讯云买的证书,CAA 该写谁?
要写实际签发证书的 CA,而不是云厂商或证书品牌。云厂商售卖的证书来自 DigiCert、GlobalSign、Sectigo 等多家 CA,不确定时查看已有证书的 Issuer 字段,或在 CA 的官方文档里核对 CAA 标识。