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

CKS — Kubernetes 安全专家

亲眼看安全配置真的挡住了什么

在 TT Lab 中继续学习

本实验在真正强制执行安全策略的集群中运行

VM 中运行着真正的 k3s。由于 kubelet 和 containerd 确实在运行,所以违反 securityContext 的 Pod 会真的被拒绝,只读根文件系统也会真的阻止写入。

CKS 课程中的其他实验运行在 kwok 之上。在那里,违规的 Pod 也会直接变成 Running,虽然能练习如何编写配置,却无法练习确认这些配置真的被遵守。

首次启动大约需要 2 分钟。

目标

区分安全控制措施在何时、何处拦截。在准入阶段被拦截与在运行时被拦截,症状不同,修复的地方也不同。

为什么重要

实际工作中出事故的地方,并不是“把配置写错了”,而是“把配置都写好了,却没有生效”。后者悄无声息,所以更危险。

而且同样是“Pod 起不来”,原因也分散在好几层。

在哪里被拦截 症状 修复位置
准入(创建时) kubectl apply 本身就被拒绝 命名空间标签、策略
kubelet(创建容器时) Pod 被创建出来,但出现 CreateContainerConfigError securityContext
运行时(进程内部) Pod 是 Running,只是应用失败 卷、capability

如果分不清这三层,每次遇到故障都得从头排查一遍。

步骤

  1. 用设置了 runAsNonRoot: true 的 Pod 启动一个以 root 运行的镜像。把拒绝原因写入 /root/cks/nonroot.txt,并同时启动一个正确的 Pod(good-nonroot)。违规 Pod 的名称是 bad-root。
  2. 给 hardened Pod 设置 readOnlyRootFilesystem: true,确认写入确实被阻止,并把结果写入 /root/cks/readonly.txt。还必须同时提供写临时文件的位置。
  3. 在同一个 Pod 上把 capability 全部 drop 为 ALL,并设置 allowPrivilegeEscalation: false,然后把其效果写入 /root/cks/caps.txt。
  4. 在 locked 命名空间上,把 Pod Security Admission 设为 restricted,并把特权 Pod 会在创建时被拒绝这一点写入 /root/cks/psa.txt。
  5. 在 locked 中创建 app-sa ServiceAccount 和 app-role,只允许读取 configmap。结果写入 /root/cks/rbac.txt。
  6. 创建 app-secret 并挂载到 Pod 中,确认该文件在 Pod 内是什么样子,并写入 /root/cks/secret.txt。
  7. 在 /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 应用的证明。
  8. 在 /root/cks/report.md 中写下 nonroot_reject_reason=、psa_level=、rbac_denied= 三行,并整理出在何时、何处被拦截。

参考

这是 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、运行时这三层。