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

策略即代码

只把一个 Webhook 改成 Fail,集群就停了

在 TT Lab 中继续学习

目标

亲自在三个层次上收窄准入 Webhook 的适用范围,真正制造出策略服务器死掉的状态,通过计时确认 failurePolicy 和 timeoutSeconds 交换了什么,并为集群中的所有 Webhook 制作回答“这个一旦死掉,什么会停摆”的报告和检查器。

为什么重要

把策略引入事故的记录汇总起来看,因为规则写错而出的事故很少。大部分是两种情况之一——挂得太宽,或者没有决定故障时的行为。挂得宽的成本,不是策略执行一次所花的时间,而是附加在进入的所有写入请求上的延迟税,而集群里控制器发出的请求,比人发出的请求多得多。更可怕的是第二种。如果把策略设置成连 kube-system 也要看,而策略服务器又死掉了,那么在 failurePolicy: Fail 之下,集群会无法自行恢复——因为想要让服务器复活的部署,也要经过那个策略。范围和故障模式不是分别选择的值,而是成对选择的值,本实验就是亲手把这一对转一遍。最后制作的爆炸半径报告,要在已经启用了策略的集群里,现在就去制作,而不是在启用策略之前。

步骤

  1. 在 /root/polscope 中工作(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。先创建命名空间 scope-app、scope-ops、scope-builtin,并在这三个命名空间中都用 kubectl create sa default -n <네임스페이스>(占位符为命名空间)亲手创建 default 服务账号(这个集群中没有控制器管理器,不会自动生成)。在 /root/polscope/webhook-wide.yaml 中编写 ValidatingWebhookConfiguration scope-wide-audit——Webhook 名称为 wide.polscope.local,admissionReviewVersions 为 ["v1"],sideEffects 为 None,failurePolicy 为 Ignore,timeoutSeconds 为 2,clientConfig.url 为 https://192.0.2.77:8443/validate。rules 设得最宽——apiGroups: ["*"]、apiVersions: ["*"]、operations: ["CREATE", "UPDATE"]、resources: ["*"]、scope: "*",一个选择器都不设置。再创建三个测试用清单——/root/polscope/pod.yaml(Pod probe-app,容器 web,镜像 registry.internal/app:1.0)、/root/polscope/pod-guard.yaml(Pod probe-guard,标签 polscope.io/guard: "on",同样的容器)、/root/polscope/cm.yaml(ConfigMap probe-cm,数据 a: b)。应用之后,测量四个请求的耗时——kubectl create -n scope-app -f pod.yaml --dry-run=server、kubectl create -n scope-app -f cm.yaml --dry-run=server、kubectl create ns probe-ns --dry-run=server、kubectl get pods -n scope-app。耗时 1 秒以上的记为 SLOW,否则记为 FAST,以 <이름> <낱말> 的形式(占位符依次为名称与词)在 /root/polscope/01-reach.txt 中写下 pod-create、configmap-create、namespace-create、pod-list 四行。
  2. 在 /root/polscope/webhook-strict.yaml 中编写第二个配置 scope-strict——Webhook 名称为 strict.polscope.local,rules 与 scope-wide-audit 一模一样设得最宽,只有另外三处不同:failurePolicy 为 Fail,timeoutSeconds 为 3,clientConfig.url 是连接会被拒绝的地址 https://127.0.0.1:19443/validate。scope-wide-audit 不要删除,保持原样——同一个范围内,只有故障模式不同的两个并排站立,就是本实验的对照组。应用之后,尝试四件事,看是否被拦住,被拦住记为 BLOCK,通过记为 PASS,以 <이름> <낱말> 的形式(占位符依次为名称与词)在 /root/polscope/02-blocked.txt 中写四行——pod-create-scope-app(kubectl create -n scope-app -f pod.yaml --dry-run=server)、configmap-kube-system(kubectl create -n kube-system -f cm.yaml --dry-run=server)、namespace-create(kubectl create ns probe-ns --dry-run=server)、webhookconfig-edit(再执行一次 kubectl apply -f webhook-strict.yaml)。
  3. 先试着用豁免标签脱身——运行 kubectl label ns kube-system polscope.io/admission=exempt --overwrite,把输出连同标准错误一起保存到 /root/polscope/03-lockout.txt(会被拦住)。然后给 /root/polscope/webhook-strict.yaml 中的 scope-strict 加入两个 namespaceSelector.matchExpressions 并重新应用——(1) 键 kubernetes.io/metadata.name,运算符 NotIn,值 ["kube-system", "kube-node-lease", "kube-public"],(2) 键 polscope.io/admission,运算符 NotIn,值 ["exempt"]。现在标签可以贴上了——给 kube-system 和 scope-ops 加上 polscope.io/admission=exempt。最后把 /root/polscope/pod-guard.yaml 用 --dry-run=server 发送到三个命名空间,以 <네임스페이스> <BLOCK|PASS> 的形式(占位符为命名空间)在 /root/polscope/03-escape.txt 中写下 kube-system、scope-ops、scope-app 三行。
  4. 把 /root/polscope/webhook-strict.yaml 中的 scope-strict 在两个层次上收窄——rules 只针对核心组("")v1 pods 的 CREATE 和 UPDATE,scope 改为 Namespaced,并用 objectSelector.matchLabels 只看带有 polscope.io/guard: "on" 标签的对象。第 3 步的 namespaceSelector 保持原样。应用之后,重新发送四个请求,以 <이름> <좁히기전> <좁힌뒤> 的形式(占位符依次为名称、收窄之前、收窄之后)在 /root/polscope/04-narrow.txt 中写四行(收窄之前的值是在第 2 步看到的)——pod-guard-scope-app(把 pod-guard.yaml 发送到 scope-app)、pod-plain-scope-app(把 pod.yaml 发送到 scope-app)、configmap-scope-app(把 cm.yaml 发送到 scope-app)、namespace-create(kubectl create ns probe-ns --dry-run=server)。判定是 BLOCK 或 PASS。
  5. 给 /root/polscope/webhook-strict.yaml 中的 scope-strict 加入两个 matchConditions 并重新应用——skip-kube-system-sa 让请求者如果是以 system:serviceaccount:kube-system: 开头的服务账号,就不调用 Webhook,skip-break-glass 让请求者的组中如果有 polscope:break-glass,就不调用。应用之后,把同一个 /root/polscope/pod-guard.yaml 以三种主体发送到 scope-app,以 <이름> <BLOCK|PASS> 的形式(占位符为名称)在 /root/polscope/05-conditions.txt 中写下 kube-system-sa、break-glass、normal 三行——kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n scope-app -f pod-guard.yaml --dry-run=server、kubectl --as=oncall --as-group=polscope:break-glass --as-group=system:masters create -n scope-app -f pod-guard.yaml --dry-run=server,以及用当前账号原样发送一次。
  6. 在 /root/polscope/webhook-slow.yaml 中编写第三个配置 scope-slow——Webhook 名称 slow.polscope.local,clientConfig.url 是完全没有响应的地址 https://192.0.2.77:8443/validate,sideEffects 为 None,namespaceSelector.matchExpressions 的键 kubernetes.io/metadata.name 为 In ["scope-ops"],objectSelector.matchLabels 为 polscope.io/slow: "on",rules 为核心组 v1 pods 的 CREATE 和 UPDATE(scope: "Namespaced"),最终状态为 failurePolicy: Fail 和 timeoutSeconds: 3。在 /root/polscope/pod-slow.yaml 中创建 Pod probe-slow(标签 polscope.io/slow: "on",容器 web,镜像 registry.internal/app:1.0)。然后创建 /root/polscope/timeout-probe.sh <Fail|Ignore> <초>(占位符为秒)——把 scope-slow 暂时改为这两个值,用 --dry-run=server 把 pod-slow.yaml 向 scope-ops 发送一次并测量耗时,务必恢复为原来的值之后,只输出一行 BLOCK <초> 或 PASS <초>(秒为四舍五入后的整数)。用这个脚本测量三次,以 <failurePolicy> <타임아웃> <BLOCK|PASS> <걸린초> 的形式(占位符依次为 failurePolicy、超时、耗时秒数)在 /root/polscope/06-timeout.txt 中写三行——分别是 Fail 6、Ignore 6、Fail 4 各一次。
  7. 就在现在这个没有 Webhook 服务器的状态下,在同一个位置建立内置策略进行比较。给命名空间 scope-builtin 加上标签 pod-security.kubernetes.io/enforce=baseline。在 /root/polscope/vap-owner.yaml 中一并编写 ValidatingAdmissionPolicy polscope-require-owner(匹配核心组 v1 pods 的 CREATE 和 UPDATE,要求 Pod 元数据中有 owner 标签)和同名的 ValidatingAdmissionPolicyBinding(validationActions 为 ["Deny"],matchResources.namespaceSelector 为 kubernetes.io/metadata.name In ["scope-builtin"])并应用。再创建三个测试用 Pod——/root/polscope/pod-owned.yaml(Pod probe-owned,标签 owner: platform)、/root/polscope/pod-host.yaml(Pod probe-host,同样的标签,再加上 spec.hostNetwork: true)、/root/polscope/pod-both.yaml(Pod probe-both,标签 owner: platform 和 polscope.io/guard: "on")。三者的容器都是 web/registry.internal/app:1.0。把四个清单用 --dry-run=server 发送到 scope-builtin,以 <파일이름> <낱말> 的形式(占位符依次为文件名与词)在 /root/polscope/07-builtin.txt 中写四行,记录是什么做出了判定——词是 VAP(ValidatingAdmissionPolicy 拒绝)、PSA(PodSecurity 拒绝)、WEBHOOK(Webhook 调用失败)、PASS(通过)之一,对象是 pod.yaml、pod-host.yaml、pod-both.yaml、pod-owned.yaml。
  8. 创建 /root/polscope/blast-radius.sh——遍历集群中所有的 ValidatingWebhookConfiguration,每个 Webhook 一个对象,把由这些对象构成的 JSON 数组输出到标准输出。键恰好有八个:config(配置名称)、webhook(Webhook 名称)、failurePolicy(没有则为 "Fail")、timeoutSeconds(没有则为 10)、resources(该 Webhook 所有 rules[].resources 去重并排序后的数组)、wildcard(resources 中有 "*" 则为真)、excludesKubeSystem(namespaceSelector.matchExpressions 中有键为 kubernetes.io/metadata.name、运算符为 NotIn、值中包含 kube-system 的项,则为真)、risk。risk 的规则是:wildcard 为真、failurePolicy 为 Fail、且 excludesKubeSystem 为假时是 "high",除此之外只要 failurePolicy 为 Fail 就是 "medium",其余为 "low"。数组按 config、webhook 的顺序排序。把输出保存到 /root/polscope/blast-radius.json。然后创建 /root/polscope/risky.sh——只把 risk 为 high 的以 <config>/<webhook> 的形式每行输出一个,只要有一个就以退出码 1 结束,一个都没有则什么都不输出,以 0 结束。两个脚本都必须读取此刻集群中存在的对象,而不是保存的文件。

参考

把一个 Webhook 挂得很宽,所有写入都慢了 2 秒

在 /root/polscope 中工作(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。先创建命名空间 scope-app、scope-ops、scope-builtin,并在这三个命名空间中都用 kubectl create sa default -n <네임스페이스>(占位符为命名空间)亲手创建 default 服务账号(这个集群中没有控制器管理器,不会自动生成)。在 /root/polscope/webhook-wide.yaml 中编写 ValidatingWebhookConfiguration scope-wide-audit——Webhook 名称为 wide.polscope.local,admissionReviewVersions 为 ["v1"],sideEffects 为 None,failurePolicy 为 Ignore,timeoutSeconds 为 2,clientConfig.url 为 https://192.0.2.77:8443/validate。rules 设得最宽——apiGroups: ["*"]、apiVersions: ["*"]、operations: ["CREATE", "UPDATE"]、resources: ["*"]、scope: "*",一个选择器都不设置。再创建三个测试用清单——/root/polscope/pod.yaml(Pod probe-app,容器 web,镜像 registry.internal/app:1.0)、/root/polscope/pod-guard.yaml(Pod probe-guard,标签 polscope.io/guard: "on",同样的容器)、/root/polscope/cm.yaml(ConfigMap probe-cm,数据 a: b)。应用之后,测量四个请求的耗时——kubectl create -n scope-app -f pod.yaml --dry-run=server、kubectl create -n scope-app -f cm.yaml --dry-run=server、kubectl create ns probe-ns --dry-run=server、kubectl get pods -n scope-app。耗时 1 秒以上的记为 SLOW,否则记为 FAST,以 <이름> <낱말> 的形式(占位符依次为名称与词)在 /root/polscope/01-reach.txt 中写下 pod-create、configmap-create、namespace-create、pod-list 四行。

如果 Webhook 的地址是 192.0.2.0/24(TEST-NET-1),连接既不会被拒绝,也不会有响应。API 服务器会等待 timeoutSeconds 这么久,然后转交 failurePolicy,所以设为 Ignore 时请求全部通过,但花费的时间就是该请求处于范围之内的证据。范围之外的请求不用等待就结束了。准入只作用于写入——读取请求根本不会经过 Webhook。时间可以用 s=$(date +%s%N) 与 e=$(date +%s%N) 相减,得到纳秒。kubectl create -f <파일> --dry-run=server(占位符为文件)会让请求照常通过准入,但不留下对象。Webhook 调用和拒绝消息,都与真实请求一样出现。

只把故障模式改成 Fail,该范围的写入就全部停摆了

在 /root/polscope/webhook-strict.yaml 中编写第二个配置 scope-strict——Webhook 名称为 strict.polscope.local,rules 与 scope-wide-audit 一模一样设得最宽,只有另外三处不同:failurePolicy 为 Fail,timeoutSeconds 为 3,clientConfig.url 是连接会被拒绝的地址 https://127.0.0.1:19443/validate。scope-wide-audit 不要删除,保持原样——同一个范围内,只有故障模式不同的两个并排站立,就是本实验的对照组。应用之后,尝试四件事,看是否被拦住,被拦住记为 BLOCK,通过记为 PASS,以 <이름> <낱말> 的形式(占位符依次为名称与词)在 /root/polscope/02-blocked.txt 中写四行——pod-create-scope-app(kubectl create -n scope-app -f pod.yaml --dry-run=server)、configmap-kube-system(kubectl create -n kube-system -f cm.yaml --dry-run=server)、namespace-create(kubectl create ns probe-ns --dry-run=server)、webhookconfig-edit(再执行一次 kubectl apply -f webhook-strict.yaml)。

连接被拒绝的地址会不用等待,立即以失败返回。Fail 把这个失败转为拒绝,Ignore 把它转为通过——同一个范围、同一个死掉的服务器,答案却相反。这就是 failurePolicy 所交换的东西。第四行是这一步的关键。如果某个资源不经过准入 Webhook,那么无论什么被拦住,都至少还能修复它——如果修改 Webhook 配置的请求也要去询问那个 Webhook,就没有人能让集群复活了。不要预测结果,请亲自把这四件事运行一遍。

想用标签排除,却发现连贴标签也被拦住了

先试着用豁免标签脱身——运行 kubectl label ns kube-system polscope.io/admission=exempt --overwrite,把输出连同标准错误一起保存到 /root/polscope/03-lockout.txt(会被拦住)。然后给 /root/polscope/webhook-strict.yaml 中的 scope-strict 加入两个 namespaceSelector.matchExpressions 并重新应用——(1) 键 kubernetes.io/metadata.name,运算符 NotIn,值 ["kube-system", "kube-node-lease", "kube-public"],(2) 键 polscope.io/admission,运算符 NotIn,值 ["exempt"]。现在标签可以贴上了——给 kube-system 和 scope-ops 加上 polscope.io/admission=exempt。最后把 /root/polscope/pod-guard.yaml 用 --dry-run=server 发送到三个命名空间,以 <네임스페이스> <BLOCK|PASS> 的形式(占位符为命名空间)在 /root/polscope/03-escape.txt 中写下 kube-system、scope-ops、scope-app 三行。

这两个条件是做同一件事的两种方法。第一种使用 API 服务器从 1.21 起自动贴在所有命名空间上的 kubernetes.io/metadata.name 标签,所以不需要任何准备;第二种用亲手贴上的标签来管理豁免列表,所以以后增加命名空间时,不必修改 Webhook 配置。可是在已经被拦住的集群中,第二种用不了——贴标签就是命名空间的 UPDATE,而那个请求恰恰被拦住了。所以顺序就定下来了:先用第一种方法开出逃生舱,然后再叠加第二种方法。namespaceSelector 的多个 matchExpressions 必须全部满足才算在范围内。

收窄范围之后,撞上的四个请求中有三个不再经过 Webhook

把 /root/polscope/webhook-strict.yaml 中的 scope-strict 在两个层次上收窄——rules 只针对核心组("")v1 pods 的 CREATE 和 UPDATE,scope 改为 Namespaced,并用 objectSelector.matchLabels 只看带有 polscope.io/guard: "on" 标签的对象。第 3 步的 namespaceSelector 保持原样。应用之后,重新发送四个请求,以 <이름> <좁히기전> <좁힌뒤> 的形式(占位符依次为名称、收窄之前、收窄之后)在 /root/polscope/04-narrow.txt 中写四行(收窄之前的值是在第 2 步看到的)——pod-guard-scope-app(把 pod-guard.yaml 发送到 scope-app)、pod-plain-scope-app(把 pod.yaml 发送到 scope-app)、configmap-scope-app(把 cm.yaml 发送到 scope-app)、namespace-create(kubectl create ns probe-ns --dry-run=server)。判定是 BLOCK 或 PASS。

收窄范围不是降低安全,而是减少询问的次数。在 rules 中被过滤掉的请求,API 服务器连这个 Webhook 都不会想起,namespaceSelector 和 objectSelector 在其次。用通配符接收、再在 Webhook 服务器内部过滤,结果相同,成本却要全部付出。scope 是 Namespaced、Cluster、* 之一,如果写成只看命名空间范围的资源,集群范围资源的请求就根本不会到来。objectSelector 看的是请求中对象的标签,而 namespaceSelector 看的是该对象所在命名空间的标签——是不同的层次。

开一扇只让控制器和值班人员通过的门

给 /root/polscope/webhook-strict.yaml 中的 scope-strict 加入两个 matchConditions 并重新应用——skip-kube-system-sa 让请求者如果是以 system:serviceaccount:kube-system: 开头的服务账号,就不调用 Webhook,skip-break-glass 让请求者的组中如果有 polscope:break-glass,就不调用。应用之后,把同一个 /root/polscope/pod-guard.yaml 以三种主体发送到 scope-app,以 <이름> <BLOCK|PASS> 的形式(占位符为名称)在 /root/polscope/05-conditions.txt 中写下 kube-system-sa、break-glass、normal 三行——kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n scope-app -f pod-guard.yaml --dry-run=server、kubectl --as=oncall --as-group=polscope:break-glass --as-group=system:masters create -n scope-app -f pod-guard.yaml --dry-run=server,以及用当前账号原样发送一次。

范围(rules、选择器)与条件(matchConditions)是不同的层次。选择器只看请求是针对什么的,而条件用 CEL 查看整个请求——是谁发送的(request.userInfo)、什么动词、乃至哪个命名空间。不过条件是在选择器之后评估的,所以如果把本可以用选择器过滤的内容推给条件,只会增加成本。条件全部为真时才会调用 Webhook,所以“想要跳过”要写成否定形式。可以用 --as 假装是其他主体,但只会得到被模拟主体的权限,所以要通过授权,必须同时给出 --as-group=system:masters(授权在准入之前)。如果连控制器发出的请求也拦住,集群就无法自我恢复,而如果没有值班人员专用的门,出现故障时除了删除 Webhook 配置,就没有别的路了。

把超时设为 6 秒,故障就随着每个请求延长了 6 秒

在 /root/polscope/webhook-slow.yaml 中编写第三个配置 scope-slow——Webhook 名称 slow.polscope.local,clientConfig.url 是完全没有响应的地址 https://192.0.2.77:8443/validate,sideEffects 为 None,namespaceSelector.matchExpressions 的键 kubernetes.io/metadata.name 为 In ["scope-ops"],objectSelector.matchLabels 为 polscope.io/slow: "on",rules 为核心组 v1 pods 的 CREATE 和 UPDATE(scope: "Namespaced"),最终状态为 failurePolicy: Fail 和 timeoutSeconds: 3。在 /root/polscope/pod-slow.yaml 中创建 Pod probe-slow(标签 polscope.io/slow: "on",容器 web,镜像 registry.internal/app:1.0)。然后创建 /root/polscope/timeout-probe.sh <Fail|Ignore> <초>(占位符为秒)——把 scope-slow 暂时改为这两个值,用 --dry-run=server 把 pod-slow.yaml 向 scope-ops 发送一次并测量耗时,务必恢复为原来的值之后,只输出一行 BLOCK <초> 或 PASS <초>(秒为四舍五入后的整数)。用这个脚本测量三次,以 <failurePolicy> <타임아웃> <BLOCK|PASS> <걸린초> 的形式(占位符依次为 failurePolicy、超时、耗时秒数)在 /root/polscope/06-timeout.txt 中写三行——分别是 Fail 6、Ignore 6、Fail 4 各一次。

timeoutSeconds 是“比 Webhook 服务器正常时花费的时间稍长一点”来确定的值。设得宽裕,发生故障时 API 服务器就会在每个请求上被拖住那么久;设得短,缓慢的响应会被当作失败,转交给 failurePolicy 处理。所以这两个旋钮不是分别选择的值——Fail 下超时越长,故障就越长;Ignore 下超时越长,虽然能通过,却会变慢。哪一边都不是免费的。脚本中要用 trap ... EXIT 设置恢复。即使中途死掉,Webhook 也不能留着奇怪的值。修改配置之后,反映会晚一两次往返,所以测量之前先稍等片刻。这个集群中第 1 步的 2 秒审计 Webhook 仍然存活,而 Webhook 是并行调用的,所以耗时会以两者中较大的那个为准。

在 Webhook 全部死掉的集群中,只有内置策略仍在给出判定

就在现在这个没有 Webhook 服务器的状态下,在同一个位置建立内置策略进行比较。给命名空间 scope-builtin 加上标签 pod-security.kubernetes.io/enforce=baseline。在 /root/polscope/vap-owner.yaml 中一并编写 ValidatingAdmissionPolicy polscope-require-owner(匹配核心组 v1 pods 的 CREATE 和 UPDATE,要求 Pod 元数据中有 owner 标签)和同名的 ValidatingAdmissionPolicyBinding(validationActions 为 ["Deny"],matchResources.namespaceSelector 为 kubernetes.io/metadata.name In ["scope-builtin"])并应用。再创建三个测试用 Pod——/root/polscope/pod-owned.yaml(Pod probe-owned,标签 owner: platform)、/root/polscope/pod-host.yaml(Pod probe-host,同样的标签,再加上 spec.hostNetwork: true)、/root/polscope/pod-both.yaml(Pod probe-both,标签 owner: platform 和 polscope.io/guard: "on")。三者的容器都是 web/registry.internal/app:1.0。把四个清单用 --dry-run=server 发送到 scope-builtin,以 <파일이름> <낱말> 的形式(占位符依次为文件名与词)在 /root/polscope/07-builtin.txt 中写四行,记录是什么做出了判定——词是 VAP(ValidatingAdmissionPolicy 拒绝)、PSA(PodSecurity 拒绝)、WEBHOOK(Webhook 调用失败)、PASS(通过)之一,对象是 pod.yaml、pod-host.yaml、pod-both.yaml、pod-owned.yaml。

VAP 在 API 服务器进程内部评估 CEL,PSA 则是内置的准入插件。两者都没有网络往返,没有证书,也没有引擎 Pod,所以即使 Webhook 全部死掉的现在,也与平时一样给出判定。故障只会以“表达式求值错误”或“贴错了标签”这类局部的形式出现。所以实际工作中的布局是:不需要引擎就能表达的验证,下沉给内置机制以缩小故障面,只把确实必要的留在 Webhook 上。判定顺序也请留意——内置准入排在前面,所以已经被内置策略拒绝的请求,根本到不了 Webhook。因此,要看到 Webhook 的故障,就必须发送能通过所有内置策略的 Pod。baseline 级别拦截什么,拒绝消息会直接告诉你。

用一张表回答“这个一旦死掉,什么会停摆”

创建 /root/polscope/blast-radius.sh——遍历集群中所有的 ValidatingWebhookConfiguration,每个 Webhook 一个对象,把由这些对象构成的 JSON 数组输出到标准输出。键恰好有八个:config(配置名称)、webhook(Webhook 名称)、failurePolicy(没有则为 "Fail")、timeoutSeconds(没有则为 10)、resources(该 Webhook 所有 rules[].resources 去重并排序后的数组)、wildcard(resources 中有 "*" 则为真)、excludesKubeSystem(namespaceSelector.matchExpressions 中有键为 kubernetes.io/metadata.name、运算符为 NotIn、值中包含 kube-system 的项,则为真)、risk。risk 的规则是:wildcard 为真、failurePolicy 为 Fail、且 excludesKubeSystem 为假时是 "high",除此之外只要 failurePolicy 为 Fail 就是 "medium",其余为 "low"。数组按 config、webhook 的顺序排序。把输出保存到 /root/polscope/blast-radius.json。然后创建 /root/polscope/risky.sh——只把 risk 为 high 的以 <config>/<webhook> 的形式每行输出一个,只要有一个就以退出码 1 结束,一个都没有则什么都不输出,以 0 结束。两个脚本都必须读取此刻集群中存在的对象,而不是保存的文件。

这份报告回答的问题只有一个——这个 Webhook 服务器一旦死掉,集群中什么会停摆。如果 failurePolicy: Fail,该范围的写入就会停止,而这个范围就是 resources 和选择器。所以危险的组合总是一样的:范围宽 + Fail + 没有排除系统命名空间。三者同时满足,连想要复活引擎的请求也会被拦住,集群就无法自行恢复。用 jq 展开 kubectl get validatingwebhookconfigurations -o json 的 .items[].webhooks[],一次就能做出来。jq 的 // 在值为 false 时也会漏到右边,所以默认值要先用 has("키")(其中的韩文表示“键”)询问是否存在,再填充。评分器会暂时建立一个危险的配置,再次调用 risky.sh——事先背好的名称答案,会在那时露馅。