Tekton Chains:从 TaskRun 结果到可验证供应链证明
一次镜像发布后,TaskRun 上出现了 chains.tekton.dev/signed=true,流水线便把镜像晋级到生产。事故回溯发现 Chains 已完成格式化与签名,却因 Registry 授权被撤销而没有把 Attestation 上传到 OCI;控制器状态描述的是处理阶段,不是目标后端的持久化回执。运行对象随后被自动清理,annotation 中的 payload 和签名也一起消失,团队只剩一个无法证明后端存在证据的布尔标签。
另一次事故来自结果伪造。构建 Task 允许用户脚本直接写 IMAGE_URL 和 IMAGE_DIGEST Result,Chains 忠实地把这些值格式化成 provenance 并签名,但并未重新读取 Registry 核对该 digest 是否由当前运行产出。攻击者没有偷签名密钥,只让可信控制器为不可信结果背书。自动签名能保护声明不被事后篡改,不能把租户可控字段变成平台观察事实。
Chains 在 Run 完成之后做什么
Tekton Chains 是监听已完成 TaskRun 与 PipelineRun 的控制器。它从 Run 状态、Results、参数和关联对象取得快照,依次经历 formatting、signing、uploading,再把状态或证据写回配置的后端。它不会改变原 Task 的执行隔离,不会自动让构建变成 hermetic/reproducible,也不负责消费端决定某个 builder、仓库或参数是否可信。
TaskRun 是单个 Task 的实际执行,包含 steps、params、results 和状态;PipelineRun 编排多个 TaskRun,并有自己的参数、结果和子运行关系。两层都生成 provenance 时,同一镜像可能出现两份粒度不同的叙述。需要步骤级取证时保留 TaskRun 证据,需要发布编排视角时保留 PipelineRun 证据;不要无条件双开,再让消费者随机选一份。
completed TaskRun / PipelineRun
-> snapshot observed fields
-> formatter builds payload
-> signer authenticates payload
-> storage uploads payload/signature/cert
-> consumer discovers and verifies by subject digest最后一步不属于 Chains 的“生成成功”:消费者还要核对 subject digest、predicate type、签名身份、builder、source 和参数策略。signed=true、Run 的 Succeeded=True 和“后端有一个对象”都不能替代这次验证。
安装时固定控制器与依赖基线
先部署与目标 Kubernetes、Tekton Pipelines 发行线兼容的 Chains 版本。生产清单固定版本 URL 与下载摘要,先在隔离集群做 server-side dry run,再安装;latest 入口只能用于发现版本,不能形成可重复基线。Chains 安装入口 与目标 release notes 应一起审查 CRD、RBAC、默认配置和弃用项。
set -euo pipefail
: "${CHAINS_VERSION:?pin an approved Chains version}"
: "${CHAINS_MANIFEST_SHA256:?record the approved manifest digest}"
curl -fsSLo chains-release.yaml \
"https://infra.tekton.dev/tekton-releases/chains/previous/${CHAINS_VERSION}/release.yaml"
echo "${CHAINS_MANIFEST_SHA256} chains-release.yaml" | sha256sum -c -
kubectl apply --server-side --dry-run=server -f chains-release.yaml
kubectl apply -f chains-release.yaml
kubectl wait --for=condition=Available deployment/tekton-chains-controller \
-n tekton-chains --timeout=120s
kubectl get pods,configmap,secret -n tekton-chainsPod Available 只证明进程启动。接下来还要创建或接入 signer、配置 storage 与 Registry/KMS 认证、运行一个最小 Run,并从每个目标后端拉回证据做独立验证。控制器默认观察范围和 --namespace 行为随目标版本检查;多租户集群宜缩小观察 namespace,避免无意收集所有团队的参数和结果。
数据模型从 Run 快照流向证明
Formatter 决定怎样把 Run 字段映射成 in-toto/SLSA payload。subject 通常来自带类型提示的输出 Results,materials 可来自输入、source 和解析后的依赖,builder/buildType 则由 Chains 与 Tekton 语义共同构造。类型提示缺失或放在错误层级时,Chains 仍可能生成 Run provenance,但 subject 或 materials 不完整。
PipelineRun 是否深入读取子 TaskRun 的结果受 deep inspection 与 formatter 能力影响。启用后证据更完整,也会增加 API 读取、对象体积和敏感字段暴露;关闭后不能从 PipelineRun provenance 推导所有子步骤材料都已记录。TaskRun 与 PipelineRun 的 labels、annotations、params、results 可能进入证据,字段进入前必须做数据分类。
结果本身不天然可信。若用户步骤能写 IMAGE_DIGEST,Chains 只看见“Run 声称输出这个摘要”。更强的设计由受信 sidecar、Registry 响应或构建平台控制面解析上传后的 digest,并禁止普通 Task 覆盖;消费端还要按 digest 拉取 manifest 复核。provenance 的真实性与 predicate 内容的事实正确性是两道门。
formatter、signer 与 storage 配置改变不同阶段
核心配置位于 tekton-chains/chains-config。artifacts.taskrun.* 与 artifacts.pipelinerun.* 分别控制两类 Run 的 format、signer 和 storage;builder.id 影响 provenance 中构建器身份;transparency 配置决定是否尝试上传透明日志;OCI repository、TLS 和认证配置决定证据去哪里。键名、允许值和默认值需按固定版本的 Chains 配置文档 核对。
kubectl patch configmap chains-config -n tekton-chains --type merge -p '{
"data": {
"artifacts.taskrun.format": "slsa/v2alpha4",
"artifacts.taskrun.signer": "x509",
"artifacts.taskrun.storage": "tekton,oci",
"artifacts.pipelinerun.storage": "",
"builder.id": "https://build.example.invalid/tekton/release/v1",
"storage.oci.repository.insecure": "false"
}
}'
kubectl rollout status deployment/tekton-chains-controller -n tekton-chains
kubectl get configmap chains-config -n tekton-chains -o yamlformatter 名称不等于 payload 规范版本。历史配置中的 slsa/v1 实际对应旧 SLSA v0.2 兼容格式,而较新的 formatter 名可能承载 SLSA Provenance v1;迁移必须解码 payload 检查 _type、predicateType 和字段结构,不能按配置字符串猜测。signer: none 可以保留未签名 provenance,却不能建立制品信任。storage: tekton,oci 是双写请求,不是双写成功承诺。
签名、Registry 与 KMS 权限要拆开
本地 X.509 或 Cosign 私钥通常存放在 tekton-chains/signing-secrets,KMS 则通过 signers.kms.kmsref 引用。控制器 ServiceAccount 只应获得读取必要 Secret、调用指定 key/version 签名和写目标证据仓库的权限。KMS token 用 Secret 挂载并通过 token path 读取,不写入 ConfigMap、命令行或日志;需要热更新时避免 subPath 阻断 Secret volume 刷新。
构建 Task 推镜像与 Chains 推 Attestation 是两个写权限面。让控制器复用任意租户的 Registry push 凭据,会使一个 namespace 的 Run 影响另一个仓库;让 Task 读取 signer Secret,则租户能绕开 Chains 任意签名。Registry scope 应限定到证据目标,KMS policy 限定签名用途,Kubernetes RBAC 限定 namespace 和 Secret 名,NetworkPolicy 只允许必要 Registry、KMS、Fulcio/Rekor 端点。
keyless/Fulcio 与 transparency 不是“用了 Cosign”就自动开启。OIDC provider、Fulcio、TUF mirror、Rekor 和 identity policy 都要显式配置。公共服务可能公开仓库、工作流身份和时间元数据;私有项目采用前先审查披露面。storage.oci.repository.insecure: true 会跳过 TLS 证书验证,只能用于隔离实验,不能靠后续签名弥补上传路径的中间人风险。
正向实验:双存储后分别拉回验证
准备一个只向测试仓库写入的 Task,输出固定的 OCI image digest;配置一个 signer 和 tekton,oci 双存储。等待 Run 完成后,不以 annotation 状态作为结论,而是分别从 Kubernetes 对象与 OCI 拉回 payload、signature 和证书/链,比较同一 subject 与 predicate。
set -euo pipefail
kubectl create namespace chains-lab --dry-run=client -o yaml | kubectl apply -f -
kubectl apply -n chains-lab -f task-build-image.yaml
kubectl apply -n chains-lab -f taskrun-build-image.yaml
kubectl wait --for=condition=Succeeded taskrun/build-image \
-n chains-lab --timeout=300s
until kubectl get taskrun/build-image -n chains-lab \
-o jsonpath='{.metadata.annotations.chains\.tekton\.dev/signed}' | grep -qx true; do
sleep 2
done
kubectl get taskrun/build-image -n chains-lab -o json > taskrun.json
kubectl logs -n tekton-chains deployment/tekton-chains-controller \
--since=10m > chains-controller.log
# 使用固定版本的 cosign/oras 或组织验证器,按实际 image digest 拉回 OCI Attestation。
cosign verify-attestation --key chains-public.pem \
"${IMAGE_REPOSITORY}@${IMAGE_DIGEST}" > oci-verification.json正向证据至少包含:Run Succeeded=True;formatter 生成 payload;签名由预期 key/identity 验证;Tekton annotation 可解码;OCI 按同一 image digest 能独立发现并拉回 Attestation;两端 Statement 的 subject digest、predicateType 和 builder identity 一致。控制器日志或 metrics 还应显示 storage 成功,而不是只有 signed=true。
删除测试 TaskRun 后再次从 OCI 验证。预期 annotation 副本消失,OCI 副本仍可验证;这证明持久性来自外部后端,而不是 Run 对象。若 OCI 也消失,说明 Registry GC、附件关联或上传路径没有满足保留设计。
反向实验:签名成功不等于上传成功
撤销 Chains controller 对测试 repository 的 push 权限,保留 signer 权限,再创建新 Run。预期 formatting 与 signing 仍可能完成,但 uploading 失败。实验要同时观察 annotation、controller log、Registry referrers 和消费者验证结果。
set -euo pipefail
# 由测试 IAM/RBAC 脚本仅撤销 Attestation repository 的 push 权限。
./deny-chains-registry-push.sh
kubectl apply -n chains-lab -f taskrun-storage-denied.yaml
kubectl wait --for=condition=Succeeded taskrun/storage-denied \
-n chains-lab --timeout=300s
kubectl get taskrun/storage-denied -n chains-lab -o json > storage-denied-run.json
kubectl logs -n tekton-chains deployment/tekton-chains-controller \
--since=10m | tee storage-denied-controller.log
if cosign verify-attestation --key chains-public.pem \
"${IMAGE_REPOSITORY}@${DENIED_IMAGE_DIGEST}"; then
echo "ERROR: OCI verification unexpectedly succeeded" >&2
exit 1
fi
grep -Ei "unauthorized|forbidden|denied|upload|storage" storage-denied-controller.log
./restore-chains-registry-push.sh这个负例要求 Run 本身仍可成功,因为构建与证据上传是不同阶段;OCI 消费验证必须失败,日志应保留 401/403 或后端原因。即使对象上出现 chains.tekton.dev/signed=true,也不能据此写成“后端已存储”。恢复权限后重新触发或重建证据,拉回验证成功,才能关闭故障。
再让 transparency endpoint 不可达:后端 Attestation 可能仍成功存储,但 Rekor bundle annotation 缺失。策略若要求透明日志包含证明,就必须拒绝这种部分成功;若不要求,也应把“已签名、已存储、未写透明日志”记录成不同状态,而不是压缩成一个绿色标记。
反向实验:结果伪造与证据随对象清理
创建一个测试 Task,让用户脚本把任意 ${FOREIGN_DIGEST} 写入带 OCI 类型提示的 Result,但不实际生成该镜像。Chains 可能按配置格式化并签名这个 subject,证明控制器忠实处理了 Run,却没有证明结果来自当前执行。
set -euo pipefail
: "${FOREIGN_DIGEST:?digest of an object not produced by this run}"
envsubst < taskrun-forged-result.yaml | kubectl apply -n chains-lab -f -
kubectl wait --for=condition=Succeeded taskrun/forged-result \
-n chains-lab --timeout=120s
kubectl get taskrun/forged-result -n chains-lab -o json > forged-result.json
# 消费策略应比较受信构建观察、source/run identity 与 Registry manifest,而非只验签。
if ./verify-release-policy.sh forged-result.json "${FOREIGN_DIGEST}"; then
echo "ERROR: tenant-controlled result was trusted as platform observation" >&2
exit 1
fi
kubectl delete taskrun/forged-result -n chains-lab
kubectl get taskrun/forged-result -n chains-lab 2>&1 | grep -E "NotFound|not found"预期签名验证可能成功,业务策略却因 provenance 来源、受信 builder 观察或产出关联不足而失败。这正是“真实性不等于事实正确”的反例。若唯一 storage 是 tekton,删除 TaskRun 后 payload、signature、cert/chain annotations 会一起丢失;Kubernetes API audit 或一行日志不能重建原始证据。生产至少使用具备保留、备份和按 digest 导出的外部后端。
流水线接入要固定 subject 与消费策略
构建 Task 应输出不可变 digest,不把 tag 当最终 subject。Result 类型提示要在 Task/Pipeline 的正确层级声明,并用契约测试检查 formatter 生成的 subject、materials、builder 和 buildType。PipelineRun 若承担发布证明,就明确哪些子 TaskRun 被 deep inspection 读取;不承担的那一层关闭 storage,减少重复证据和歧义。
推广作业等待的是“目标 storage 可读且消费验证通过”,不是等待 annotation。验证器按 digest 发现证据,约束 signer identity、issuer、builder ID、source repository/ref/digest、predicate type 与允许参数,并输出 policy version、evidence digest 和原因码。tag 只用于人类定位,推广后仍传递 digest。
对 formatter、signer、storage、transparency 各建立独立 SLO 和状态。格式化失败看 schema/type hint;签名失败看 key/KMS/OIDC;上传失败看 Registry/IAM/TLS/限流;验证失败再区分找不到证据、密码学失败和策略不匹配。这样值班人员不会把所有问题都归为“Chains failed”。
排障从控制器观察到后端回读
Run 没被处理时,检查它是否已终态、namespace 是否在观察范围、controller informer/RBAC 是否能 list/watch/get 对象,以及配置是否禁用了对应 Run 类型。payload 没有 subject 时,检查 Results type hints、结果值、TaskRun 与 PipelineRun 层级和 deep inspection;不要手工补一份 JSON 绕过生成缺陷。
签名失败时检查 Secret 格式、KMS reference、token path、key version、OIDC/Fulcio/TUF 网络和时钟。上传失败时检查使用的是谁的 Registry 凭据、repository scope、TLS chain、附件发现语义、429 与 GC。annotation 存在但 OCI 查不到,优先判定为存储部分失败,不要让控制器标签覆盖 Registry 事实。
升级后突然全量缺证据,比较 chains-config 实际 generation、controller 启动日志、默认 formatter/storage、弃用键与新 payload schema。某些副本成功、某些失败时,检查配置热加载、Secret volume 更新、节点 DNS 和身份令牌,而不是重复 Run 直到碰巧成功。
容量、成本与 HA 不能只数控制器副本
吞吐由每秒完成的 Run 数、每个 Run 的结果和子 Task 数、formatter 遍历量、签名/KMS 延迟、每份证据大小、storage 数量、Registry 限流和 transparency 调用共同决定。双存储近似增加上传与保留成本,deep inspection 增加 Kubernetes API 读取和 payload 大小,annotation 存储还会消耗 etcd 对象预算。
容量测试应记录 formatting/signing/uploading 分段延迟、队列深度、失败重试、KMS 限流、Registry 401/429、payload 字节数、annotation 对象大小和 GC 后可验证率。标签维度保持低基数,不把完整 digest、repository 或 Run UID 全放进 metrics;详细关联进入受控日志。
多副本 Chains controller 的 leader election、工作队列与重复处理语义需按目标版本验证。HA 能减少控制器实例故障,不能修复单一 KMS、Registry、DNS、错误 ConfigMap 或共享凭据失效。后端要有自己的跨区/备份策略,Run GC 不能早于外部回读确认;重试必须幂等,重复 Attestation 也要能被消费者按 evidence digest 去重。
成本不仅包括 controller CPU/内存,还包括 KMS 请求、Registry/object storage、跨区 egress、透明日志、审计保留和密钥轮换。根据发布频率和恢复目标选择 tekton + oci、对象存储或文档后端,不为“后端越多越安全”支付无法验证的一致性成本。
清理与回滚先保留可恢复证据
实验清理顺序是:导出测试 Run、payload、signature、证书链与 OCI evidence digest;撤销测试 Registry/KMS 权限;删除测试 Run、Task 与 namespace;删除仅供实验的 signing Secret;最后确认控制器不再读取旧凭据。共享 Registry、KMS key、Tekton Pipelines 和 CRD 不随一次实验直接删除。
配置回滚先恢复经验证的 chains-config 快照和 signer/storage Secret 引用,再滚动控制器并运行正反样本。把 signer 改为 none 只会绕过签名,不是故障恢复;正确做法是暂停推广、切换预演过的备用 signer,在有限双信任窗口内让消费者同时接受新旧身份,随后移除故障身份。
卸载前停止新 Run 进入,等待队列排空并确认外部后端回读。删除 Chains controller 后,TaskRun/PipelineRun 与外部证据不会自动获得新的保留保证;先完成归档和消费者迁移,再处理 release 资源。清理后用旧 ServiceAccount、旧 key 和旧 Registry token做负向请求,确认它们不能继续签名或上传。
升级、迁移与退出要比较 payload 而非配置名
升级前固定 controller image、manifest、RBAC、ConfigMap、Secret schema 与验证 CLI,阅读 Chains 弃用说明 和目标 release notes。用历史样本比较 TaskRun/PipelineRun 观察范围、formatter 实际 Predicate 版本、subject/materials、builder、签名格式、annotation 键和每个 storage 行为。默认值变化必须显式写回配置,避免升级后静默从一种 formatter 或后端切到另一种。
蓝绿迁移时,新旧控制器不要同时无约束处理同一 Run。可按 namespace 分片或让候选环境回放导出的 Run,在只读后端比较 payload;随后开启 tekton,oci 或新旧外部后端双写,逐个按 subject digest 拉回并独立验签。只有双端证据数量、字段、身份和消费者决策一致,才切换读取路径。
退出平台时导出标准 in-toto/SLSA payload、独立签名、证书链、Rekor bundle/checkpoint、subject digest、builder/source 身份策略和验证日志,不把 Tekton CR JSON 或 annotation 当唯一可移植格式。目标平台先验证历史样本,再接管新证据;停止旧 signer 签发后保留只读历史验证,最终移除旧根、旧 ServiceAccount 和旧 Registry scope。恢复演练必须证明在原集群和原 API 不可用时,归档证据仍能验证。
机制选型看证据在哪里生成和保存
Chains 适合已经使用 Tekton Pipelines、希望由独立控制器观察 Run 并自动生成证明的团队。若构建事实只存在于其他 CI,强行引入 Tekton 只为签名会增加 Run 映射和权限边界;平台原生 Attestation 或通用签名器可能更直接。无论使用哪种生成器,消费策略都应独立存在。
tekton storage 便于调试和近端观察,但受对象大小、RBAC 和 Run GC 约束;OCI 让证据靠近 subject,适合随 digest 复制,却要治理 Registry 附件发现、复制与 GC;对象或文档后端适合集中保留和查询,也会引入 IAM、schema、费用和退出导出问题。选择依据是保留期、查询模式、离线恢复、跨 Registry 晋级和消费者兼容,而不是后端名称数量。
Tekton Chains 文档与安装入口。Tekton Chains 配置。Tekton Chains SLSA Provenance
Tekton Chains Signing。Tekton Chains Authentication。Tekton Chains Sigstore 与透明日志
