收窄 RBAC 并切断令牌
目标
亲自在集群中找出权限过大的角色,用最小权限 Role 取代它们,再通过询问 API 服务器验证结果。同时关闭 ServiceAccount 令牌自动进入 Pod 的所有路径。
为什么重要
RBAC 决定“谁能做什么”,但人类要阅读并验证它却非常困难。只要一个 ClusterRole 中混入 resources: ["*"],该角色就会连未来新增的 CRD 也全部涵盖,而可能无人察觉。因此,加固工作的真正起点不是“写好策略”,而是“列出当前究竟允许了什么”。
第二条主线是凭据。Pod 默认会获得令牌,即使应用完全不使用 API 也会获得。一旦某个 Pod 被攻破,攻击者就等于立即得到一份集群凭据。automountServiceAccountToken: false 虽然只有一行,却能大幅缩小入侵后的横向扩散范围。
步骤
- 创建命名空间
cks-rbac,并在其中创建 ServiceAccountreport-sa。 - 在集群所有 ClusterRole 中,筛选
rules[].verbs包含*的角色名称, 按字典序排序后逐行保存到/root/cks-rbac-hardening/wildcard-roles.txt。 - 创建 ClusterRole
cks-pod-reader。apiGroups 只有核心组(空字符串), resources 为pods和pods/log,verbs 为get、list、watch。 任何字段都不得包含*。 - 在集群所有 ClusterRole 中,筛选 verbs 至少包含
escalate、bind、impersonate之一的 角色名称,排序后保存到/root/cks-rbac-hardening/danger-verb-roles.txt。 - 在
cks-rbac命名空间中,通过 RoleBindingreport-sa-pod-reader为report-sa绑定 ClusterRolecks-pod-reader。然后把以下两个问题的答案(yes或no) 按顺序分两行保存到/root/cks-rbac-hardening/can-i.txt。 第一行:report-sa能否在cks-rbac中 list pods; 第二行:report-sa能否在cks-rbac中 get secrets。 - 在
cks-rbac中创建 Podreport(镜像nginx:1.27-alpine)。serviceAccountName为report-sa,Pod 规范的automountServiceAccountToken为false。 - 在
cks-rbac中创建 Secretreport-sa-token。类型为kubernetes.io/service-account-token,注解kubernetes.io/service-account.name的值为report-sa。再创建 Podtoken-app(镜像nginx:1.27-alpine,serviceAccountName 为report-sa),但通过 projected 卷获取令牌。卷名称为api-token,serviceAccountToken源的audience为api,expirationSeconds为3600,path为token。 - 为
cks-rbac中的defaultServiceAccount 设置automountServiceAccountToken: false, 并将 default SA 无法在该命名空间中 list pods 的结果(no) 单独一行保存到/root/cks-rbac-hardening/default-sa-can-i.txt。
参考
- 通配符审计示例:
kubectl get clusterrole -o json | jq -r '.items[] | select(...) | .metadata.name' | sort kubectl auth can-i list pods -n cks-rbac --as=system:serviceaccount:cks-rbac:report-sakubectl create clusterrole cks-pod-reader --verb=get,list,watch --resource=pods,pods/log- 常见错误 1:把
automountServiceAccountToken放入容器规范。它应直接位于 Pod 规范下。 - 常见错误 2:RoleBinding 的 subjects 中遗漏 ServiceAccount 的
namespace,导致绑定完全无效。
命名空间与专用 ServiceAccount
创建命名空间 cks-rbac,并在其中创建 ServiceAccount report-sa。
使用 kubectl create serviceaccount 创建。如果没有先创建命名空间,SA 创建会失败。
找出使用通配符的 ClusterRole
在集群所有 ClusterRole 中,筛选 rules[].verbs 包含 * 的角色名称,
按字典序排序后逐行保存到 /root/cks-rbac-hardening/wildcard-roles.txt。
使用 jq 遍历 kubectl get clusterrole -o json,筛选 rules 中 verbs 包含 * 的对象。结果只保存名称,每行一个。
编写收窄权限的 ClusterRole
创建 ClusterRole cks-pod-reader。apiGroups 只有核心组(空字符串),
resources 为 pods 和 pods/log,verbs 为 get、list、watch。
任何字段都不得包含 *。
可以使用 kubectl create clusterrole --resource= --verb= 生成草稿。apiGroups、resources、verbs 中都不要使用通配符。
筛选包含危险动词的角色
在集群所有 ClusterRole 中,筛选 verbs 至少包含 escalate、bind、impersonate 之一的
角色名称,排序后保存到 /root/cks-rbac-hardening/danger-verb-roles.txt。
目标是包含 escalate、bind、impersonate 三个动词中任意一个的 ClusterRole。使用与第 02 步相同的方法,只保存排序后的名称。
使用 auth can-i 验证结果
在 cks-rbac 命名空间中,通过 RoleBinding report-sa-pod-reader 将 report-sa 绑定到
ClusterRole cks-pod-reader。然后把以下两个问题的答案(yes 或 no)
按顺序分两行保存到 /root/cks-rbac-hardening/can-i.txt。
第一行:report-sa 能否在 cks-rbac 中 list pods;
第二行:report-sa 能否在 cks-rbac 中 get secrets。
使用 --as=system:serviceaccount:<네임스페이스>:<이름> 格式模拟主体。输出只有一行 yes 或 no,可直接汇总到文件。
关闭 Pod 的令牌自动挂载
在 cks-rbac 中创建 Pod report(镜像 nginx:1.27-alpine)。serviceAccountName 为
report-sa,Pod 规范的 automountServiceAccountToken 为 false。
automountServiceAccountToken 是 Pod 规范顶层字段(直接位于 spec 下),不在容器内部。
手动令牌 Secret 与 projected 令牌
在 cks-rbac 中创建 Secret report-sa-token。类型为
kubernetes.io/service-account-token,注解 kubernetes.io/service-account.name 的值为
report-sa。再创建 Pod token-app(镜像 nginx:1.27-alpine,serviceAccountName
为 report-sa),但通过 projected 卷获取令牌。卷名称为 api-token,
serviceAccountToken 源的 audience 为 api,expirationSeconds 为 3600,
path 为 token。
手动令牌 Secret 的类型是 kubernetes.io/service-account-token,并通过 kubernetes.io/service-account.name 注解指定所有者。另一侧则在 Pod 的 projected 卷中使用 serviceAccountToken 源,并设置 audience 和过期时间。
禁用 default ServiceAccount
为 cks-rbac 中的 default ServiceAccount 设置 automountServiceAccountToken: false,
并将 default SA 无法在该命名空间中 list pods 的结果(no)
单独一行保存到 /root/cks-rbac-hardening/default-sa-can-i.txt。
使用 kubectl patch serviceaccount default -n <네임스페이스> -p '...' 一行即可完成。然后通过 can-i 确认 default SA 确实没有任何权限。