---
purpose: SAN 闭包发现（证书 ↔ 域名图遍历、通配符展开、跨 zone 硬边界、影子证书溯源）
loaded_by: SKILL.md §3 Phase 1 资产盘点按需引用
blocking: true
---

# SAN 闭包发现

> 资产盘点不是"一次性快照"，而是**图遍历直到不动点**：证书 ↔ 域名是一张双向图，
> 从一个种子（一个域名或一张证书）出发，必须迭代展开直到"无新发现"才算完整盘点完成。

---

## 1. 图模型与遍历动作

```
         绑定关系                   SAN 列表
Domain ──────────→ Certificate ──────────→ [Domain, Domain, Domain...]
   ↑                                                │
   │                                                │ 每个新 Domain 继续反查
   └────────────────────────────────────────────────┘
                   新一轮遍历
```

**每一轮迭代包含两个动作**：

1. **域名 → 证书**：对每个已知域名，查询当前实际绑定的所有证书（公网 TLS 握手 + CT 日志 + 云 API 绑定关系）
2. **证书 → 域名**：对每张已发现证书，读出 SAN 清单中的所有 FQDN 和通配符

---

## 2. 收敛条件与边界

Agent 不能无限递归。硬上限：

| 维度 | 默认上限 | 触达上限后的行为 |
|---|---|---|
| 迭代深度 | ≤ 3 轮 | 停下，呈现当前发现清单，征询客户"是否继续深挖" |
| 涉及 DNS zone 数 | ≤ 5 个 | 停下，呈现新发现的 zone 列表，征询客户"是否纳入本次变更" |
| 涉及证书数 | 软上限 ≤ 10 张 | 超出时高亮提示，让客户判断是否存在"僵尸证书"或需要范围收缩 |

**收敛判定**：某一轮迭代**没有**发现新的域名、新的证书、新的 zone → 宣告闭包收敛完成。

---

## 3. 证书类型对闭包遍历的影响

| 证书类型 | SAN 结构 | 闭包遍历行为 |
|---|---|---|
| 单域名（SD） | 1 个 FQDN | 最简单，仅需"域名 → 证书 → 可能的其他 SAN"一轮即可收敛 |
| 通配符（Wildcard） | **1 个通配符**（可能附 1 个裸域）| **需要展开子域**（按下文"通配符展开分层"执行）|
| 多域名（SAN / UCC，单 zone） | **多个确定 FQDN**，**全部位于同一 DNS zone** | 每个 SAN 独立反查，**无需展开** |
| 多域名跨 zone（SAN / UCC，多 zone） | **多个确定 FQDN**，**跨 2+ DNS zone** | 每个 zone 独立授权 + 每个 SAN 独立反查（详见下方跨 zone 硬边界）|

> ⚠️ **事实校准**：多域名证书（SAN/UCC）的 SAN 是多个**确定的 FQDN**，不是多个通配符。
> "多通配符混合"证书现实中非常少见，本 Skill 不按主流形态设计。

---

## 4. 通配符展开分层规则（遇到 T2 通配符 SAN 时触发）

发现一个通配符 SAN（如 `*.api.example.com`）时，按"CT 先行、DNS 精补"分层执行：

| 层级 | 动作 | 授权要求 | 作用 |
|---|---|---|---|
| **第 1 层：CT 日志反查** | 查询 crt.sh / Cert Spotter，列出曾被签发给 `*.api.example.com` 范围内的所有 FQDN | 零授权（CT 是公共日志）| 广度发现，**可跨 zone 自由查询**，发现未在 DNS 活跃但历史签发过的域名 |
| **第 2 层：DNS 探针** | 对第 1 层发现的 FQDN，结合 `_acme-challenge` / A / AAAA 记录确认当前活跃状态 | **每个 zone 独立授权**（不继承其他 zone）| 精度补全，区分"历史签发但已下线" vs "当前活跃服务" |

**重要**：对多域名证书（T3/T4）**不触发**通配符展开，每个 SAN 本身就是确定 FQDN。

---

## 5. 跨 zone 闭包的硬边界

当闭包遍历跨越多个 DNS zone（例：SAN 同时包含 `api.example.com` 和 `cdn.example.cn`）：

- **每个新 zone 必须单独征询 DNS 只读授权**；`example.com` 的授权**不得继承**到 `example.cn`
- 跨 zone 发现的新证书 / 新域名，Agent 停下，批量呈现给客户，客户勾选后才继续深挖
- **CT 日志反查不受 zone 限制**（因为是公共日志），可在所有已发现 SAN 上自由执行
- Agent 不按"域名后缀 / 国别 / 语种"推断合规属性；合规差异（如国密、境外隐私法规）**必须由客户明示**才能分组处理

---

## 6. 新发现的批量呈现格式

每轮迭代结束，Agent 以如下格式向客户汇报新发现，让客户一次性勾选：

