设想一个虚构场景:某支付平台发布了 checkout:2026.10。仓库里有签名,Kubernetes 配置了验证 Webhook,发布记录也写着“镜像已验证”。一天后,应急团队发现该版本来自错误的源代码修订。此时没人能准确回答它运行在哪里:GitOps 只显示标签,部分 Pod 在策略启用前已经创建,ARM 节点选择了另一份平台清单,而系统命名空间的例外既没有负责人,也没有到期时间。
场景虽为假设,控制问题却很具体。在发布管理语境中,容器基础设施安全意味着:能够从已批准的不可变内容一路证明到实际运行的实例;发生撤回时,阻止今后匹配的准入操作,识别既有 Pod 内部的容器重启路径,并在不造成未经批准停机的前提下替换实例。签名、准入策略和 Pod 清单缺一不可,但任何一个单独存在都不能回答全部问题。
发布需要证据链,而不是一个绿色状态
有效评估不应从“是否启用了签名验证”开始,而应验证五个相互关联的断言。发布负责人批准了特定 digest;该 digest 的签名来自预期签名身份和 issuer;证明材料满足有实际内容的发布策略;所有相关 API 请求都经过强制准入;运行时清单表明当前运行的正是已批准 digest。
本主题比一般性的 Kubernetes 安全审计 更窄,后者还覆盖 RBAC、密钥、网络控制与工作负载配置;它也比广义的 软件供应链安全评估 更聚焦。这里讨论源代码和构建器,是因为这些属性参与了某个具体镜像的准入决定。
每个断言都应有自己的交付物:包含 digest 的发布卡、签名与证明验证结果、策略版本、准入事件、实际镜像快照,以及任何例外或撤回决定。“Cosign 已通过”并未说明授权了哪个身份、检查了证明中的哪些字段,也未说明目标集群当时是否执行了同一策略。
- 批准发布负责人记录 digest、平台、预期签名者、构建器和允许的构建参数。
- 验证验证器把签名与证明绑定到 digest,并按版本化策略检查其内容。
- 准入集群在约定的资源、API 版本和命名空间中阻止不合规请求。
- 核对清单对比期望引用、获准 digest 与运行容器报告的镜像身份。
- 撤回团队阻止匹配的新 Pod,核对既有 Pod 内部的容器重启,再按有证据的方案替换或隔离实例。
Digest、标签与平台专用镜像
标签是便于阅读的名称,但仓库运营者可以把它改指向其他内容。Digest 是内容标识符,客户端可以对取得的字节重新计算哈希。镜像引用同时包含标签和 digest 时,Kubernetes 使用 digest 拉取。因此,发布卡和准入决定应以 digest 为锚点,标签只作为方便人员识别的附加名称。
多平台镜像不能只记录一个模糊的“镜像版本”。OCI image index 是更高层对象,其中包含平台专用清单的描述符。一个 index digest 可以为 linux/amd64 选择一份清单,为 linux/arm64 选择另一份清单;平台清单再指向配置与层。index 签名与独立的子清单签名针对不同对象。经验证的 digest 链把已签名 index 与其子内容绑定;只有所选策略明确要求时,才需要逐个子清单另行签名。
验收包应保留 index digest、允许的平台集合,以及每个平台预期的子清单 digest。测试环境需要覆盖所有允许的平台,或采用可信的等价模拟。amd64 上成功并不能证明 arm64 运行时会选择预期子清单。启动后的清单还应同时展示 PodSpec 中的原始引用和容器运行时解析出的镜像身份;只有标签列表不足以支撑撤回调查。
签名证明什么,策略还必须决定什么
密码学上有效的签名只表明某个密钥或经过认证的身份对特定内容签了名。它不会替组织决定这个身份是否应被授权。采用 keyless 签名时,至少要匹配预期证书 subject 和预期 OIDC issuer;任一字段使用过宽的正则,都可能把强密码学降格为弱授权。采用托管密钥时,对应问题是:哪个密钥可以为哪个仓库和发布通道签名,谁能修改这种映射,轮换如何审批。
准入策略必须比 signature valid 更丰富。对具体产品,通常应明确:
- 允许的仓库主机和路径,以及获批 digest 或从受控发布卡取得它的方式;
- 签名身份与 issuer、信任根,以及所选方案要求的透明日志检查;
- 必需的证明类型、证明 subject 与镜像的绑定方式,以及实际要判断的 predicate 字段;
- 规范源代码仓库、可信
builder.id、buildType和允许的externalParameters; - 允许的平台、环境与 Kubernetes 资源类型;
- 被禁止的 digest、例外负责人和到期时间,以及撤回条件;
- 没有任何镜像策略匹配时,以及验证服务不可用时的行为。
SLSA 给出了一条有用的 provenance 验证顺序:验证封装签名,确认 statement subject 与制品 digest 相符,检查构建器身份是否在信任根中,再把源、buildType 和外部参数与预期比较。但 provenance 不是代码安全分析,也不承诺列出全部依赖。如果策略允许了错误的源或参数,一份真实的证明完全可能准确描述一个不应发布的构建。只检查证明存在,证明的是“有这份文件”,不是“版本符合预期”。
准入究竟在哪里生效,旁路又在哪里出现
准入控制只评估匹配其配置的 API 请求,不会持续重评全部运行状态。Webhook 的作用域必须列明资源与操作、API 版本、matchPolicy、namespaceSelector、objectSelector 和 matchConditions。被 selector 排除的请求根本不会进入验证,因此与 failurePolicy 取何值无关。
failurePolicy 的含义比很多设计假设的更窄。对已经匹配的请求,Webhook 调用发生错误或超时时,Fail 拒绝请求,Ignore 让 API Server 忽略此次调用失败。若 Webhook 成功收到请求并明确返回 allowed: false,这是显式拒绝,而不是调用错误;Ignore 不会把它改成允许。反过来,Fail 也保护不了因 rules、API 版本或 selector 不匹配而从未触发 Webhook 的请求。
测试不能止于直接创建 Pod。Deployment、StatefulSet、DaemonSet、Job 和 CronJob 保存 Pod 模板,之后由控制器创建真实 Pod。具体策略引擎可能检查上层资源、最终 Pod,或两者都检查。验收必须覆盖每条获准的创建与更新路径,包括 init container、长期 sidecar、用于调试的 ephemeral container,以及集群支持的 image volume。如果引擎不分析其中某类对象,应把它记录成明确边界。
“没有策略匹配”也必须做负向测试。allow、warn 和 deny 提供的保证不同;warn 适合逐步上线,却不构成强制阻止。采用 namespace opt-in 时还要测试新建命名空间:缺少所需标签不应悄悄变成生产旁路。
请求获准不等于当前运行状态已被证明
准入回答的是某个时刻、某次操作的问题。今天新增禁止 digest 或撤销签名身份的信任,并不会自动重评昨天创建的 Pod。因此,运行状态需要独立的核对流程。
最小清单记录应包含:集群、namespace、workload 与负责人、Pod 名称、容器及其角色、规格中的引用、获准 digest、运行时报告的镜像身份、节点平台、启动时间,以及准入时生效的策略版本。清单要覆盖业务容器、init container 和长期 sidecar;调试容器单独标识。快照应能回连发布卡,每个差异都要有负责人和期限。还要单独检查数据时效:Pod 状态可能滞后,尤其是节点与控制平面失联时。验收应要求确认节点实际状态,并把其镜像身份映射到 digest 链,不能仅凭过期状态就认定运行状态已经验证。
即使所有 digest 完全一致,也不能证明进程启动后使用的每个字节都不可变。镜像层为只读,但容器可以有可写层,Pod 还可挂载 emptyDir、持久卷、ConfigMap、Secret 等数据。应用也可能在启动后下载并执行内容,这些字节不会进入主镜像 digest。若允许运行时下载,策略应另行约束来源、完整性和执行;若禁止,则要验证应用行为与 egress 控制。结论必须保持边界:镜像签名验证的是镜像对象,而不是进程此后的每一种状态。
在不造成未经批准停机的情况下撤回镜像
把 digest 加入 deny list、撤销对签名者的信任或收紧 provenance 预期,都应阻止今后匹配的准入操作。但这些动作不会终止已经运行的容器。kubelet 按 restartPolicy 在同一个节点、同一个 Pod 内重启容器时,并不会创建新的 Pod API 对象,因此也没有新的 Pod 准入。仅靠 deny list 不能保证被撤回字节绝不再次启动。自动删除所有相关 Pod 也不是通用安全答案:对需要 quorum 的服务、singleton 组件或 StatefulSet,这可能把安全事件变成可用性事故。
撤回计划从两份清单开始:该 digest 实际运行在哪里,以及哪个已批准 digest 可以替换它。随后由风险负责人按情境选择动作:存在活跃入侵时立即隔离;镜像有漏洞但没有利用迹象时受控 rollout;无法立即替换的关键组件则采用短期例外、补偿控制和明确退出日期。方案要分别覆盖既有 Pod 内的容器重启、控制器创建替代 Pod,以及被排除或静态的路径。没有替代版本本身应成为显式风险决定,不能默默变成“暂不处理”。
对 Deployment,更新期间的可用性由 rollout 策略、maxUnavailable、maxSurge、readiness 检查以及新版本服务能力共同决定。PodDisruptionBudget 约束自愿 eviction,但它本身不管理 workload 控制器执行的 rolling update。若应急团队通过 Eviction API 撤下实例,PDB 和优雅终止时间会参与;直接批量删除则绕过了有用的协调机制。StatefulSet 还需要顺序、数据安全与 quorum 规则;DaemonSet 要定义最小安全节点集合;Job 要明确重试与幂等性。
Rollback 只能指向一个已知且仍满足当前策略的 digest。“回滚到上一个标签”可能在标签被移动后取得另一对象。应先让备用 digest 经过同一路径的准入验证,再运行 canary 或小批次,观察健康度与业务信号,最后扩大 rollout。替换完成前就从仓库删除被撤回对象,可能使新 Pod 无法启动,而旧实例仍继续运行,从而制造“撤回已经完成”的假象。
例外应是有限决定,而不是永久通道
系统插件、紧急发布和供应商镜像有时暂时无法满足统一策略。只有在例外足够窄且可被机器读取时,它才比大范围关闭强制控制更安全。记录至少应包括:精确 digest、集群与 namespace、资源类型、原因、负责人、风险批准人、开始与结束时间、补偿控制和经过测试的退出路径。
按标签或整个仓库设置例外过于宽泛,因为它的未来含义无需修改例外本身就能改变。签名身份正则需要负向样例。Break-glass 流程应留下两条独立记录:谁批准旁路、谁实际执行;窗口结束后,还要核对期间创建的每个 Pod。到期应自动关闭例外,或向负责人产生阻断信号;无人跟进的日历提醒不是强控制。
例外也必须进入撤回演练。团队应预先知道 deny list 是否优先于例外、谁有权延期,以及工作负载是否有安全替换路径。否则,最关键的组件恰好会在事故发生时游离于受控流程之外。
采购应要求什么,获授权的渗透测试可以做什么
采购方需要可复现的证据包,而不是“验证成功”的截图。工作说明应要求:仓库与集群地图、版本化策略导出、规则匹配矩阵、测试 digest 与平台映射、正向和负向测试结果、期望镜像与运行镜像对账、例外登记表、撤回 runbook,以及一次有记录的 rollback 演练。报告中的每个断言还应说明证据类型:测试确认、根据配置推断,或由系统负责人陈述。
获授权测试可以尝试以下旁路:换用另一 workload 类型或 API 版本;进入无标签 namespace;利用不匹配的 object selector;提交没有策略匹配的镜像;制造 Webhook 超时;使用错误的 subject 或 issuer;由正确身份给错误 digest 签名;提交含意外 buildType 的证明;让另一平台选择不同子清单;制造 PodSpec 与运行时清单差异。还要单独终止一个测试容器,观察 kubelet 是否能在既有 Pod 内使用相同镜像重启且不触发新准入。故障模式测试需要约定环境或维护窗口,因为 fail-closed 可能阻断合法发布,而 fail-open 可能临时放行未验证请求。
授权文件要明确测试者能否创建 workload、降低或关闭 Webhook、提交畸形证明、读取仓库元数据、终止容器以测试重启,或触发受控 rollout。删除生产镜像、批量停止 Pod、接触私钥和对 API Server 施加压力,并不会因为任务叫“渗透测试”就自动获准。托管集群或第三方仓库还可能需要各自所有者另行授权。
可通过 Pentest.red 的渗透测试范围表约定上述技术边界:集群、仓库、允许的旁路场景、测试 digest、维护窗口和停止条件。渗透测试服务页面说明合作形式,渗透测试报告示例可帮助双方提前确定证据结构。交付结果是在获授权样本和环境中的技术发现,不是认证,也不能证明所有未测试镜像都安全。
验收矩阵:哪些断言必须拿出证据
应为每个生产集群和每条发布路径填写这张表。“通过”意味着保存了可复现证据,而不是团队口头确认控制存在。
左右滑动查看对照
| 断言 | 正向测试 | 负向测试 | 验收证据 |
|---|---|---|---|
| 标签解析到获批 digest | 部署获批版本时记录预期 digest | 移动标签不能改变获准内容 | 发布卡与规范化 PodSpec |
| 所有平台均获批准 | amd64 与 arm64 取得预期子清单 | 未知平台或子 digest 被拒绝 | index 到平台清单映射 |
| 签名者已获授权 | subject、issuer 和仓库范围都匹配 | 签名虽有效,但错误身份或 issuer 被拒绝 | 验证结果与信任策略版本 |
| Provenance 满足预期 | subject、builder、source、buildType 和参数匹配 | 非官方 fork 或意外参数被阻止 | 证明与策略判断结果 |
| 所有创建路径被覆盖 | 获批 Deployment、Job 与 Pod 执行同一规则 | 另一资源类型或 API 版本无法绕过 | 资源、操作与版本矩阵 |
| Selector 不留下通道 | 每个生产 namespace 都处于 enforce | 缺少或伪造标签产生预期拒绝 | Webhook 或 policy binding 导出与测试日志 |
| Webhook 故障行为明确 | 合法请求按既定可用性模型处理 | 超时准确体现 Fail 或 Ignore,显式 deny 仍为 deny | 故障测试记录 |
| 运行镜像与批准相符 | 每个容器的期望引用与运行时身份一致 | 旧 Pod 或例外 Pod 会被识别为差异 | 含负责人的带日期清单 |
| 撤回阻止受控 API 路径上的新 Pod | 创建新的匹配 Pod 时,被禁 digest 遭拒绝 | 同一 Pod 内的容器重启被识别为没有新准入的独立路径 | deny list、影响清单与准入事件 |
| 替换维持约定可用性 | Canary 与 rollout 达到健康标准 | 新版本失败时停止扩批并执行获批 rollback | Runbook、决策指标与回滚记录 |
| 例外始终有限 | 精确 digest 只在规定范围和时段内生效 | 到期或离开 namespace 后旁路关闭 | 例外登记与到期证据 |
发现项应转化为可测试的工程任务:负责人、精确对象、完成条件和复测方式。关于这种验收纪律,可继续参考渗透测试修复验证。
就绪决定:发布负责人必须回答的四个问题
发布前,负责人应能说出准确的 index digest 与各平台子清单 digest;说明为何签名者、issuer、构建器和参数被允许;为每条 workload 创建路径重现一次负向测试;并提供带日期的实际运行镜像清单。撤回前还要补充获批替代 digest、可用性影响评估、同一 Pod 内的容器重启测试、发布批次和停止标准。
缺少某个答案并不总是要求立即停止业务,但它意味着决定缺少证据,必须作为有负责人和到期时间的有限例外管理。这样,容器基础设施安全才会成为一套可运行的决策系统:组织能证明准入了什么、找到它运行在哪里,并在事故中按计划替换被撤回版本。
PENTEST.RED
讨论渗透测试范围与报价
首先明确系统、访问权限、限制条件和预期证据。
本文以深入研究为基础,经过编辑整理,注重清晰表达和实际应用。