让开发者自己改 deployment,但不能碰副本数和资源
英文版:English
起因
集群访问原来分三种:admin、只读,以及一个"developer"角色,也就是只读再加删 pod、删 job 的权限。结果除了重启,任何改动都得找 infra,哪怕只是一个环境变量、一个镜像 tag、一处配置。开发者想自己动手,infra 又不想发一个顺手就能把副本数翻倍、把内存 request 提三倍的角色。一个团队的改动变成整个集群的节点压力和成本,走的就是这两个字段。
所以需求很具体:对工作负载有完整写权限,唯独 spec.replicas 和 containers[].resources 不能改。
访问链路上的三层
动 YAML 之前,我先把访问链路写了下来。角色变更如果只落在其中一层,等于没生效。所有权限系统回答的都是同一个表达式 Allow(主体, 动作, 资源, 上下文),区别只在每一部分由谁执行。我们这里有三个执行点:
| 层 | 职责 | 新角色在这层需要什么 |
|---|---|---|
| 审批平台 | 决定给不给钥匙 | 角色选项、审批策略、映射 |
| 堡垒机 | 保管钥匙并记录使用 | 一个绑定 ServiceAccount token 的账号 |
| Kubernetes | 决定钥匙能开哪些门 | ClusterRole + ServiceAccount + binding |
这个项目做的是第三层,另外在门里面加了一把更细的锁。
RBAC 管不到字段
RBAC 的词汇是"资源 × 动作"。patch deployments 是一条授权,覆盖 Deployment 的所有字段,env、image、replicas、resources 一视同仁。RBAC 里写不出"可以 patch 但不能碰某个字段"这样的规则,角色拆得再细也没用。
字段级的控制要放到请求路径的下一步,也就是 admission。API server 先认证,再按 RBAC 授权,持久化之前把对象交给 admission webhook。于是设计分成两层:
- RBAC 给
developer-edit角色对 pod、deployment、statefulset、daemonset、service、configmap、job、ingress、PVC 的完整 CRUD,集群级资源只允许读 namespace。 - 一条 Kyverno
ClusterPolicy(validating admission webhook):来自 developer ServiceAccount 的UPDATE请求,只要spec.replicas和旧对象不同,或者任一容器的resources和旧对象里同名容器不同,就拒绝。
1 | - name: deny-replicas-change |
策略能不能真正守住,看三个细节:
kubectl scale写的是deployments/scale子资源,和 Deployment 本身是两种 kind。只 matchDeployment的策略 review 时看着没问题,却会被最常见的改副本方式绕过去,所以规则两种 kind 都要 match。- resources 要按容器名逐个比较,两边都带 null 默认值。整体比较容器列表,会把无害的顺序变化判成违规;不加 null 默认值,"没设 resources"会直接求值报错,得不出判定。
- RBAC 在 admission 之前执行,所以测试要先证明 RBAC。角色本身如果不能
patch,请求在 Kyverno 看到之前就被拒了,验收会显示"改副本被拒",原因却是错的。每次推广的第一步都是kubectl auth can-i patch deployment --as=<sa>,这一步返回 yes,后面的策略测试才算数。
分阶段推广
一条会拒绝写入的策略,哪怕只针对开发者,也是生产变更,所以按风险变更的流程推:
- Audit:策略把违规写进 policy report,不拦任何请求。
- 验收:用一个一次性的测试 deployment,绝不指向真实业务。加环境变量必须成功;改 replicas、改 resources、
scale三项都必须被记录为违规。 - Enforce:切
validationFailureAction,重跑同一组用例。env 仍然成功,另外三项被拒并返回策略消息。 - 回滚:一次 patch 切回
Audit,保留记录、停止拦截。删除策略是另外单独的一步。
每一步都幂等(kubectl apply、helm upgrade --install),推到一半的集群可以从头重跑。集群一个一个推,先只上 RBAC,再装 Kyverno 和 Audit 策略,最后 Enforce,任何一项验收失败就停在当前集群。推广前先摸了五个集群的底:一个已经齐全;三个要从零建 ServiceAccount、角色、binding 并安装 Kyverno;PCI 环境那个本来就跑着 Kyverno,只需叠加 RBAC 对象和策略。五个集群最后全部切到了 Enforce。
还没解决的
最大的缺口在归因。开发者经堡垒机、用一个共享的 ServiceAccount token 访问集群,所以 Kubernetes 审计日志里记的是 serviceaccount:developer-edit,看不到具体的人,谁做了什么只存在堡垒机的会话日志里。作为第一步可以接受,作为终态不行。升级路线是按人的身份访问,堡垒机侧做 impersonation 或者上 OIDC,让 API server 自己的审计轨迹里出现具体的人。
另外,这条策略是两个字段的黑名单。如果以后要收得更紧,就反过来列白名单:开发者只能改 env、args、image、annotation,其余一律拒绝。
回头看,几件事比较值得记下来。RBAC 只回答"哪个资源、哪个动作",哪个字段、什么值要交给 admission,靠拆角色拿字段级控制是找错了地方。策略有多强,取决于它最窄的那个 match,/scale 这类子资源正是看着没问题的规则漏掉的地方。各层要按请求顺序测,对一个 RBAC 已经拒绝的角色跑 admission 测试,会因为错误的原因通过。最后,先 Audit 再 Enforce,并且留一行命令的退路:分阶段推广多花一天,策略拦下一次真实发布就是一次事故。
脱敏后的策略、RBAC 模板和分阶段推广脚本在 github.com/shaorui0/k8s-developer-guardrails。