```
【本轮发现】

新证书（N 张）：
  [ ] Cert-B · 颁发给 api-v2.example.com · SAN 还包含 [foo.example.com]
      → 勾选后继续遍历其 SAN
  [ ] Cert-C · 颁发给 *.backend.example.com · 通配符
      → 勾选后展开子域

新域名（M 个）：
  [ ] api-v2.example.com  · 来自 Cert-A 的 SAN
  [ ] foo.example.com     · 来自 Cert-B 的 SAN

新 DNS zone（K 个，需单独授权）：
  [ ] example.cn  · 首次出现，源自 Cert-D 的 SAN
      → 如需 Agent 探针，请提供该 zone 的只读授权

【请勾选要继续纳入本次变更盘点的条目】
【未勾选的条目将停留为「已发现但暂不深挖」状态，不影响本次变更】
```

---

## 7. 隐式决策禁令

- Agent **绝不**在客户未确认前把新发现条目合并到资产清单正文；所有未勾选条目在 L2/L1 交付物中**标记为【已发现但暂不深挖】**
- Agent **绝不**在客户未确认前自动使用其他 zone 的授权去访问新 zone
- Agent **绝不**因为 CT 日志反查"零授权"就扩大证书 → 域名的推断范围超出收敛上限

---

## 8. 绑定点（binding）统一定义

"绑定点"是盘点与部署的**最小管理单元**，贯穿 Phase 1-6 的粒度基础：

> **绑定点 = 可以通过 API 或控制台替换证书的最小管理单元**

| 资源类型 | 绑定点定义 | 反例 |
|---|---|---|
| CDN | 1 个 CNAME 域名 = 1 个绑定点 | 不是每台 CDN 边缘机 |
| 负载均衡 | 1 个监听器（Listener）= 1 个绑定点 | 不是整个 LB 实例 |
| K8s | 1 个 Ingress 或 1 个 TLS Secret = 1 个绑定点 | 不是一个 namespace |
| Nginx | 1 个 `ssl_certificate` 指令 = 1 个绑定点 | 不是一整个进程 |
| Java / Go 应用 | 1 个应用实例持有的 JKS/PFX 文件 = 1 个绑定点 | 不是一个主机 |
| API 网关 | 1 个自定义域名绑定 = 1 个绑定点 | 不是整个网关 |

---

## 9. cert_role（证书角色）维度

同一批域名可能涉及多张证书，扮演不同角色。Phase 1 盘点必须明示 cert_role：

| cert_role | 定义 | 举例 | 验证层级裁剪 |
|---|---|---|---|
| `edge` | 面向终端用户的边缘证书 | CDN / WAF 前端证书 | L1-L6 全验 |
| `origin` | CDN 回源用的源站证书 | 源站 Nginx 证书 | 仅 L1 + L3（跳过 L4 客户端 / L5 CT）|
| `internal` | 仅内网服务间通信 | K8s service mesh / 内部 API | 仅 L1-L3 |
| `mtls-server` | 双向 mTLS 服务端 | API 网关对第三方 mTLS | 双端同步 + 客户端证书链验证 |
| `mtls-client` | 双向 mTLS 客户端 | 调 SaaS 时的客户端证书 | 同上 |

> 📌 **为什么重要**：`origin` 证书换了 CDN 用户看不到，不需要跑 L4/L5；`edge` 证书换了才需要。
> 不区分 `cert_role` 会导致**过度验证**或**漏验证**。详见 `phases/02-scope-lock-and-reflow.md § 7`。

---

## 10. SAN 极少时的反向扩展启发式

当 SAN 数 ≤ 3 时，传统闭包发现 1 轮就收敛，价值缩水。Agent 应启用**反向扩展启发式**：

- **启发式 1**：DNS zone 内同 IP 子域扫描（其他 FQDN 可能用同一张证书）
- **启发式 2**：CT 日志历史证书扫描（主域过去签过哪些证书，可发现影子/兄弟证书）
- **启发式 3**：CT 日志旁系证书扫描（主站 + 移动站 + WAP 站常归同团队管）

触发条件：
- SAN ≤ 3 且非 Fast Path 场景 → 默认启用
- Fast Path 场景 → 询问客户"要不要我顺手做一下反向扫描（~5 min）"

---

## 11. 部分授权阻塞的降级策略

多 zone 场景下，部分 zone 授权未就位不能阻塞整体闭包。降级策略：

- **已授权 zone**：正常 DNS 探针闭包
- **未授权 zone（授权等待中）**：走 **CT log 外部视图**（crt.sh）替代 DNS 探针，资产清单标记为 **【CT-only · 授权等待中】**
- **未授权 zone（凭证彻底丢失）**：生成"授权缺口报告"，触发决策上移（可能资产瘦身）
- CT 日志反查**不受 zone 限制**（公共日志），可在所有已发现 SAN 上自由执行

---

## 12. 影子证书溯源工作流

Phase 1/2 CT 查询发现"非种子证书闭包内"的证书时，按以下流程处置：

```
Step 1  按 CA 查签发账号主体 → 明确"是谁签的"
Step 2  内部通告（邮件 / 企微 / Slack），限期 48h 内认领
Step 3  三路分流：
        - 认领 → 合法影子（纳入 Scope Lock 的 defer 队列）
        - 逾期未认领 → 上报安全组（潜在盗签 / 供应链 / 离职员工遗留）
        - 可疑 → 按安全组流程评估吊销
Step 4  记录溯源结果到资产清单
```

完整流程见 `phases/02-scope-lock-and-reflow.md § 3`。
