TT Lab
开始
学习 学习路径 课程

策略即代码

策略已经应用,却什么都没拦下来

在 TT Lab 中继续学习

目标

用 Kubernetes 内置的 ValidatingAdmissionPolicy(CEL)创建检查 Pod 的规则,从警告提升到拦截,在消息中写入违规的值,并亲手加上例外、范围和参数。

为什么重要

ValidatingAdmissionPolicy 让你不必安装外部策略引擎,就能在 API 服务器内部完成验证。与 Webhook 不同,它没有网络往返,也没有证书,更不会因为引擎死掉而让集群停摆。代价是要多学一样东西——判定规则和适用范围是不同的对象。多亏这种分离,同一条规则可以对某些团队以警告方式、对另一些团队以拦截方式挂上,还可以只把规则的值放到 ConfigMap 中,不修改策略就能更改。反过来,如果不了解这种分离,就会在“策略明明应用了,却什么都拦不住”这种地方困很久。在实际工作中,第一次引入策略时,总是从 Warn 开始,看看撞上了什么,用 matchConditions 把系统组件排除在外,然后把范围收窄,再提升到 Deny。跳过这个顺序的策略,会让部署停摆并被回退,而之后就再也没有人重新启用它。

步骤

  1. 在 /root/vap 中工作(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。在 /root/vap/policy.yaml 中编写 admissionregistration.k8s.io/v1 的 ValidatingAdmissionPolicy require-cpu-limit。matchConstraints.resourceRules 匹配核心组("")v1 pods 的 CREATE 和 UPDATE,validation 要求所有容器都具有 resources.limits.cpu。再创建两个测试用 Pod——/root/vap/pod-nocpu.yaml 中容器 web-tier 只有 memory 限制,/root/vap/pod-ok.yaml 中容器 web-full 同时具有 cpu 和 memory 限制。应用策略之后,创建一个没有任何标签的命名空间 team-open,用 kubectl create --dry-run=server 把 pod-nocpu.yaml 发送到那里,并把输出保存到 /root/vap/01-nobinding.txt。
  2. 创建命名空间 team-warn,并加上标签 cpu-limit=warn。在 /root/vap/binding-warn.yaml 中编写 ValidatingAdmissionPolicyBinding cpu-limit-warn——policyName 为 require-cpu-limit,validationActions 为 ["Warn", "Audit"],matchResources.namespaceSelector.matchLabels 为 cpu-limit: warn。应用之后,用 --dry-run=server 把 pod-nocpu.yaml 发送到 team-warn,并把输出保存到 /root/vap/02-warn.txt(连同标准错误)。即使出现警告,Pod 也必须能够创建。
  3. 创建命名空间 team-deny,并加上标签 cpu-limit=deny。在 /root/vap/binding-deny.yaml 中编写第二个绑定 cpu-limit-deny——同一个 policyName,validationActions 为 ["Deny"],选择器为 cpu-limit: deny。应用之后,把 pod-nocpu.yaml 发送到 team-deny,并把输出保存到 /root/vap/03-deny.txt,接着再发送 pod-ok.yaml,确认它能通过。警告用的绑定(cpu-limit-warn)不要删除,保持原样——两个范围同时生效,才是实际推行时的样子。
  4. 修改 /root/vap/policy.yaml,在 spec.variables 中放入 noCpu——计算没有 cpu 限制的容器的名称列表。把 validation 改为 size(variables.noCpu) == 0,并用 messageExpression 代替 message,生成包含这些名称和 Pod 名称的消息。然后在 /root/vap/pod-two.yaml 中创建具有两个容器的 Pod probe-two——web-nolimit 根本没有 resources,cache-ok 同时具有 cpu 和 memory 限制。重新应用策略之后,把它发送到 team-deny,并把拒绝消息保存到 /root/vap/04-message.txt。消息中只能出现 web-nolimit,不能出现 cache-ok。
  5. 在 /root/vap/policy.yaml 中加入 spec.matchConditions。有两个条件——skip-kube-system-sa 让请求者如果是以 system:serviceaccount:kube-system: 开头的服务账号,就跳过策略;skip-exempt-pods 让带有标签 cpu-limit-exempt: "true" 的 Pod 被跳过。在 /root/vap/pod-exempt.yaml 中创建带有该标签的 Pod probe-exempt(容器 web-tier,只有 memory 限制)。重新应用策略之后,在 team-deny 中确认两件事,并把输出保存到 /root/vap/05-skipped.txt——(1) kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n team-deny -f pod-nocpu.yaml --dry-run=server,(2) 照常发送 pod-exempt.yaml。两者都必须能创建。没有标签的 pod-nocpu.yaml 仍然必须被拒绝。
  6. 创建命名空间 vap-params 和 team-images(标签 registry-check=on)。在 vap-params 中创建 ConfigMap allowed-registries,键 allowed 的值必须恰好是 registry.internal/,ghcr.io/labhub/。在 /root/vap/policy-registries.yaml 中编写第二个策略 allowed-registries——spec.paramKind 为 apiVersion: v1、kind: ConfigMap,用相同的 matchConstraints 匹配 Pod,拒绝镜像不以 params.data['allowed'] 用逗号拆分出的列表中任何一项开头的情况。在 /root/vap/binding-registries.yaml 中编写绑定 allowed-registries——validationActions 为 ["Deny"],paramRef 是那个 ConfigMap(name、namespace,parameterNotFoundAction: Deny),选择器为 registry-check: "on"。在 /root/vap/pod-img-bad.yaml 中创建镜像为 docker.io/nginx:1.27 的 Pod probe-img-bad(容器 web-img,包含 cpu 和 memory 限制),依次把 pod-ok.yaml 和 pod-img-bad.yaml 发送到 team-images,并把两份输出保存到 /root/vap/06-param.txt。
  7. 创建 /root/vap/scope.sh <네임스페이스>(占位符为命名空间)。用 --dry-run=server 把 /root/vap/pod-nocpu.yaml 发送到该命名空间,如果被拒绝,就输出一行 DENY 并以退出码 3 结束;如果只出现警告而创建成功,就输出 WARN 并以 2 结束;如果毫无提示地创建成功,就输出 OPEN 并以 0 结束。输出只有这一个词。不要通过读取命名空间名称或标签来判断,而要按实际请求的结果来判定——评分器会暂时修改 team-open 的标签,同时调用这个脚本,再恢复原样。创建之后,依次对 team-deny、team-warn、team-open 三处运行,把结果以 <네임스페이스> <낱말> 的形式(占位符依次为命名空间与词)每行一条,保存到 /root/vap/07-scope.txt。
  8. 在 /root/vap/policy.yaml 中加入第二条规则——变量 noMem(没有 memory 限制的容器名称列表),以及检查 size(variables.noMem) == 0 的第二个 validation。这个 validation 的 messageExpression 中必须包含词 memory 和该容器的名称,而第一个 validation 的消息中必须包含词 cpu 和该容器的名称。在 /root/vap/pod-mixed.yaml 中创建 Pod probe-mixed——容器 web-tier 只有 memory 限制,cache-tier 只有 cpu 限制。重新应用策略之后,依次把这个 Pod 发送到 team-warn 和 team-deny,并把两份输出保存到 /root/vap/08-two-rules.txt。Warn 一侧会同时提示两条规则,而 Deny 一侧只提示一条。

参考

只应用策略,什么都不会发生

在 /root/vap 中工作(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。在 /root/vap/policy.yaml 中编写 admissionregistration.k8s.io/v1 的 ValidatingAdmissionPolicy require-cpu-limit。matchConstraints.resourceRules 匹配核心组("")v1 pods 的 CREATE 和 UPDATE,validation 要求所有容器都具有 resources.limits.cpu。再创建两个测试用 Pod——/root/vap/pod-nocpu.yaml 中容器 web-tier 只有 memory 限制,/root/vap/pod-ok.yaml 中容器 web-full 同时具有 cpu 和 memory 限制。应用策略之后,创建一个没有任何标签的命名空间 team-open,用 kubectl create --dry-run=server 把 pod-nocpu.yaml 发送到那里,并把输出保存到 /root/vap/01-nobinding.txt。

策略只定义“如何判定”,“在哪里适用”由另外的绑定对象决定。所以只应用策略,API 服务器连那个表达式都不会评估。在 CEL 中检查整个列表时使用 all(...),对可能不存在的字段,要先用 has(...) 检查。kubectl create -f <파일> --dry-run=server(占位符为文件)会让请求照常通过准入,但不留下对象。拒绝消息和警告,都与真实请求一样出现。

一挂上绑定,警告就开始出现

创建命名空间 team-warn,并加上标签 cpu-limit=warn。在 /root/vap/binding-warn.yaml 中编写 ValidatingAdmissionPolicyBinding cpu-limit-warn——policyName 为 require-cpu-limit,validationActions 为 ["Warn", "Audit"],matchResources.namespaceSelector.matchLabels 为 cpu-limit: warn。应用之后,用 --dry-run=server 把 pod-nocpu.yaml 发送到 team-warn,并把输出保存到 /root/vap/02-warn.txt(连同标准错误)。即使出现警告,Pod 也必须能够创建。

绑定决定“把这个策略用在哪里、以什么强度”。Warn 只通过响应头返回警告并放行请求,Audit 则在审计日志中留下注解。警告是通过标准错误而不是标准输出输出的,所以要加上 2>&1,才会一并写入文件。namespaceSelector 查看的是命名空间对象的标签。

同一个策略提升到 Deny,请求就被拦住了

创建命名空间 team-deny,并加上标签 cpu-limit=deny。在 /root/vap/binding-deny.yaml 中编写第二个绑定 cpu-limit-deny——同一个 policyName,validationActions 为 ["Deny"],选择器为 cpu-limit: deny。应用之后,把 pod-nocpu.yaml 发送到 team-deny,并把输出保存到 /root/vap/03-deny.txt,接着再发送 pod-ok.yaml,确认它能通过。警告用的绑定(cpu-limit-warn)不要删除,保持原样——两个范围同时生效,才是实际推行时的样子。

一个策略可以挂上多个绑定,每个绑定有自己的范围和自己的强度。所以“同一条规则,对某些团队是警告,对另一些团队是拦截”,无需复制策略就能做到。拒绝消息中会同时打印是哪个策略、哪个绑定拦住的——这是多条规则重叠时查找原因的线索。

拒绝消息会说明是哪个容器

修改 /root/vap/policy.yaml,在 spec.variables 中放入 noCpu——计算没有 cpu 限制的容器的名称列表。把 validation 改为 size(variables.noCpu) == 0,并用 messageExpression 代替 message,生成包含这些名称和 Pod 名称的消息。然后在 /root/vap/pod-two.yaml 中创建具有两个容器的 Pod probe-two——web-nolimit 根本没有 resources,cache-ok 同时具有 cpu 和 memory 限制。重新应用策略之后,把它发送到 team-deny,并把拒绝消息保存到 /root/vap/04-message.txt。消息中只能出现 web-nolimit,不能出现 cache-ok。

variables 是把一个表达式起个名字、再用 variables.<이름>(占位符为变量名称)重复使用的机制。它是惰性求值的,如果 validation 中没用到,就不会计算。用 filter(...) 只保留违规的,再用 map(...) 只取出名称,就得到了列表。字符串列表可以用 join(', ') 连接起来。如果 messageExpression 出错,API 服务器会悄悄退回到 message——如果消息没有变化,就要怀疑表达式。

让策略完全不去看某些请求

在 /root/vap/policy.yaml 中加入 spec.matchConditions。有两个条件——skip-kube-system-sa 让请求者如果是以 system:serviceaccount:kube-system: 开头的服务账号,就跳过策略;skip-exempt-pods 让带有标签 cpu-limit-exempt: "true" 的 Pod 被跳过。在 /root/vap/pod-exempt.yaml 中创建带有该标签的 Pod probe-exempt(容器 web-tier,只有 memory 限制)。重新应用策略之后,在 team-deny 中确认两件事,并把输出保存到 /root/vap/05-skipped.txt——(1) kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n team-deny -f pod-nocpu.yaml --dry-run=server,(2) 照常发送 pod-exempt.yaml。两者都必须能创建。没有标签的 pod-nocpu.yaml 仍然必须被拒绝。

matchConditions 会对 matchConstraints 选出的请求再过滤一次。只有所有条件都为真时才会评估 validations,所以“想要跳过”的条件要写成否定形式。request 中包含请求者信息(request.userInfo.username)。如果连控制器创建的 Pod 也拦住,集群就无法自我恢复,所以排除系统主体,是推行时的第一道安全装置。可以用 kubectl --as 假装是其他主体。

把规则的值放到 ConfigMap 中,不修改策略就能更改

创建命名空间 vap-params 和 team-images(标签 registry-check=on)。在 vap-params 中创建 ConfigMap allowed-registries,键 allowed 的值必须恰好是 registry.internal/,ghcr.io/labhub/。在 /root/vap/policy-registries.yaml 中编写第二个策略 allowed-registries——spec.paramKind 为 apiVersion: v1、kind: ConfigMap,用相同的 matchConstraints 匹配 Pod,拒绝镜像不以 params.data['allowed'] 用逗号拆分出的列表中任何一项开头的情况。在 /root/vap/binding-registries.yaml 中编写绑定 allowed-registries——validationActions 为 ["Deny"],paramRef 是那个 ConfigMap(name、namespace,parameterNotFoundAction: Deny),选择器为 registry-check: "on"。在 /root/vap/pod-img-bad.yaml 中创建镜像为 docker.io/nginx:1.27 的 Pod probe-img-bad(容器 web-img,包含 cpu 和 memory 限制),依次把 pod-ok.yaml 和 pod-img-bad.yaml 发送到 team-images,并把两份输出保存到 /root/vap/06-param.txt。

paramKind 决定“这个策略读取什么作为参数”,绑定的 paramRef 决定“其中具体是哪个对象”。所以同一个策略可以为每个团队挂上不同的值。CEL 的字符串有 split 和 startsWith,列表有 exists。找不到参数时怎么办,由 parameterNotFoundAction 决定——如果是安全规则,默认应该是拦截而不是放开。在本实验的后半部分,评分器会暂时修改 ConfigMap 的值再改回去。如果把允许列表写死在表达式里,就会在那时露馅。

同一个请求在各个命名空间得到不同的答案

创建 /root/vap/scope.sh <네임스페이스>(占位符为命名空间)。用 --dry-run=server 把 /root/vap/pod-nocpu.yaml 发送到该命名空间,如果被拒绝,就输出一行 DENY 并以退出码 3 结束;如果只出现警告而创建成功,就输出 WARN 并以 2 结束;如果毫无提示地创建成功,就输出 OPEN 并以 0 结束。输出只有这一个词。不要通过读取命名空间名称或标签来判断,而要按实际请求的结果来判定——评分器会暂时修改 team-open 的标签,同时调用这个脚本,再恢复原样。创建之后,依次对 team-deny、team-warn、team-open 三处运行,把结果以 <네임스페이스> <낱말> 的形式(占位符依次为命名空间与词)每行一条,保存到 /root/vap/07-scope.txt。

警告是通过标准错误输出的,所以必须合并输出才能看到。拒绝的退出码不是 0,警告的退出码是 0——先区分这两者,再看是否有警告。如果加上 set -e,脚本会在被拒绝时先死掉。准入是根据命名空间的标签来决定范围的,所以同一个清单会因位置不同而得到不同的答案。

把两条规则放进一个策略,Deny 和 Warn 就会告知不同的内容

在 /root/vap/policy.yaml 中加入第二条规则——变量 noMem(没有 memory 限制的容器名称列表),以及检查 size(variables.noMem) == 0 的第二个 validation。这个 validation 的 messageExpression 中必须包含词 memory 和该容器的名称,而第一个 validation 的消息中必须包含词 cpu 和该容器的名称。在 /root/vap/pod-mixed.yaml 中创建 Pod probe-mixed——容器 web-tier 只有 memory 限制,cache-tier 只有 cpu 限制。重新应用策略之后,依次把这个 Pod 发送到 team-warn 和 team-deny,并把两份输出保存到 /root/vap/08-two-rules.txt。Warn 一侧会同时提示两条规则,而 Deny 一侧只提示一条。

validations 是列表,每一项有自己的 expression 和自己的 messageExpression。变量由多个 validation 共用。Deny 在第一个失败的项上就结束请求,而 Warn 会把所有失败的都以警告返回——所以推行初期的 Warn 阶段,能一次性显示“需要把什么都修好”。如果两条消息无法区分,就没有人知道撞上的是哪条规则。