题干与适用场景
公司每天从 GitHub Actions 和自托管 Runner 构建数千个容器镜像。请设计一套服务,为每个镜像生成并验证可追溯的构建 Provenance,只有满足组织策略的制品才能部署。说明数据模型、签发与验证、信任根、密钥轮换、Runner 被攻陷、离线部署、回滚和可观测性。
公开的供应链安全招聘设计题已经把“为数千个服务和混合 Runner 生成可验证 Provenance”作为系统设计提示。SLSA 把 Provenance 生产、分发、验证和构建平台评估分开,Sigstore 文档则要求验证签名、身份、发行者和制品摘要。题目核心是建立可审计的信任链,不是给镜像仓库加一个签名字段。
面试官考察点
- 区分制品摘要、Provenance 声明、签名、透明日志和部署策略。
- 把不可信的源代码、依赖、构建配置和 Runner 纳入威胁模型。
- 让证明绑定不可变制品,而不是可变标签或构建结果之外的文本。
- 定义验证失败、密钥轮换、撤销、离线和回滚的明确语义。
- 估算写入、查询、缓存、保留和审计成本,并设置策略版本和观测指标。
回答前需要澄清的问题
- 目标是阻止未授权制品部署,还是只提供审计?默认部署准入是强门槛,审计证据独立保存。
- 制品类型只有容器吗?默认先支持 OCI 镜像,抽象接口可扩展到二进制和包。
- 构建环境都能联网吗?默认有离线或受限网络环境,需要预缓存信任根和证明。
- 签名身份按人、仓库、工作流还是构建平台?默认使用工作流身份和受控构建器,禁止只信任提交者邮箱。
- 需要保留多久?先让面试官给合规和审计期限,再推导对象存储、索引和日志保留策略。
30 秒回答框架
我会把流程拆成四个边界:构建器生成包含源版本、依赖、构建参数和构建器身份的 Provenance;签发服务把声明绑定到不可变制品摘要并写入透明日志;验证器在部署前检查签名链、身份、摘要、策略版本和时间窗口;准入控制器决定允许、隔离或拒绝。源代码、依赖和 Runner 都是不可信输入,不能把“CI 成功”当作证明。策略版本、失败原因和回滚版本要可追踪,离线环境使用已固定的信任根和证明缓存。
分步骤深入解答
第一步:建立信任边界和威胁模型
列出源代码仓库、依赖解析器、构建配置、Runner、制品仓库、签发服务和部署控制器。攻击者可能篡改依赖、窃取 Runner 凭证、替换镜像标签、伪造声明或重放旧证明。先明确系统要证明“哪个摘要由哪个受信构建器按哪些输入产生”,不要声称证明代码本身无漏洞。
第二步:定义 Provenance 与制品绑定
证明至少包含源提交摘要、构建器身份、构建入口、依赖锁定信息、参数、时间、构建步骤摘要和输出制品摘要。制品摘要是绑定键;标签只用于发现,不能作为授权依据。证明、签名和验证结果都记录 schema 版本,避免字段含义静默变化。
artifact_digest -> provenance_digest -> signature -> log_entry
policy_version + identity + builder + time_window -> admission_decision第三步:设计签发与透明日志
构建器把声明提交给签发服务,服务验证构建身份和声明格式,再签名或生成可验证的签名包。签名对象必须覆盖制品摘要和关键声明;透明日志用于发现同一身份的异常签发,不替代部署时的策略判断。高吞吐写入可先落对象存储,再异步建立按摘要、仓库和身份索引。
第四步:实现部署前验证
验证器先解析不可变摘要,再检查签名链、信任根、证书身份、发行者、声明完整性和日志证明,最后应用组织策略。例如只允许主分支构建、受管 Runner、通过依赖扫描且未过期的声明。验证结果包含 allow、quarantine 或 deny、策略版本和原因码;不能只返回布尔值。
第五步:处理密钥、身份和 Runner 风险
优先使用短期工作流身份或密钥托管服务,限制签发权限和受众。密钥轮换不应让历史制品全部失效;验证器需要保留旧信任根的有效期和撤销状态。Runner 被攻陷时,撤销其身份、冻结受影响工作流、标记相关证明并阻止新制品准入;已经部署的制品按风险策略触发重新验证或回滚。
第六步:覆盖重放、回滚和离线验证
证明应带构建时间、版本和策略适用窗口,验证器拒绝超出窗口或与制品摘要不匹配的声明。回滚必须验证目标旧摘要仍符合当前或明确兼容的策略,不能因为“以前部署过”直接放行。离线环境预置受信根、撤销快照和证明包,并记录快照年龄;超过上限就进入隔离,而不是假装完成最新验证。
第七步:估算容量、保留和故障降级
按每日制品数、平均证明大小、签名写入、验证 QPS 和部署峰值估算对象存储与索引。证明本体放便宜的不可变存储,热索引只保留近期摘要。签发服务不可用时停止新制品准入或进入隔离;部署验证器不可用时,高风险环境 fail-closed,低风险环境可按预先批准的短暂缓存窗口工作,并记录例外。
第八步:设计可观测性与迁移
记录每次验证的制品摘要、策略版本、信任根版本、原因码和耗时,不记录不必要的源代码或密钥。监控证明覆盖率、签名失败、身份异常、策略拒绝、缓存命中、日志延迟和 Runner 撤销数。策略升级先影子评估旧制品,再按环境灰度;每次拒绝都应能重放输入并解释决策。
高质量示范回答
我会把系统分成构建证明、签发与透明日志、部署验证、准入策略四层。构建器生成绑定源提交、依赖、参数、构建器身份和输出摘要的 Provenance;签发服务验证工作流身份后,把签名绑定到不可变制品摘要并写入日志。部署前验证器检查签名链、证书身份、发行者、声明完整性、信任根、时间窗口和策略版本,再返回带原因码的允许、隔离或拒绝。标签永远不是授权依据。短期身份、受管 Runner、密钥轮换和撤销处理凭证风险;离线环境使用有年龄上限的信任根、撤销快照和证明缓存。证明本体放不可变对象存储,索引服务按摘要和身份查询;验证失败或服务故障按环境采用 fail-closed、隔离或受限缓存,并完整记录策略版本和决策证据。
常见错误
- 只签名镜像标签,标签移动后仍误以为制品可信。
- 只验证签名数学正确,不验证证书身份、发行者和声明内容。
- 把 SBOM、Provenance、签名和漏洞扫描混成一个字段。
- 信任所有 CI Runner,忽略自托管 Runner 被入侵后的撤销和影响范围。
- 密钥轮换后让所有历史制品失效,或永远接受已撤销的身份。
- 验证服务故障时无条件放行,导致故障降级成为绕过策略。
- 只保存“通过/拒绝”,没有策略版本、原因码和可重放证据。
追问及应对
如何证明构建器没有在声明中撒谎?
Provenance 只能证明受信构建流程发布了某项声明,不能自动证明每一步都诚实。使用隔离构建器、最小权限、可复现或可比较的构建、独立日志和策略约束降低信任假设;高风险制品再要求第二方复核或额外证明。
如果制品仓库被攻击,验证链还能工作吗?
部署按摘要拉取并验证签名,仓库标签和元数据被改动不会改变已签名摘要。证明和签名包应有独立、不可变副本;发现仓库入侵后冻结新发布、比较日志与摘要、撤销受影响身份,并按部署环境重新验证。
如何支持多个构建系统和供应商?
用统一声明 schema 和身份映射层,把 GitHub Actions、自托管 Runner 与供应商构建器映射到策略可识别的工作流身份。适配器只负责产生规范化声明,验证器仍执行同一摘要、身份、时间和策略检查,避免每个供应商拥有一套绕过规则。
业务团队抱怨验证拖慢发布,如何取舍?
先测验证耗时、缓存命中和失败原因,优化热索引与并行读取,不降低信任边界。对低风险环境提供短时、可审计缓存;对生产保留强验证,并用影子模式证明拒绝率来自真实缺陷还是策略误配。任何例外都要有期限、负责人和自动到期。