亲眼看安全配置真的挡住了什么
本实验在真正强制执行安全策略的集群中运行
VM 中运行着真正的 k3s。由于 kubelet 和 containerd 确实在运行,所以违反 securityContext 的 Pod 会真的被拒绝,只读根文件系统也会真的阻止写入。
CKS 课程中的其他实验运行在 kwok 之上。在那里,违规的 Pod 也会直接变成 Running,虽然能练习如何编写配置,却无法练习确认这些配置真的被遵守。
首次启动大约需要 2 分钟。
目标
区分安全控制措施在何时、何处拦截。在准入阶段被拦截与在运行时被拦截,症状不同,修复的地方也不同。
为什么重要
实际工作中出事故的地方,并不是“把配置写错了”,而是“把配置都写好了,却没有生效”。后者悄无声息,所以更危险。
而且同样是“Pod 起不来”,原因也分散在好几层。
| 在哪里被拦截 | 症状 | 修复位置 |
|---|---|---|
| 准入(创建时) | kubectl apply 本身就被拒绝 |
命名空间标签、策略 |
| kubelet(创建容器时) | Pod 被创建出来,但出现 CreateContainerConfigError |
securityContext |
| 运行时(进程内部) | Pod 是 Running,只是应用失败 | 卷、capability |
如果分不清这三层,每次遇到故障都得从头排查一遍。
步骤
- 用设置了
runAsNonRoot: true的 Pod 启动一个以 root 运行的镜像。把拒绝原因写入/root/cks/nonroot.txt,并同时启动一个正确的 Pod(good-nonroot)。违规 Pod 的名称是bad-root。 - 给
hardenedPod 设置readOnlyRootFilesystem: true,确认写入确实被阻止,并把结果写入/root/cks/readonly.txt。还必须同时提供写临时文件的位置。 - 在同一个 Pod 上把 capability 全部 drop 为 ALL,并设置
allowPrivilegeEscalation: false,然后把其效果写入/root/cks/caps.txt。 - 在
locked命名空间上,把 Pod Security Admission 设为restricted,并把特权 Pod 会在创建时被拒绝这一点写入/root/cks/psa.txt。 - 在
locked中创建app-saServiceAccount 和app-role,只允许读取 configmap。结果写入/root/cks/rbac.txt。 - 创建
app-secret并挂载到 Pod 中,确认该文件在 Pod 内是什么样子,并写入/root/cks/secret.txt。 - 在
/root/cks/audit-policy.yaml中编写审计策略。第一条规则要对核心 API 组的secrets、configmaps、serviceaccounts/token,不限用户、动词和命名空间,以Metadata级别加以保护。接下来只把/healthz、/readyz、/livez以及各自的/*子路径设为None,最后一条是无条件的Metadata。omitStages只能省略RequestReceived。总共使用 3 条规则,或者在默认规则之前再加上 RBAC 写入详细规则,共 4 条规则。可选的详细规则只针对rbac.authorization.k8s.io的 roles、rolebindings、clusterroles、clusterrolebindings 上的 create、update、patch、delete、deletecollection,以RequestResponse级别记录。在/root/cks/audit.txt中说明级别、首次匹配、正文保护和日志保留的原因。这一步是编写策略,并不是 API 应用的证明。 - 在
/root/cks/report.md中写下nonroot_reject_reason=、psa_level=、rbac_denied=三行,并整理出在何时、何处被拦截。
参考
- 违规 Pod 的状态用
kubectl get pod bad-root -o jsonpath='{.status.containerStatuses[0].state}'查看。 - PSA 标签是
pod-security.kubernetes.io/enforce=restricted。audit和warn也可以单独设置,实际工作中会先设置 warn,看看有哪些会被拦截,然后再提升到 enforce。 - 权限检查使用
kubectl auth can-i <동사> <자원> --as=system:serviceaccount:<ns>:<sa> -n <ns>(占位符依次为动词、资源、命名空间与 ServiceAccount 名称)。 - 使用只读根文件系统时,需要在
/tmp之类的位置挂载emptyDir。否则大多数需要写临时文件的程序都会崩溃。 - 常见错误 1:只设置
runAsNonRoot: true而不设置runAsUser。如果镜像以 root 运行就会被拒绝,但这一点只有查看镜像才能知道,所以容易让人困惑。 - 常见错误 2:直接把 PSA 设为
enforce。已经在运行的工作负载会在下一次重新部署时全部被拦截。请先用warn试探。
这是 70 分钟的实验,请在到期前通过+时间延长(最长 180 分钟)。会话结束后,工作成果会消失。
以 root 运行就根本起不来
用设置了 runAsNonRoot: true 的 Pod 启动一个以 root 运行的镜像。把拒绝原因写入 /root/cks/nonroot.txt,并同时启动一个正确的 Pod(good-nonroot)。违规 Pod 的名称是 bad-root。
runAsNonRoot: true 是 kubelet 在创建容器之前,根据镜像的 USER 来判断的。要点在于拦截的是 kubelet,而不是准入。
把根文件系统设为只读
给 hardened Pod 设置 readOnlyRootFilesystem: true,确认写入确实被阻止,并把结果写入 /root/cks/readonly.txt。还必须同时提供写临时文件的位置。
只设置 readOnlyRootFilesystem: true 的话,需要写临时文件的程序会崩溃。请同时挂载 emptyDir。
把 capability 全部去掉
在同一个 Pod 上把 capability 全部 drop 为 ALL,并设置 allowPrivilegeEscalation: false,然后把其效果写入 /root/cks/caps.txt。
在 drop: ["ALL"] 之后,只用 add 把真正需要的重新加回来。大多数应用什么都不需要。
在创建时拦截
在 locked 命名空间上,把 Pod Security Admission 设为 restricted,并把特权 Pod 会在创建时被拒绝这一点写入 /root/cks/psa.txt。
给命名空间加上 pod-security.kubernetes.io/enforce=restricted 标签。这属于准入,所以 kubectl apply 本身就会被拒绝。
只授予需要的权限
在 locked 中创建 app-sa ServiceAccount 和 app-role,只允许读取 configmap。结果写入 /root/cks/rbac.txt。
请用 kubectl auth can-i --as=system:serviceaccount:<ns>:<sa> 同时确认允许和拒绝两种结果。只看放开的部分,只完成了一半。
Secret 在 Pod 内是什么样子
创建 app-secret 并挂载到 Pod 中,确认该文件在 Pod 内是什么样子,并写入 /root/cks/secret.txt。
以卷的方式挂载之后,用 ls -la 查看。要点是 ..data 符号链接的结构以及存储介质。
选择要留下什么
在 /root/cks/audit-policy.yaml 中编写审计策略。第一条规则要对核心 API 组的 secrets、configmaps、serviceaccounts/token,不限用户、动词和命名空间,以 Metadata 级别加以保护。接下来只把 /healthz、/readyz、/livez 以及各自的 /* 子路径设为 None,最后一条是无条件的 Metadata。omitStages 只能省略 RequestReceived。总共使用 3 条规则,或者在默认规则之前再加上 RBAC 写入详细规则,共 4 条规则。可选的详细规则只针对 rbac.authorization.k8s.io 的 roles、rolebindings、clusterroles、clusterrolebindings 上的 create、update、patch、delete、deletecollection,以 RequestResponse 级别记录。在 /root/cks/audit.txt 中说明级别、首次匹配、正文保护和日志保留的原因。这一步是编写策略,并不是 API 应用的证明。
保护规则必须先匹配。omitStages 并不是删除正文。请区分编写策略与真正收集已完成事件。
在何时、何处被拦截
在 /root/cks/report.md 中写下 nonroot_reject_reason=、psa_level=、rbac_denied= 三行,并整理出在何时、何处被拦截。
写下 nonroot_reject_reason=、psa_level=、rbac_denied= 三行,并整理准入、kubelet、运行时这三层。