信任根轮换、离线恢复与安全退出手册
一次例行 CA 更换后,在线集群验证新镜像一切正常,隔离区却拒绝了所有新 bundle。平台把 Fulcio 新链放进在线 verifier,却没有更新离线站点的 TUF root、Sigstore trusted root 和 Rekor checkpoint;隔离区管理员为恢复发布,直接关闭了证书链与透明日志检查。短暂的更新故障因此变成永久信任旁路,旧根、旧 checkpoint 和被跳过的策略再也说不清谁授权过谁。
另一家公司退出托管签名平台时导出了镜像和公钥,却遗漏历史 Rekor 证明、Notation trust policy、验证器版本和决策日志。旧账号关闭后,历史制品只能证明“某个未知 key 签过”,无法证明 key 当时由哪个身份、哪份根和哪版策略接受;更糟的是旧 workflow 仍持有 KMS 授权,退役平台虽然不再被观察,却还能产生新签名。安全退出不是停账单,而是让历史证据继续可验,同时让旧签发路径确定地失去效力。
先把四类“根”拆成不同对象
TUF root.json 是更新元数据的根:它保存角色公钥、threshold、版本与到期信息,授权下一版 root,并保护 targets、snapshot、timestamp 的更新链。Sigstore trusted_root.json 是由更新系统分发的业务验证材料集合,包含 Fulcio CA、CT/Rekor/TSA 公钥及有效窗口;它不能反过来验证 TUF root 自身。Notation trust store 保存 X.509 CA 或 signing authority 证书,trust policy 再决定哪些 Registry scope、签名级别和 identity 可接受。KMS public key 或企业 PKI root 又是长期 key 签名路径的验证锚。
根生命周期还必须保留历史对象。Fulcio 证书链证明身份到临时公钥的绑定;Rekor entry、inclusion proof、signed entry timestamp 或 checkpoint 提供透明日志历史;Sigstore bundle 把签名、证书和日志材料带给验证者;Notation 签名及其证书链、revocation 检查结果和 trust policy 共同形成裁决。只归档“当前公钥”会丢失旧证据当时的有效窗口和授权语境。
TUF 规范要求客户端从可信 bootstrap root 开始逐版本更新 root,并保存已信版本来拒绝回滚。根不是随时可覆盖的配置文件,而是单调演进的安全状态。删除客户端状态、跳号发布 root 或只用新 key 签下一版,都会切断存量客户端的授权链。
最小启用从资产盘点和只读验证开始
先建立根清册,而不是立即轮换。每个条目记录 root_type、root_digest、version、key_ids、threshold、valid_from/to、issuer/operator、在线或离线保管位置、消费策略、分发渠道、最后确认客户端集合和责任人。再从生产样本抽取历史签名、bundle、checkpoint、Notation signature 与决策日志,验证清册能解释每个样本。
最小工具集包括固定版本的 TUF client/仓库工具、Cosign、Notation、Registry 客户端、离线对象存储和一个只读验证作业。工具二进制或镜像本身也记录 digest 与签名,不能在灾难时从未知镜像临时下载。根私钥不进入这些验证容器;验证环境只挂载公开根、policy 和只读证据。
trust-lifecycle/
bootstrap/tuf-root.json
roots/sigstore-trusted-root.json
roots/notation-truststore/
policies/verification-policy.yaml
checkpoints/rekor/
bundles/by-subject/sha256/<digest>/
tools/manifest.sha256
decisions/
inventory/root-ledger.jsonl首次启用先运行 audit-only:在线与离线验证同一批黄金样本,比较 subject digest、identity、root digest、policy digest 和 reason code。只有正常、篡改、错误身份、缺 bundle、过期 metadata 与撤销样本都能稳定分型,才允许信任生命周期作业修改生产根。
配置字段就是轮换与恢复的控制杆
TUF root 的 version 必须单调递增,expires 控制根刷新最后期限,roles.<name>.keyids 与 threshold 决定角色授权,consistent_snapshot 影响仓库对象命名和复制。timestamp、snapshot 和 targets 各自也有版本、到期、下游 metadata hash/length;客户端持久化这些已信状态,才能拒绝冻结与回滚。
Sigstore 侧要区分 signing config 与 trusted root。前者描述当前签名客户端联系的 Fulcio、Rekor、OIDC、TSA 服务和有效窗口,后者保存活动及历史验证材料。轮换时先让消费者取得包含旧新材料的 trusted root,再把签发流量切到新服务。Rekor 分片停止写入后,其公钥、checkpoint 与 tile/entry 历史仍需保留;写入地址下线不等于历史验证材料可以删除。
Notation 的 trust policy 关键字段包括 Registry scope、signature verification level、trust stores 和 trusted identities。skip 或宽泛 identity 只能用于受控恢复实验,不能作为轮换兼容策略。证书放入 trust store 只建立候选信任锚,最终是否接受仍由 policy scope、identity、完整性、真实性、可信时间与撤销语义共同决定。Notation trust policy给出了这些对象的职责。
双根窗口必须有进入与退出条件
双根不是把两个 PEM 永久拼在一起。进入条件是旧根仍可信、新根经过带外验证、客户端覆盖率可测、历史样本可验;窗口对象包括旧新 root digest、允许签发的 signer、只验旧证据的历史路径、开始条件、最大持续时间、撤销触发器和负责人。新签发只能逐步转向新 signer,旧根在窗口内主要承担历史验证与回切,不应继续无限制产生新制品。
推荐顺序是:发布由旧信任链授权的新 TUF root;在 trusted root 或 Notation trust store 中加入新验证材料;等待在线、边缘和离线客户端确认;让签发端切新 key/CA;双验新旧样本;停止旧 signer;观察完整发布周期;最后从活动 policy 移除旧根,但把历史验证所需材料转入带有效窗口的归档策略。
若 root key 被怀疑泄露,普通双根窗口可能不再安全。响应依据仍可信的 threshold key、预置恢复 root 或带外 bootstrap 路径执行;不能让疑似被盗的单一 key 自己授权替换。对 compromise time 之前的历史签名,结合可信时间与透明日志判断;对之后或时间无法证明的对象,按 digest 拒绝并重建,而不是一刀删除全部历史根。
第一组正向实验:旧新根连续轮换
在隔离仓准备连续的 root.N.json 与 root.N+1.json。下一版同时满足旧 root threshold 和新 root threshold,客户端从旧 bootstrap 开始刷新,再验证新 targets。将下面入口保存为 ./lab/refresh.py,Python TUF client 通过环境变量选择持久 metadata 目录:
import os
from pathlib import Path
from tuf.ngclient import Updater
state = Path(os.environ.get("METADATA_DIR", "./lab/client-metadata"))
targets = Path("./lab/downloads")
updater = Updater(
metadata_dir=str(state),
metadata_base_url="https://updates.example.test/metadata/",
target_base_url="https://updates.example.test/targets/",
)
updater.refresh()
info = updater.get_targetinfo("release/app.bin")
assert info is not None
path = updater.download_target(info, str(targets))
print(path)METADATA_DIR=./lab/client-metadata python ./lab/refresh.py预期客户端按顺序接受下一版 root,持久状态中的版本增加,timestamp、snapshot、targets 与目标 hash/length 全部通过。在线 Sigstore/Notation 黄金样本在双根 policy 下都能验证,新签发样本命中新根,旧历史样本仍命中旧根;每次 decision 都记录实际 root digest,而不是笼统写“双根通过”。
第一组反向实验:跳版与单边签名必须拒绝
复制客户端状态,服务端只暴露跨越多个版本的 root,或让下一版仅由新 key 签名。不要修改生产 metadata;实验目录可直接替换测试服务响应:
set -euo pipefail
cp -a ./lab/client-metadata ./lab/client-negative
cp ./fixtures/root.$((CURRENT_ROOT_VERSION + 2)).json \
./lab/repository/metadata/$((CURRENT_ROOT_VERSION + 1)).root.json
if METADATA_DIR=./lab/client-negative python ./lab/refresh.py; then
echo "unexpected accept: root chain was not continuous" >&2
exit 1
fi
sha256sum ./lab/client-negative/root.json \
./lab/repository/metadata/*.root.json预期刷新非零退出,客户端仍保留原可信 root,不覆盖为服务端“最新文件”。失败证据应区分版本连续性、旧 threshold 不满足、新 threshold 不满足和过期。若恢复手册建议清空 metadata 目录重试,它会同时删除回滚防护状态,应立即更正为从受控 bootstrap 恢复。
离线包要携带证据、根、策略和新鲜度
一个可恢复的离线包以 subject digest 为主键,包含制品或 OCI manifest、原始 in-toto/SLSA statement、独立签名、Sigstore bundle、证书链、Rekor entry/proof/checkpoint、TUF root 与相关 targets metadata、Sigstore trusted root、Notation trust store/policy、KMS public keys、撤销快照、验证工具镜像 digest、在线决策样本和文件清单哈希。包外再用独立离线恢复 key 或组织归档签名保护整个清单。
checkpoint 不是普通日志文本。它要绑定 log identity、tree size/root hash 和签名,并与 bundle 中 inclusion proof 对应;离线站点要保存已见 checkpoint 状态,拒绝树大小倒退或无法建立一致性的视图。长期离线会产生撤销与根刷新滞后,所以包带 issued_at、refresh_by、root/policy digest 和序列号;超过刷新期限进入受限模式,不用本地时钟回拨伪装新鲜。
根刷新通过单向介质或受控摆渡导入 staging,先验证包签名、清单、连续 root 更新和 checkpoint,再原子切换 active。旧 active 保留为只读回切点,不能被新包原地覆盖。离线站点回传的消费日志同样脱敏签名,用于中心侧确认哪些 digest 与 root version 已实际使用。
第二组正向实验:断网验证完整 bundle
先在联网隔离区导出 blob、bundle、trusted root 与策略,记录哈希;进入禁止网络的容器后验证。网络阻断方式由运行时实现,测试目标是验证过程中没有任何 DNS 或 HTTP 依赖:
set -euo pipefail
sha256sum artifact.bin artifact.sigstore.json trusted-root.json policy.json \
> offline-manifest.sha256
docker run --rm --network none \
-v "$PWD:/work:ro" -w /work \
"$PINNED_COSIGN_IMAGE" \
verify-blob artifact.bin \
--bundle artifact.sigstore.json \
--trusted-root trusted-root.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"
sha256sum -c offline-manifest.sha256预期无网络条件下退出码为零,bundle 内签名、证书、透明日志材料与 subject digest 均闭合,工具镜像 digest、root digest、policy digest 和 checkpoint hash 被写入离线 decision。若验证器静默回退到在线查询,断网实验会暴露该依赖。
第二组反向实验:旧 checkpoint、旧根与缺包分别失败
反例不要一次破坏所有输入,否则无法分型。依次删除 bundle、替换为旧 trusted root、回放较小 tree size 的 checkpoint,并让每次验证产生独立 reason code:
set -euo pipefail
run_must_fail() {
name="$1"; shift
if "$@" >"$name.out" 2>"$name.err"; then
echo "unexpected accept: $name" >&2
exit 1
fi
}
run_must_fail missing-bundle cosign verify-blob artifact.bin \
--bundle missing.json --trusted-root trusted-root.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"
run_must_fail stale-root cosign verify-blob artifact.bin \
--bundle artifact.sigstore.json --trusted-root old-trusted-root.json \
--certificate-identity "$EXPECTED_IDENTITY" \
--certificate-oidc-issuer "$EXPECTED_ISSUER"
cmp checkpoint.current checkpoint.replayed && exit 1 || true预期缺 bundle、无法建立证书链或日志信任、checkpoint 回退分别被记录,不能统一降级成“离线模式放行”。最后一项还需由 checkpoint monitor 验签并比较树状态;cmp 只证明测试输入确实不同,不承担密码学判断。
冻结与回滚拒绝依赖客户端持久状态
freeze 攻击向客户端持续提供仍有有效签名但不再新鲜的 timestamp/snapshot,使安全更新不可见;rollback 则返回版本更低的 metadata 或旧 policy/root。客户端必须保存最高已信版本、检查到期、下游 metadata hash/length 和目标摘要。代理缓存即使返回 200 也可能在重放旧对象,因此监控要比较 metadata version 与中心已发布状态,而不是只看 HTTP 可用性。
离线站点允许滞后,但滞后必须显式预算。接近 refresh_by 时告警,到期后停止接受新制品,仅允许按专门历史策略验证已登记 digest;不得删除状态或调慢时钟。备份恢复时,恢复点里的 root/timestamp 版本可能低于故障前,系统先与独立账本比较单调性,再决定使用更新包或带外 bootstrap,不能直接把旧快照挂回 active。
break-glass 与灾备恢复要缩小爆炸半径
break-glass 用于恢复验证能力,不用于绕过制品身份。预先准备独立恢复 identity、只读根仓副本、离线工具、恢复 namespace、最小 selector 豁免和双人审批。Kubernetes Gatekeeper 的官方紧急动作可能删除整个 validating webhook,从而移除全部 Gatekeeper checks,而不只是供应链验证;执行前要记录集群对象,开启补偿审计,恢复后重验窗口内创建或更新的 workload。
灾备演练至少覆盖根仓不可达、KMS/HSM 不可用、Registry 丢失 referrer、Rekor 服务或历史存储故障、验证缓存污染和离线介质损坏。恢复顺序是保护现存状态,加载已签离线根与工具,恢复只读验证,再恢复签发;签发服务未恢复时宁可暂停发布,也不能把 Chains/Cosign signer 改成 none 继续晋级。
恢复完成后清理所有 verifier/admission 缓存,以 digest、policy digest、root digest 重验故障窗口对象。break-glass 产生的豁免逐项回收,恢复身份和介质重新封存,窗口内每个部署要么补验通过,要么隔离。没有补验闭环的“服务恢复”只是把风险转入生产。
Fulcio、Rekor、Notation 与平台验证怎样接入
Fulcio 轮换 CA 时,旧新链进入 trusted root 的受控有效窗口;签发端只使用当前 intermediate,root key 离线或由高保护等级 HSM 托管。Rekor 轮换 log key 或分片时,SigningConfig 指向新写入端,TrustedRoot 保留活动与历史公钥;旧 tile、entry 和 checkpoint 继续只读可用。Sigstore root-signing展示了公共信任材料通过 TUF 组织的实现入口,私有部署也应保持 root、targets、snapshot、timestamp 的角色分离。
Notation 迁移先把新 CA/signing authority 加入 trust store,使用 shadow policy 双验,再切换 signer,最后收紧 trusted identities 并移除旧活动根。Registry 中的旧签名是否保留由历史策略决定;删除签名不是撤销,已复制副本仍可能存在。Ratify 或其他平台 verifier 缓存必须在根/策略轮换后失效,decision 记录实际命中的 trust store 与 policy 摘要。
托管平台的 GitHub attestation、公共 Sigstore 或云 KMS 都可以成为签发和验证实现,但不能成为唯一历史仓。平台 API 可用于在线发现,离线 bundle、根材料、身份约束和 digest 索引必须可导出;账户冻结、服务退出或 API 变化时,历史验证不应要求原平台再签发一个新 token。
排障按更新链、业务根和策略三层分型
root threshold 或版本错误先查 TUF bootstrap、连续 root 文件、key IDs、threshold 与客户端持久版本;timestamp/snapshot 过期先查时钟、镜像缓存与发布链,禁止全局忽略 expiry。证书链错误再查 Sigstore/Notation 业务根、有效窗口、intermediate 顺序和 revocation 状态。identity mismatch 属于 policy 层,不能通过向 trust store 塞更多 CA 解决。
Rekor 验证失败继续区分 log ID 不匹配、entry/proof 缺失、checkpoint 签名错误、树状态回退和历史分片不可达。离线成功而在线失败通常指向发现、网络或缓存;在线成功而离线失败通常缺 bundle、trusted root、checkpoint 或工具能力。升级后全部放行或全部拒绝时,比较 verifier schema、reason code、policy 字段和默认行为,尤其防止“零 verifier report”被解释成成功。
权限与敏感材料必须分层托管
root threshold keys、Fulcio CA、Rekor checkpoint key、TSA key、Notation signing key 和恢复包签名 key 不共用账号、Secret 或 KMS policy。root 角色优先离线分权;在线 timestamp/snapshot 或 intermediate 只获得限定用途。创建根、发布 metadata、切换 signer、修改消费 policy 和删除历史证据由不同角色承担,高风险轮换采用双人或阈值审批。
公开验证材料也可能泄露组织结构。证书 identity、workflow URI、私有 Registry 路径、内部 package 名、checkpoint operator 和 provenance 参数都需分类;bundle 与 DSSE payload 不提供机密性。恢复包加密、离线介质编号、出入库审计,解密 key 与包分离保存。日志不得记录私钥、OIDC token、KMS bearer token、完整 Authorization header 或未脱敏内部 URL。
容量、成本、HA 与升级迁移共同设计
根文件本身体积小,成本主要来自历史 bundle、Rekor entry/tile、SBOM/provenance、Registry 副本、离线介质和周期演练。按 subject digest 内容寻址并去重,根、checkpoint、policy 和决策日志因关键且体积小可长保留;大型 evidence 使用压缩对象存储。预算同时包含 KMS/HSM 操作、跨区复制、对象锁、离线运输和人员阈值签名,不只看验证请求单价。
HA 要区分签发可用与历史验证可用。签发 KMS 故障可以暂停新发布,历史 verifier 应仍能使用归档根与 bundle;TUF metadata、checkpoint 和 trust store 至少跨故障域只读复制,写入保持单调版本与单一发布序列。多副本验证器各自缓存根时,用 root digest 和版本监控收敛,避免负载均衡后同一制品时过时不过。
升级 TUF 实现、Cosign、Notation、Ratify 或根 schema 时,先固定旧新工具 digest,在影子环境重放黄金样本和负例。双写 metadata 或双验 decision 时明确唯一发布者,避免两个控制面竞争递增版本。迁移完成的信号不是新平台能签,而是旧历史证据、新签发、离线负例、撤销与灾备恢复都能得到预期裁决。
平台退出包让历史可验、旧路失效
退出包以 digest 索引 artifacts、OCI manifests、signatures、attestations、SBOM/VEX、Sigstore bundles、Rekor entries/proofs/checkpoints、Fulcio/CT/TSA chains、TUF 全角色 metadata、Notation trust store/policy、KMS public keys、verification decisions 和部署记录。包内还包含固定验证工具及其校验值、字段 schema、身份映射、保留与撤销规则、根清册和双平台对比结果。
迁移采用只读导入、影子验证、双写签发证据、双平台验证、消费者切流、停止旧签发、旧平台只读历史、最终下线的顺序。双根窗口结束后,从活动 policy 删除旧 root/signer,但历史策略仍可按可信时间验证已登记 digest。旧平台删除在线 attestation 前先导出 bundle;删除动作记录对象、操作者和结果,因为删除本身不会让所有副本失效。
旧签发路径核销要逐项证明:workflow 被禁用,OIDC trust/audience 被移除,KMS grant 与 service account 回收,Fulcio intermediate 和长期 key 停签,Registry push token 与机器人账号撤销,旧 signer 镜像和 secret 删除,DNS/webhook/API endpoint 下线,缓存清空,账单与数据保留合同关闭。最后用旧 identity 进行一次受控负向签发和验证,预期签发被拒或新签名无法通过活动策略;仅看控制台“已删除”不算核销。
清理、回滚与机制选择遵循可证明状态
清理轮换实验时先恢复消费者 policy,再停止测试 signer,删除测试 evidence 与 artifact,最后销毁测试 key;保留 root/version、决策与删除审计。生产切换若出现误拒,可以在双根窗口内回切 signer 或 verifier,但不能降低 identity、透明日志、expiry 和 rollback 检查。疑似根泄露时也不能回滚到已泄露根,只能走预置恢复信任或重新分发 bootstrap。
TUF 适合需要安全更新与根角色分权的分发链;Sigstore 适合把工作负载身份、短期证书、透明日志和 bundle 组合起来;Notation 适合 OCI 场景中的 X.509/插件与 Registry scope policy;长期 key/KMS 适合组织已具备成熟密钥治理且需要稳定离线验证的路径。它们可以组合,但每多一层都增加根刷新、历史留存与退出成本。
选择机制时实际演练四件事:存量客户端能否连续轮换;原服务断网后历史 bundle 能否验证;旧 checkpoint、回滚 metadata、错误 identity 与撤销 key 能否稳定拒绝;退出后旧 signer 能否确定失效。能完成这四项的简单组合,比拥有更多在线服务却没有恢复包更可靠。
The Update Framework Specification定义 root、timestamp、snapshot、targets、连续更新与攻击检测规则。Sigstore bundle说明离线验证所携带的签名、证书与透明日志材料。
Sigstore root-signing展示 TUF 信任材料和根签名事件的组织方式。Notation trust policy定义 trust store、Registry scope、验证级别与 identity 策略。
Gatekeeper emergency recovery说明准入控制失效时的紧急恢复边界。
