我们团队去年接手一个运行了三年的老集群,版本从1.24一路升到1.28,结果升级后CI/CD的流水线全挂了。排查下来发现是之前有人图省事,给Jenkins的ServiceAccount绑了一个cluster-admin权限,新版本里kubelet的默认安全策略收紧,直接把这个过度授权的账户给拦截了。这事让我意识到,很多人对RBAC的理解还停留在“能跑就行”的阶段,根本没细想过权限边界在哪。
Kubernetes 1.30(2024年4月刚发布)里,RBAC虽然早就稳定了(从1.8开始),但迭代出来的细节特别多。我现在的习惯是,新集群搭好第一件事就是关掉默认的宽松策略。RBAC的核心其实就四个东西:Role、ClusterRole、RoleBinding、ClusterRoleBinding。但别被名字骗了,Role和ClusterRole定义的是“能做什么”,而Binding是把这个“能做什么”贴到具体的人或账户上。
我们那个订单系统之前出过一个问题:有个开发同学写了个脚本,想读取kube-system命名空间下的ConfigMap来调试网络,结果直接用kubectl get cm -n kube-system就报权限不足。他跑来问我是不是集群坏了,我给他看了这段Role定义:
这个Role虽然定义了读ConfigMap的权限,但它的namespace是default。Role天生就被锁死在单个命名空间里,你绑到default的账户,哪怕有configmap的get权限,也只能在default里读,跨到kube-system就失效。这就是Role和ClusterRole的核心差异——ClusterRole没有命名空间限制,比如我想让某个账户能读所有命名空间的Pod日志(子资源控制,1.30里这块更细了),就得用ClusterRole:
然后绑定的时候,如果我想让这个权限只在team-a命名空间生效,就用RoleBinding去引用这个ClusterRole:
这样ci-builder这个ServiceAccount在team-a里能读Pod和日志,但跑到team-b就不行。我之前见过有人直接把ClusterRole用ClusterRoleBinding绑到全局,说“省得每个命名空间都配”,结果后来有个漏洞让攻击者拿到了这个ServiceAccount的Token,直接把整个集群的Secret都列出来了。
1.30版本里还有个细节我挺喜欢:系统组件比如kubelet的默认权限更收敛了。以前kubelet默认能读所有Node的资源,现在默认只给它需要的最小权限。我们之前有个边缘节点跑K3s,升级后kubelet起不来,查审计日志发现是它试图访问一个已经被移除的API资源,权限被拒。后来给kubelet的ClusterRole里补了对应的resources才解决。这也印证了现在K8s的安全趋势:默认最小权限,你要什么自己显式申请,别指望系统给你开绿灯。
前年我们给一个日活120万的电商做集群重构,之前他们所有团队都挤在一个default命名空间里,结果大促时订单服务的Pod被推荐系统的同学误删了,导致下单接口挂了15分钟。后来我们决定按团队拆分命名空间,用RBAC做隔离,现在这个架构跑了快两年,没再出过越权的问题。
我们当时有五个核心团队:订单、商品、支付、推荐、基础架构。每个团队一个主命名空间,比如team-order、team-product,再加一个共享的infra命名空间放监控和日志组件。隔离的核心需求是:订单团队只能操作team-order里的资源,不能碰team-product的Pod;基础架构团队能看所有命名空间的监控数据,但不能改业务团队的Deployment。
我给订单团队设计的Role是这样的,专门限制他们只能管自己命名空间里的核心资源:
这里特意把pods/log和pods/exec单独列出来,因为1.30版本支持子资源精细化控制。之前有个开发想进Pod调试,我们没给pods/exec权限,他直接用kubectl exec就报权限不足,后来单独给他开了这个子资源的权限,而不是给整个Pod的create权限,避免他乱删Pod。
然后给订单团队的ServiceAccount绑定这个Role:
这里我用了Group来绑定团队成员,因为我们有OIDC对接企业微信,开发同学登录时带的组信息就是order-team,不用给每个人单独绑。CI/CD的ServiceAccountorder-ci也绑进来,这样流水线部署时只能操作team-order的资源。
基础架构团队需要跨命名空间看监控,就用ClusterRole:
然后用ClusterRoleBinding绑到基础架构的组:
这样基础架构的同学能看所有Pod的监控数据,但不能改任何团队的Deployment。有一次推荐团队的同学想删team-product里的Pod,用自己的账户执行kubectl delete pod xxx -n team-product,直接被拒,因为他的Role只在team-recommend命名空间生效。
我们还用kubectl-who-can工具定期扫权限冗余,比如发现有个离职同学的账户还绑着team-order的RoleBinding,直接删掉。现在这个集群跑了18个命名空间,QPS峰值到过8000,权限相关的故障为零。如果当初没做隔离,现在估计每天都有团队误删别人的资源。
上个月我们生产集群出了个挺悬的事:有个攻击者通过某个应用的未授权接口拿到了ServiceAccount的Token,然后试图列集群里所有的Secret。幸好我们后来加了OPA策略,但排查过程中发现根本问题是一个ClusterRole被滥用了。
事情是这样的:我们有一个日志收集组件Fluentd,需要读所有命名空间的Pod日志来采集。之前负责这块的同学图省事,直接写了一个ClusterRole叫fluentd-reader,然后给所有需要读日志的ServiceAccount都用ClusterRoleBinding绑到这个角色。结果有个业务的ServiceAccountorder-sa也被绑进去了,攻击者拿到order-sa的Token后,就能通过kubectl get secrets --all-namespaces列所有Secret,因为fluentd-reader里不小心加了secrets的get权限。
我排查时先看审计日志,发现有个来自order-sa的list请求,目标资源是secrets,API路径是/api/v1/secrets。然后我用kubectl describe clusterrolebinding fluentd-binding看绑定关系,发现subjects里确实有order-sa:
然后看fluentd-reader的定义,问题找到了:
Fluentd根本不需要读Secret,之前写规则的人可能觉得“多给点权限没事”,结果埋了雷。我当时的解决步骤是:先把order-sa从fluentd-binding里移除,然后修改fluentd-reader,删掉secrets的权限:
然后给Fluentd单独建了一个ClusterRole用于读Secret(其实它不需要,后来发现是之前配置错误),再给业务的ServiceAccount单独建Role,只给它们需要的权限。比如order-sa只需要读team-order里的ConfigMap,就给它绑一个Role:
这次事故后我们定了个规矩:ClusterRole除非必要(比如需要跨命名空间或访问集群级资源如Node),否则一律用Role;所有ClusterRoleBinding必须经过两人审批,而且每周用kubescape扫一次权限风险,它会直接标出来哪些ServiceAccount有过度授权。
现在回想起来,如果当初没及时发现,攻击者拿到Secret后就能拿到数据库的密码,后果不堪设想。很多人觉得RBAC配置麻烦,但比起生产被攻破,这点配置时间真的不算什么。我现在给团队培训时总说,权限就像门锁,你嫌麻烦不锁,丢东西只是迟早的事。
去年我们团队接手了一个运行了三年的老集群,版本还是 Kubernetes 1.24,里面跑着四十多个微服务和一堆遗留的 CI/CD 任务。我接手第一件事就是去梳理权限,结果打开 kubectl get rolebindings -A 的输出,直接给我看懵了。一个叫 jenkins-deploy 的 ServiceAccount 居然绑了一个 ClusterRole,权限是 *.*,也就是能操作集群里任何资源。我后来问之前的运维,他说当时是为了赶上线,图省事直接给的。这种场景其实特别典型,也是我为什么后来坚决要引入混合策略的原因。
单纯聊理论没意思,我们直接看这三种方案在我们真实场景下的表现。
RBAC 的局限性
Kubernetes 自 1.8 版本就稳定了 RBAC,到现在的 1.30(2024年4月刚发),它依然是权限的基石。它的好处是简单、直接,和 K8s 原生集成好。比如我们给订单团队划分命名空间,限制他们只能看 order-prod 里的 Pod,RBAC 几行 YAML 就搞定。
但问题来了,RBAC 是“基于属性”的吗?不是,它是基于“角色”的。它没法处理动态条件。比如我有个需求:只允许在早上 9 点到下午 6 点之间,从公司内网 IP 发起的请求才能删除生产环境的 Pod。RBAC 做不到,它不认识“时间”和“IP”这两个维度。
ABAC 的笨重
ABAC(基于属性的访问控制)听起来很美,把用户、资源、环境属性都考虑进去。但我在 1.24 版本上试过配置 ABAC 策略文件,那个 JSON 文件写得我头都大了。而且每次改策略都要重启 kube-apiserver,这对于我们这种 7x24 小时跑业务的集群来说,简直是灾难。更别提那个策略文件一旦长了,维护起来就是个黑盒,谁也看不懂谁写的逻辑。
OPA 的灵活性
后来我们引入了 OPA(Open Policy Agent)和 Gatekeeper。这东西确实强,它能弥补 RBAC 的短板。比如上面那个“限制时间删除 Pod”的需求,用 OPA 写个 Rego 策略就很清晰。
但 OPA 也有痛点。它太灵活了,甚至有点“失控”。如果所有权限判断都扔给 OPA,那开发同学学习 Rego 语言的成本极高。而且,OPA 是旁路校验(Admission Webhook),它不负责“鉴权”(Authentication/Authorization)的底层逻辑,它只负责“准入”。也就是说,RBAC 通不过的请求,根本到不了 OPA 这一层。
我们的混合策略
所以,我们现在的做法是:RBAC 做基础隔离,OPA 做精细化拦截。
具体的分工是这样的:
get, list, watch 权限,这是通过 RBAC 死死卡住的。我们利用了 1.30 版本里对子资源(如 Pod logs、exec)的精细化控制能力,把 exec 权限收得很紧。team 和 cost-center 这两个 Label,否则拒绝创建。这属于业务合规,RBAC 管不了,扔给 OPA。这里有一个我们当时遇到的一个实际问题。有一次线上排查问题,开发同学需要 kubectl exec 进一个 Pod 看日志。按理说他没有 exec 权限,但他发现只要他创建一个 Pod(他有 create pod 权限),在 spec.containers.command 里写个后门脚本,就能绕过 exec 的限制。
我们当时排查了半天,最后发现是 RBAC 只限制了 pods/exec 子资源,但没限制 create 权限下的容器启动命令。
解决方案就是上 OPA。我们写了一个策略,禁止 Pod 使用特定的 command 覆盖,或者限制 Pod 的 image 必须是白名单里的。
看下我们当时写的一个 OPA 策略,用来限制只有带特定 Label 的 ServiceAccount 才能使用 latest 标签的镜像(这是个坏习惯,但在过渡期我们得拦住):
这个策略的意思是,在 prod 命名空间,除了 infra 命名空间下的 ci-robot 这个 ServiceAccount(因为 CI 构建时确实需要临时用 latest),其他人都不能用 latest 镜像。
总结一下,RBAC 是地基,必须打牢,但别指望它解决所有问题。OPA 是精装修,用来堵那些 RBAC 堵不住的漏洞。这种混合模式跑了一年多,没出过权限越界的问题,而且维护成本比纯 ABAC 低多了。
做权限治理最头疼的不是“给权限”,而是“收权限”。我们那个集群跑久了,堆积了大量的 RoleBinding。很多同学离职了,或者项目下线了,但权限还留着。我称之为“权限僵尸”。
去年 11 月份,我们搞了一次安全审计,用 kubescape 扫了一下,报告里显示集群里有 17 个 ServiceAccount 拥有 cluster-admin 权限,其中 12 个挂载在根本不重要的测试服务上。这太吓人了。如果其中一个测试服务被攻破,黑客就能直接控制整个集群。
手动排查的无力感
一开始我想手动理,用 kubectl get clusterrolebindings -o json | jq ... 这种命令去筛。结果几千行的 JSON 输出,眼睛都看花了,还没理出头绪。而且,你很难判断一个权限是不是“真的没用”。比如某个 SA 有 get secrets 的权限,你删了它,结果半夜告警说某个应用读不到证书了。这种背锅的事我可不干。
引入 kubectl-who-can 和自动化脚本
后来我主要依赖 kubectl-who-can 这个工具(它是 kubectl 插件的一部分)。它能反向查询:谁有权限做某件事。
比如我想知道谁有权限删除 Nodes(这是个高危操作),我会跑:
但这还不够,我需要批量分析。我写了一个 Python 脚本,逻辑是这样的:
rules,把权限打平。我们开启了 K8s 的审计日志,存到了 Elasticsearch 里。通过对比,我发现了一个惊人的事实:我们给一个叫 report-generator 的 SA 授予了 get, list, watch 所有 Pod 的权限,但实际上审计日志显示,它过去 30 天只访问过 default 命名空间下的 Pod,而且只调用了 get 接口,QPS 峰值才 0.5。
AI 辅助的权限优化
这里我尝试用了一下 AI 辅助分析。我把那个 SA 的审计日志片段和现有的 Role 定义丢给 GPT-4(当时还是 3.5 版本,后来换了 4),让它帮我生成一个最小化的 Role 建议。
AI 给出的建议非常精准,它建议把原来的:
改成更具体的,甚至建议我如果只需要读取状态,其实 get 就够了,list 和 watch 都可以去掉,或者限制 resourceNames。
但我没敢直接全信 AI,我结合实际情况做了调整。因为那个服务下个版本要加新功能,需要 list 权限,所以我保留了 list,但把 watch 去掉了,并且把 resources 明确限定在它需要的几个 Pod 名称上(虽然这有点反模式,但在这种极度敏感的场景下,我愿意牺牲一点可维护性换取安全)。
实际操作与收敛
最后,我执行了权限收敛。这是我在测试环境跑的一个真实收敛脚本片段(Python 伪代码逻辑,但展示了核心思路):
执行完这个变更后,我观察了两天。那个服务的接口响应时间(P99)从原来的 120ms 变成了 118ms,几乎没影响,但安全系数提升了很多。
为什么这么做?
如果不做收敛,一旦 report-generator 这个服务被入侵,黑客就能通过这个 SA 的凭证,利用 watch 权限监听整个集群的 Pod 变化,甚至通过 list 获取所有 Pod 的详细信息,为下一步攻击收集情报。现在把它锁死在特定的 Pod 上,就算被攻破,影响面也极小。
现在的趋势也是往“策略即代码(PaC)”和自动化治理走。Kubernetes 1.30 之后,这种动态权限调整越来越实时。我现在的习惯是,每次上线新服务,先给个最小权限,跑一周,通过审计日志看缺什么权限再补,而不是一股脑给 *.*。这种“先紧后松”的策略,比“先松后紧”要安全得多。
去年我们接手了一个金融客户的私有化部署项目,业务跑在 Kubernetes 1.28 上,规模不算大,但合规要求极严。当时为了赶上线进度,我直接复用了之前电商项目的 RBAC 模板,给业务团队分配了 edit 这个 ClusterRole。
结果上线第三周,客户的安全团队直接找上门。他们发现某个业务 Pod 竟然能通过 ServiceAccount 访问到隔壁团队的 Namespace,甚至能读取 Secret。我当时冷汗都下来了,赶紧排查。
问题出在哪儿?我当初图省事,把 edit 权限绑定到了一个 全局的 ClusterRoleBinding,而不是针对具体 Namespace 的 RoleBinding。导致权限直接穿透了租户隔离。
我连夜做了两件事:
* 把全局绑定全部拆解,换成基于 Namespace 的 RoleBinding。
* 利用 kubectl-who-can 扫了一遍集群,把那些拥有 get secrets 权限的非必要账号全部回收。
那次之后,集群的权限策略从原来的 20 多条膨胀到了 100 多条,虽然维护起来麻烦了点,但再也没出过越权的问题。
关于 RBAC,我有几个不太“政治正确”的看法:
* 小团队别过度设计。如果你团队就三五个人,集群也没跑什么敏感数据,直接用简单的命名空间隔离就行。搞一套复杂的 OPA 或者花里胡哨的权限治理平台,纯属给自己找活干,维护成本远大于收益。
* 别迷信“最小权限”。理论上最小权限最安全,但实际上开发同学会天天找你提权限,你会变成瓶颈。我现在的做法是:开发环境给宽松点,生产环境卡死。生产环境我坚持用 Role 而不是 ClusterRole,除非你真的需要操作节点或 PV。
学 RBAC 别光看文档里的 apiGroups 和 resources,那些背了也记不住。
直接去搭个本地的 Kind 集群,试着把你自己锁在一个 Namespace 里,让你自己只能看 Pod,不能删 Service。 当你真的被 403 Forbidden 卡住的时候,你才会明白 verbs 和 resources 到底是怎么配合的。这种“被拒绝”的感觉,比看十篇教程都管用。