我见过太多团队在 Policy-as-Code 这件事上翻车翻车姿势还出奇地一致安全团队拉个群说要搞合规左移照着文档装了个策略引擎两周憋出四十多条规则CI 里加一道门禁。上线第一个月研发被拦一百多次群里天天吵第三个月门禁悄悄改成只告警第六个月策略仓库最后一次提交停在两个月前README 顶上还挂着一行 TODO。问题从来不在工具。OPA、Kyverno、Conftest、Checkov 这些开源件文档都写得挺清楚跑通一个 demo 也就半小时。真正难的是把“策略”当成一份需要长期维护的软件资产来经营谁写、谁审、怎么测、怎么发、怎么豁免、怎么退场。这篇就围绕企业落地 Policy-as-Code 的完整方案展开流程、规范、工具链三块我都会拆开讲适合正在做云原生治理、合规左移、平台工程的团队参考也适合被门禁折腾过的研发同学看看对面到底在想什么。1. Policy-as-Code 到底在裁决什么先把这个想明白1.1 从“人肉签字”到“机器判定”的那条分界线传统变更管控的链路是这样的研发提工单安全或运维评审评审人打开一堆 YAML 扫两眼看不出问题就签字放行。这条链路的问题不在于人不负责而在于评审人根本没法在几分钟内判断一份几十行的资源配置有没有风险——他不知道这个镜像来自哪个仓库不知道这个 Service 会不会暴露到公网更不知道这个 PVC 挂的存储类是不是允许生产用的。Policy-as-Code 干的事本质是把“能不能做”这个判断从人脑搬到代码里。判断的输入是被检查对象的结构化表示Terraform plan 的 JSON、Kubernetes 的 API 对象、云资源的配置快照输出是一个明确的三元组通过与否、违反了哪条、证据是什么。它管的是事实不管意图。一条策略能成立的前提是存在一个机器可读的输入里面包含了判定所需的全部信息。一个特别常见的误解以为上了策略引擎人就不用管了。恰恰相反策略引擎把人从“逐条看配置”里解放出来转而去做“设计判定规则”和“处理例外”。前者是可复用的一次性投入后者才是长期的日常。1.2 三类最容易被误当成策略的东西第一类是制度条文。“重要系统变更需报备”“上线前须完成安全评估”这类规则没有可结构化的输入机器判定不了。硬要写进策略引擎最后只能做成一个让人手动勾选的表单字段等于把签字仪式搬进了流水线毫无意义。第二类是个人偏好。“我不喜欢用 latest tag”“我觉得命名应该带业务域”这类东西在写下来之前是某个人的习惯写下来之后如果没经过组织层面的共识确认就会变成研发眼中的无妄之灾。策略的合法性来自共识不来自代码。第三类是已经有工具在管的事。代码风格有 lint依赖漏洞有 SCA镜像扫描有漏洞扫描器。这些不该再包一层策略去重复判断策略层应该做的是“消费这些工具的输出做组合判定”比如“高危漏洞未修复的镜像不允许部署”而不是自己再去解析镜像层。1.3 一条规则能不能落地用三个条件卡一下我给团队做策略梳理时通常用三个条件做筛子条件含义反例输入可结构化判定所需信息在某个 API 对象或配置文件中能拿到“该服务是否属于核心链路”——信息在 CMDB 里但没同步进配置判定无歧义两个不同的人看同一个输入结论一致“资源配置是否合理”——合理没有定义结论可执行违规后能给出明确的修改动作“请提升系统安全性”——改哪里三个条件缺一个这条规则就先别写。我见过最典型的翻车案例是有人写了条“禁止使用非官方镜像”结果“官方”的定义散落在三个 wiki 页面里策略里只硬编码了五个前缀半年后业务方换了个合规仓库全量阻塞。2. 策略盘点规则不在文档里在历史工单和评审记录里2.1 别从制度文档开始翻从“本应该拦住”开始翻大部分团队的策略盘点会从安全基线文档开始这是个效率很低的入口。基线文档是写给审计看的措辞抽象跟实际可判定的条件之间隔着一层翻译。更有效的入口是事故和接近事故的记录过去一年的故障复盘找出那些“如果有条规则能提前拦住就不会出事”的条目代码评审里反复出现的同类评论比如同一个 reviewer 在二十个 PR 里都在说“记得加 resource limits”运维同学手工写的巡检脚本和 crontab 里的检查命令这些是野生策略价值极高云账号里的历史配置快照反查哪些资源长期处于“不该有的状态”。我参与过的一次盘点最后产出的六十多条候选策略里从基线文档里抠出来的不到二十条剩下四十多条全来自工单和脚本。原因很简单文档写的是理想状态脚本和工单记录的是真实踩过的坑。2.2 给每条策略打四个标签而不是一个很多人只给策略打“强制/建议”两档这个粒度不够用。我建议至少四个维度级别强制阻断 / 告警 / 观察记录适用范围全集群、特定命名空间、特定标签的工作负载、特定云账号判定依据来自哪条制度、哪次事故、哪个合规项写清楚来源后面评审和豁免都有据可依生命周期永久基线、阶段性收紧、试验性策略。“阶段性收紧”这一档特别容易被忽略。比如大促期间禁止变更核心服务的副本数这种策略有明显的生效窗口如果当初没标注大促结束三个月后它还在拦人就成了策略债。2.3 先出表格再写代码盘点阶段的交付物应该是一张能评审的表格而不是一堆 Rego 文件。表格字段我通常这样定编号策略描述判定条件输入来源执行点级别负责人计划上线P-001工作负载必须声明 CPU/内存 requestsspec.containers[*].resources.requests存在且非空Pod spec集群准入强制平台组第 3 周P-002镜像不得使用 latest 标签image不以:latest结尾且含显式 tagPod spec集群准入 CI强制平台组第 3 周P-003生产命名空间资源必须带 cost-center 标签metadata.labels[cost-center]存在任意资源集群准入告警财务 IT第 5 周表格过一轮跨团队评审比直接甩代码过去评审的效率高一个量级。研发看得懂表格看不懂 Rego而策略一旦挡住研发他们就一定会来看代码那时候再解释成本就高了。3. 工具链选型先定执行点再定工具3.1 四个执行点决定了你需要几套工具策略要在哪里生效比用什么工具重要得多。企业里通常有四个执行点每个点的输入形态和时效要求完全不同IaC 扫描点代码提交前或 PR 阶段输入是 Terraform plan、Helm 渲染结果、K8s manifest 文件。特点是反馈快、成本低、能拦住问题于未然。这一层用 Conftest OPA 或者 Checkov/tfsec 这类专用扫描器都行。CI 门禁点流水线里的强制卡点输入通常是渲染后的完整 manifest。这一层适合放“阻断级”策略因为此时改配置的成本还不高。集群准入点API Server 的 admission webhook输入是提交到集群的实时对象。这是最后一道防线也是研发感知最强的位置——被拦的时候人正在 kubectl apply。Gatekeeper 和 Kyverno 都在这一层。运行时巡检点定时扫描存量资源找出那些在策略上线前就已存在、或者通过绕过路径进来的资源。这一层往往被忽略但它是清理存量的唯一手段。我的经验是四个点不要一次全铺。先做 IaC 和 CI让研发在写代码阶段就形成肌肉记忆等策略稳定了再往准入层推。反过来的顺序会让研发觉得你在针对他。3.2 OPA 路线和 Kyverno 路线差异比想象中大这两条路线的选择本质是“通用性”和“易用性”的取舍。OPA Rego是通用策略引擎不绑定 Kubernetes。它的优势在于一套策略语言可以同时覆盖 K8s、Terraform、API 网关、数据库访问控制等场景策略可以通过 bundle 分发、decision log 集中收集。代价是 Rego 这门语言有门槛——它是声明式的基于 Datalog写惯了命令式代码的人第一次写会很不适应尤其是处理“不存在”这类否定条件时特别容易写错。Kyverno是 K8s 原生的策略直接用 YAML 写支持 validate、mutate、generate 三类操作。研发和运维看着眼熟上手快。缺点是它主要在 K8s 生态内跨出集群边界就不太适用了复杂逻辑用 YAML 表达会比较绕。一个粗略的判断标准如果你的策略 80% 以上都在管 Kubernetes团队里没有专人维护策略引擎选 Kyverno如果你的策略要横跨 IaC、云资源、K8s 多个层面且愿意投入一两个人做策略平台选 OPA。3.3 扫描类工具别和策略引擎混为一谈Checkov、tfsec、Terrascan 这类工具自带了几百条内置规则开箱即用很多人拿来当 Policy-as-Code 的全部。它们的问题在于内置规则是通用最佳实践不一定匹配你的组织现状——比如它可能要求所有 S3 桶都开版本控制但你的日志桶开了版本控制成本会翻三倍。正确的用法是把它们当成“策略来源”而不是“策略终点”内置规则里挑出你认可的作为基线的起点不认可的关掉组织特有的规则自己写。同时要注意版本锁定这类工具的内置规则集会随版本变化某次升级后新增十条规则CI 突然全红这种事故很常见。3.4 一张对照表把选型说清楚执行点典型工具输入形态反馈时机适合放的策略IaC 扫描Conftest、Checkov、tfsecplan JSON / manifest提交前成本标签、资源规格、加密配置CI 门禁Conftest、OPA eval、Kyverno CLI渲染后 manifest合并前镜像来源、副本数、探针配置集群准入Gatekeeper、KyvernoK8s API 对象apply 时安全上下文、资源限制、网络策略运行时巡检Cloud Custodian、定时 Job OPA资源快照定时存量清理、漂移检测、闲置资源4. 策略代码也是代码工程化这步省不掉4.1 仓库目录怎么切决定了后面三年的维护成本策略仓库最忌讳的就是所有规则平铺在一个目录里。我推荐的切法是按“执行点 领域”两级policy-repo/ ├── policies/ │ ├── kubernetes/ │ │ ├── workload/ │ │ │ ├── resources.rego │ │ │ └── image-source.rego │ │ └── network/ │ ├── terraform/ │ │ └── tagging.rego │ └── shared/ │ └── helpers.rego ├── tests/ │ └── kubernetes/workload/resources_test.rego ├── bundles/ │ └── bundle.yaml └── docs/ └── policy-catalog.mdshared目录放公共函数比如“判断镜像是否来自内部仓库”这种被多条策略复用的逻辑。这一步很关键我见过策略仓库里同一个判断逻辑被复制了七份后来内部仓库地址变了改了六份漏了一份导致两个环境的判定结果不一致。docs/policy-catalog.md是给研发看的策略目录每条策略一句话说明加一个违规示例。这个文档不用手写可以从策略注释里生成但必须有。4.2 策略分层平台层管底线业务层管特色把所有策略混在一起管理会带来一个死结平台组想收紧基线业务组说这条不适用。解决办法是分层平台基线层全组织强制不可豁免数量控制在二十条以内。这一层只放“违反了就是安全事故”的规则比如禁止特权容器、禁止 hostPath 挂载根目录、必须设置资源限制。业务规则层由业务团队在自己的命名空间范围内定义平台组提供模板和测试框架。比如某个团队要求所有服务的日志必须输出到指定采集端这是他们自己的事。临时收紧层带过期时间的策略到期自动降级为告警。实现方式是在策略里读一个配置对象里面记录生效窗口。分层的实际价值在于责任归属清晰。研发被拦的时候能立刻知道该找谁——基线层的问题找平台业务层的问题找自己团队。4.3 单测不写策略迟早变成玄学Rego 的单测框架opa test用起来很轻一个策略配三到五个用例就能覆盖主要分支。关键是用例要包含反向场景很多人只写“合规的输入应该通过”不写“不合规的输入应该被拦住”结果策略写得再错测试也是绿的。package kubernetes.workload.resources import future.keywords.if # 检查所有容器都声明了 requests deny[msg] if { container : input.spec.containers[_] not container.resources.requests.cpu msg : sprintf(容器 %s 未声明 CPU requests, [container.name]) } deny[msg] if { container : input.spec.containers[_] not container.resources.requests.memory msg : sprintf(容器 %s 未声明内存 requests, [container.name]) }对应的测试package kubernetes.workload.resources test_missing_cpu_requests if { input : {spec: {containers: [{name: app, resources: {requests: {memory: 128Mi}}}]}} count(deny) 1 } test_all_requests_present if { input : {spec: {containers: [{name: app, resources: {requests: {cpu: 100m, memory: 128Mi}}}]}} count(deny) 0 } test_empty_containers if { input : {spec: {containers: []}} count(deny) 0 }第三个用例是边界场景专门测空列表。这种用例看着没意义但它能防住“用了_遍历但没处理空集合”导致的全量误报。一个实操提醒写 Rego 的时候处理“字段不存在”比处理“字段值不对”难得多。input.spec.foo在字段缺失时会 undefined整个规则体静默失败策略表现为“永远通过”。这类 bug 单测很难发现因为测试输入里字段都在。对付它的办法是在策略顶部先做一次输入结构断言或者在测试里专门造几条“缺字段”的输入。4.4 策略升级会打死存量资源版本和兼容要提前想策略是有版本的而且策略版本的升级是破坏性变更。你新加一条“必须设置 seccomp profile”存量上千个工作负载立刻全部变成不合规。处理这个问题的标准做法是“策略版本 存量豁免”双轨每条策略带一个 semver 版本号破坏性变更升主版本新版本上线时同时提供一个“存量资源豁免名单”名单里是策略生效前创建的资源的唯一标识UID 或创建时间早于某时间戳策略里读这个名单命中则跳过判定但在 decision log 里记录下来供后续清理。Kyverno 里可以用validationFailureAction: Audit配合exclude资源选择器做类似效果Gatekeeper 里可以用enforcementAction: dryrun加 constraint 的match.excludedNamespaces。核心思路一致新策略不能一次性打断存量只能拦住增量存量靠时间清。5. 门禁与豁免策略不能成为研发的敌人5.1 三档处置别只有“拦”和“不拦”策略的处置力度应该分三档而且默认从最轻的开始档位行为适用阶段研发感知记录只写 decision log不影响流程策略试验期无感告警PR 评论 / 工单 / IM 通知推广期弱阻断拒绝合并或拒绝 apply稳定期强一个新策略从记录走到阻断我建议至少留四周。前三周收集数据看命中率、看误报、看违规集中在哪些团队第四周才开始告警。很多策略在这个过程里会被发现设计得不合理——比如命中率高达 40%那多半是策略太严或者基线本身没跟上硬推只会引发对抗。5.2 豁免流程设计不好整个体系就烂了豁免是 Policy-as-Code 里最容易烂掉的一环因为它天然是“破坏规则”的行为如果处理得随意策略就会形同虚设如果处理得太死业务方会绕过去。我的做法是把豁免也做成代码apiVersion: policy.example/v1 kind: PolicyExemption metadata: name: legacy-batch-job spec: policyId: P-001 target: namespace: batch resourceName: legacy-importer reason: 存量任务改造排期在下一季度 requestedBy: team-batch approvedBy: platform-owner expiresAt: 2025-06-30三个要点豁免必须绑定到具体策略和具体对象不能是“这个命名空间全部豁免”这种粗粒度。粗粒度豁免一旦出现通常意味着这块区域从此脱离治理视野。豁免必须有到期时间。没有过期时间的豁免等于永久例外而永久例外会越积越多。到期后策略自动恢复生效如果需要延期就重新走一次审批——这个摩擦是有意设计的。豁免必须可审计。谁申请的、谁批的、理由是什么、什么时候到期这些要能随时查出来。很多团队的做法是在策略里硬编码一个白名单数组改一次要发一次版本这种做法的下场通常是白名单被注释掉然后忘了删。5.3 报错信息要写成人话这是策略的“用户体验”策略拦截时输出的信息直接决定了研发对你的态度。对比两种写法# 写法一 deny[msg] { msg : policy violation: P-001 } # 写法二 deny[msg] { not input.spec.containers[_].resources.requests msg : sprintf( 工作负载 %s 未声明资源 requests。请在 containers 下补充 resources.requests.cpu 和 memory。参考https://内部文档/policy/P-001, [input.metadata.name] ) }第一种写法会让研发去翻文档、找负责人、问群里平均解决时间是小时级第二种写法当场就能改分钟级。这条经验看起来很小但对推广成败影响极大——研发对策略体系的印象八成来自他被拦的那几次看到的提示。好的人话提示包含四要素哪个对象、违反了哪条、怎么改、去哪查。缺任何一个都会增加一次沟通成本。6. 灰度上线与度量策略体系也需要看诊指标6.1 先跑审计模式用真实数据校准阈值任何策略上线前都应该先在审计模式下跑够数据量。做法很简单策略逻辑不变处置档位设为“记录”跑两到四周然后看三个数命中次数一周内被命中多少次。如果一条策略一周命中上千次说明它不是策略问题是基线问题——要么基线早就该改要么你的策略选错了治理对象。命中分布集中在哪几个团队、哪几类资源。分布过于集中说明是个案可以走豁免分布很散说明是共性问题值得推动。误报率抽样看被命中的对象有多少是真违规。误报超过 10% 就必须先改策略再上告警。我见过一个团队跳过这一步直接上阻断结果策略里把 initContainer 和主容器一起判而他们的基础镜像恰好有个 initContainer 没设 requests导致全集群新部署全部失败。这类问题的发现成本审计模式下几乎为零阻断模式下就是一次生产事故。6.2 该盯的指标不多但这几个必须有策略体系的度量不需要太复杂五个指标足够反映健康状况指标定义健康区间异常信号策略覆盖率已纳管资源 / 全部资源 90%长期低于 70% 说明有治理盲区违规拦截数单位时间阻断次数稳定或下降突然飙升多为策略变更导致豁免存量生效中的豁免数量稳定持续增长说明基线脱离实际豁免平均存活期从申请到关闭的天数 90 天超过半年说明豁免流程失效策略变更失败率上线后回滚的策略占比 10%偏高说明测试覆盖不足其中我最看重“豁免平均存活期”。这个数字直接反映策略和现实的距离——如果大量豁免活了半年以上还没关闭说明这些策略要么设计得不合理要么对应的技术债没人管两种情况都需要重新审视。6.3 策略债要定期清不清就会反噬策略债这个词是我自己习惯的叫法指的是那些“已经不再产生价值但仍然在运行的策略”。来源通常有四类当初为了某次专项治理临时加的专项结束了策略还在对应的技术债已经被其他手段解决了策略变成冗余策略的判定条件和现实脱节比如判断“镜像来自内部仓库”用的前缀早就废弃了但因为没触发过所以没人发现豁免名单越拉越长策略实际上只对少数新资源生效。清理的节奏我建议是每季度一次方法很朴素拉出所有策略的最近 90 天触发记录触发次数为零的策略逐条 review——是真的没有违规还是判定条件失效了前者可以考虑降级或下线后者要修。这个动作听起来琐碎但不做的话两年后你的策略仓库里会有一半是僵尸规则。而僵尸规则最危险的地方在于它们平时不响某天因为一个边缘输入突然响了然后所有人一脸茫然地去查这条规则是哪来的、为什么要拦。最后分享一个我自己踩过的坑策略上线初期我习惯把所有规则写在一个大文件里觉得方便管理。后来某次要临时下线一条策略因为规则之间共享了 helper 函数改一处牵连三条最后只能整包回滚。从那以后我就坚持一个原则——一条策略一个文件公共逻辑进 shared允许重复不允许耦合。策略这种资产可独立回滚比代码复用重要得多。