ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubernetes安全加固实战:RBAC、NetworkPolicy与Secret管理全解析

Kubernetes安全加固实战:RBAC、NetworkPolicy与Secret管理全解析

做容器平台运维这些年,我见过太多“看起来能用、实则裸奔”的集群:Dashboard用NodePort直接怼到公网,ServiceAccount权限大得能删整个namespace,Pod里privileged的root进程随便跑,Secret用base64编码就当成加密……每次做安全评审,一抓一个准。更麻烦的是,很多人觉得Kubernetes安全是安全团队的事,但真实踩过坑才明白:从你创建第一个Deployment开始,安全就是集群管理员、开发者和运维者共同的事。

这篇内容虽然标题是安全机制,但我不想写成文档式的原理罗列。我把它拆成一线运维真正会碰到的六块:威胁全景、RBAC权限、网络隔离、Pod安全、Secret管理、审计日志。每块都带实际配置和踩坑记录,你可以直接对着抄作业,也可以拿来做加固对照清单。适合正在维护生产集群的同学,也适合准备把安全从零补起来的新手团队。

1. 先把敌人找出来:K8s安全威胁全景

1.1 四个最容易被打穿的地方

聊安全机制之前,先把攻击面理清楚。我做过几次故障复盘和攻防演练,大多数K8s集群被打穿,路径无非就四条。

第一是控制面暴露。kube-apiserver是集群的总闸门,如果这玩意儿直接暴露在公网、没有严格的认证和TLS校验,等于把大门钥匙挂在了门口。很多早期集群为了让同事方便访问,把6443端口监听在0.0.0.0,防火墙规则又是全放行,扫到端口就能直接发请求,甚至能匿名读取部分数据。这是最严重的入口,没有之一。

第二是容器逃逸和权限滥用。Pod本身是隔离单元,但隔离强度取决于运行时配置。privileged容器、挂载了宿主目录的hostPath、把Docker socket塞进容器、以root用户运行容器进程,这些都是逃逸的常见跳板。攻击者一旦通过应用漏洞进入容器,再借这些配置逃逸到宿主机,整个Node就沦陷了,后面就是横向移动。

第三是供应链污染。有相当一部分事故不是打进来的,而是从镜像里带进来的。基础镜像里埋了挖矿程序、依赖包被投毒、镜像仓库不设防、拉取策略不校验,这些问题会在每次部署时悄悄把恶意代码放进集群。我见过某团队把内部镜像放在一个没有鉴权的仓库里,被人在镜像里塞了后门,上线当天整个集群的Pod全被人拿去挖矿。

第四是配置错误。这个覆盖面最广:RBAC授权过宽、Secret明文进环境变量、Pod没设resources导致邻居被挤爆、NetworkPolicy完全不配、未知来源的Ingress规则暴露内部服务。配置类漏洞单个看不致命,组合起来就是一条完整的攻击链。

1.2 分层防御的通用模型

面对这么多攻击面,靠一个组件解决所有问题是不可能的。我在实际加固时,会把安全问题拆成几个隔离的层,从外到内逐层收缩,这样即使某一层被突破,攻击面也不会直接展开到全集群。

层面大致是这样:最外层是网络边界,解决谁可以访问API Server和集群网络;往里一层是认证与授权,解决用户和服务身份能做什么;再往里是准入控制,解决什么样的Pod允许被创建;再往里是运行环境本身,解决容器隔离和资源限制;最深层是数据安全,解决敏感凭据的存储与访问;最后是审计和监控,让攻击行为留痕,方便事后溯源。

这套分层不是安全团队拍脑袋定的,它是K8s自身架构天然划分的:API Server负责认证授权和准入,kubelet和容器运行时负责执行层的隔离,etcd负责数据存储,CNI负责网络路径。每一层你都有对应的原生机制可以加固,不理解这一点,就会在某个单点上死磕,结果其他地方全是洞。

2. 权限管控:RBAC 与 ServiceAccount 的完整实践

2.1 ServiceAccount:默认权限比你想的大不少

RBAC的全称是基于角色的访问控制,它是K8s集群权限体系的核心。很多初学者以为RBAC就是创建几个Role然后绑定用户,但实际运维中,最容易出问题的不是用户,而是ServiceAccount。

先明确一个概念:Pod里跑的进程要访问API Server时,它使用的身份不是你的公司账号,而是ServiceAccount。默认情况下,每个namespace里有个叫default的ServiceAccount,而且每个Pod创建时,如果没有显式指定serviceAccountName,就会自动挂上这个default SA,同时把它的访问令牌挂载到容器的/var/run/secrets/kubernetes.io/serviceaccount目录里。

从K8s 1.24开始,挂载的令牌不再是长期有效的静态Secret,而是通过TokenRequest API签发的短期令牌,默认过期时间一小时左右,这比之前版本安全了不少。但真正的问题不在这里,而在于两个误区。

第一,如果你的应用根本不需要访问API Server,那自动挂载令牌就是白白扩大攻击面。容器被攻破后,攻击者可以拿这个令牌尝试调用API。正确的做法是在Pod的spec里设置automountServiceAccountToken: false,或者直接修改default SA的automountServiceAccountToken: false,让集群里新建的Pod默认不挂载令牌。

第二,很多人会给default SA甚至整个namespace的所有ServiceAccount绑定很高的权限,图省事。这是最典型的权限过大问题。攻击者只要打进任何一个Pod,拿到默认SA令牌,就继承了全部权限。

我在新集群里通常先做这几件事:检查所有namespace下有没有可疑的高权限ClusterRoleBinding绑定到default SA;把不需要访问API的命名空间里,default SA的自动挂载关掉;然后给每个应用单独建SA,再按需绑定最小权限。

2.2 Role、ClusterRole 与绑定关系的最佳实践

RBAC有两套角色对象:Role和ClusterRole。Role只作用于单个namespace,ClusterRole作用于整个集群,也可以被RoleBinding引用到某个namespace里。选型逻辑很简单:如果权限资源是namespace级别的(比如Pod、Deployment、Service),优先用Role;如果是集群级别的(比如Node、PersistentVolume、ClusterRole本身),才用ClusterRole;如果想让一组权限被多个namespace复用,就建一个ClusterRole,然后在各个namespace里用RoleBinding引用它。

绑定关系也分两种:RoleBinding把角色绑定到某个namespace内的Subject(用户、组、ServiceAccount);ClusterRoleBinding把角色绑定到整个集群范围的Subject。有一句口诀我经常给团队讲:RoleBinding绑定ClusterRole,权限范围被限制在该RoleBinding所在的namespace;ClusterRoleBinding绑定ClusterRole,权限范围才是整个集群。

在写rules的时候,最核心的原则是最小权限。我见过有人为了让某个应用能读Pod日志,直接给了*这个verbs,最后这个应用能看到所有Pod的日志还能删掉所有Pod。用kubectl auth can-i --list --as=system:serviceaccount:demo:web-sa -n demo就能快速验证某个SA到底有哪些权限,这个命令在排查权限问题的时候几乎是必用工具。

需要注意的是,RBAC变更不是要等很久才生效,kube-apiserver会动态加载规则,通常几秒钟内就可以生效。我曾经有个误解,以为要kubectl apply之后过一段时间才生效,结果有一次紧急收紧权限后立刻去验证,发现确实马上就没权限了。不过如果你开着多个API Server的高可用模式,各节点之间可能有短暂的缓存延迟,紧急情况下给所有的API Server节点都保留几秒的余量。

2.3 实战中踩过的权限坑

第一个坑是Secrets的读取权限被当成普通权限乱发。在RBAC体系里,secrets资源是非常敏感的,因为Secret里存的是各种密钥和令牌。你只需要get、list权限就能把整个namespace的Secret全部拖走,而很多团队竟然在自定义Role里顺手给了一个*,美其名曰“灵活”。在生产中,应该把Secrets的读写权限严格限制给少数几个专用SA,其他应用一律不要给。

第二个坑是Role和RoleBinding的namespace不匹配。你创建一个Role在ns-a,然后用RoleBinding把它绑定到ns-b里的ServiceAccount,这个绑定是不生效的。很多新手排查了半天,发现根本原因是RoleBinding引用了错误namespace里的Role或者ClusterRole名称写错。用kubectl get rolebinding -n ns-a -o yaml检查时,要关注roleRef的kind和name,以及subjects的namespace字段。

第三个坑是aggregate权限聚合。有些组件(比如dashboard这类)通过标注聚合ClusterRole来实现动态扩展权限,如果你在Roll中配置了aggregationRule,但下面的role自身没有对应的clusterRoleSelector匹配,最终权限可能为空,或者反过来聚合出了意料之外的过大权限。遇到这类权限异常,直接检查聚合标签是否匹配了预期之外的角色。

我习惯的做法是为每个应用或每个需求单独建一个最小Role,而不是让多个应用共享一个大而全的角色。虽然维护成本高一点,但一旦某个应用被攻破,损失半径是可控的。为了降低维护成本,我把角色的命名规则统一成<app-name>-<purpose>,比如web-read-pods、web-get-secrets这种,一眼能看出是给谁用的。

3. 网络层面的隔离:NetworkPolicy 落地指南

3.1 默认策略是个“裸奔”状态

所有K8s集群默认的Pod网络是“全互通”的。也就是说,不管Pod在哪个namespace、属于哪个应用,只要网络层可达,它们之间就能通信。这在开发阶段很舒服,但在生产环境就是个巨大的安全隐患。

打个比方,整个集群就像一个写字楼,默认所有办公室的门都敞开着,任何人都能自由进出任何房间。而你之前写的所有服务,其实都需要在办公室门口加装门禁,规定谁能进、谁能出。K8s里的门禁就是NetworkPolicy。

不过必须说明一个前提:NetworkPolicy要生效,依赖CNI插件对它的实现。如果你的集群用的是不实现NetworkPolicy的那种基础网络插件,那么你配了也没用。判断方法很简单,看CNI类型,如果是支持NetworkPolicy的类eBPF方案、或者主流的网络插件里的NetworkPolicy模式,就能用;否则就配不了,需要换CNI或者额外部署支持策略的网络方案。

还要记住一个核心规则:在一个namespace里,如果你为某个Pod写了一条NetworkPolicy,那么它在这个策略覆盖方向(Ingress或Egress)上,默认拒绝所有未显式放行的流量。多个策略之间是叠加的,只要任意一条策略放行就被放行,但如果一条策略的限制更严格,而另一条放行得更宽,最终效果取并集。很多人在这里绕晕了,紧记“默认拒绝+规则并集”就能理清楚。

3.2 一套能直接用的隔离配置

我建议生产环境采用一套最基础的防御配置:先对整个namespace做默认拒绝,然后显式放行需要通信的流量。下面是一个经典的三件套配置。

先看默认拒绝:

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: deny-all namespace: demo spec: podSelector: {} policyTypes: - Ingress - Egress

这个配置对namespace里所有Pod生效,Ingress和Egress都拒绝。紧接着放行DNS解析,否则Pod之间的域名解析会失败:

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-dns-egress namespace: demo spec: podSelector: {} policyTypes: - Egress egress: - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system ports: - protocol: UDP port: 53 - protocol: TCP port: 53

最后,再为具体应用开放端口。比如前端Pod要访问后端Pod的8080端口:

apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-frontend-to-backend namespace: demo spec: podSelector: matchLabels: app: backend ingress: - from: - podSelector: matchLabels: app: frontend ports: - protocol: TCP port: 8080

这里有个细节:如果backend还需要接收来自外部负载均衡器的健康检查流量,或者来自监控组件的抓取流量,必须在ingress里把对应的来源和端口一并放行。否则你会发现“退路全断”,Pod健康检查失败,服务莫名被摘掉。我遇到过不止一次,配了NetworkPolicy后服务探针失败,原因是忘了放行kubelet对Pod默认探针的流量。

3.3 排查 NetworkPolicy 不生效的方法

说实话,NetworkPolicy是最容易出“看起来配了但流量还是通/不通”的环节。我排查的时候有一个固定流程,建议你对照着用。

第一步,确认CNI支持。先kubectl get networkpolicy -n demo看策略有没有被接受,如果不支持,策略可能不会出现在CNI侧的管理面,甚至会被忽略。更稳妥的方法是直接看CNI组件的文档和日志,比如看看是否有network policy相关配置开关。

第二步,确认策略里的podSelector和namespaceSelector匹配了预期对象。很多人栽在这里:namespaceSelector要匹配的目标namespace必须有对应的标签,而Pod的标签则要看kubectl get pods --show-labels -n demo。如果标签没打上,selector会匹配空集,策略等于白配。

第三步,拿一个测试Pod进去实际验证。我常用netshoot这类排障镜像临时起一个Pod,放在两个需要联通的namespace里,分别用curl、ping、nc测试端口连通性。有一个小技巧:从被限制的Pod里往外测,看通不通;再换到没有匹配策略的Pod里测,做对照实验,就能定位是策略没生效,还是规则写窄了。

第四步,确认多策略叠加。如果一个Pod同时命中多条策略,它们的效果是并集,不要以为某条策略会“覆盖”另一条更严格的。如果有人写了一条Deny策略,又有人写了一条Allow策略,最终是“Deny策略作为基线,Allow策略在基线上开放”,所以必须是默认拒绝+显式放行的思路,否则很容易出现“一条策略把另一条给冲掉了”的假象。

4. Pod 安全从准入开始:从 PSP 到 Pod Security Admission

4.1 为什么 PSP 会被废弃,替代方案又强在哪

在早期版本里,K8s用PodSecurityPolicy来控制Pod能不能以特权模式运行、能不能挂载hostPath等。但是PSP的体验一言难尽:授权模型混乱,RBAC和PSP遗留下来一堆兼容性配置;策略默认行为经常让人意外;维护成本高。所以在1.21版本被标记为废弃,1.25版本被正式移除。

替代方案是内置的Pod Security Admission,简称PSA。它的核心思路是把安全策略固化成三个预定义级别:privileged、baseline、restricted。这三个级别从放宽到严格,基本覆盖了绝大多数工作负载的需求。

  • privileged:完全不设限制,适用于系统级组件或者需要特殊权限的Pod。
  • baseline:禁用明显的危险配置,比如privileged容器、hostNetwork、hostPID、hostIPC、hostPath,以及一些明显容易被滥用的不可变文件系统、坏caps。
  • restricted:在baseline基础上更进一步,强制非root运行、限制capabilities、要求seccomp等。这是应用上线时最推荐的策略。

PSA的好处是不需要像PSP那样维护复杂的CRD和授权绑定,只要给namespace打上标签,准入控制器就会自动生效。这对实际运维来说是个巨大的减负。

4.2 三种模式怎么用

PSA支持的三种模式:enforce、audit、warn。它们可以同时工作。

enforce是硬性拒绝,不符合策略的Pod根本创建不出来。audit是记录违规事件但不阻止,方便你观察集群里到底有多少工作负载不符合标准,但又不会影响现有业务。warn则是在用户执行kubectl apply时给出警告提示,同样不阻断操作。

我第一次启用PSA时,没有直接上enforce,而是先用audit和warn观察了一周。这一步非常关键,因为在存量集群里,直接enforce restricted会导致大量现有Pod创建失败,很多基础组件根本不符合条件。正确的路径是:先audit看违规面,再warn提醒开发,然后逐步整改,最后才切换到enforce。

给namespace打标签的方式像这样:

kubectl label namespace demo pod-security.kubernetes.io/enforce=restricted kubectl label namespace demo pod-security.kubernetes.io/audit=restricted kubectl label namespace demo pod-security.kubernetes.io/warn=restricted

在restricted限制下,创建Pod时如果缺少必要的securityContext配置,准入控制器会直接拒绝,并给出具体的违规项。比如缺少runAsNonRoot: true、没有设置seccompProfile、没有drop所有capabilities,都会被一条条列出来。我经常跟开发同学说,看到这几个报错别慌,看提示就知道怎么改。

4.3 配置实例:把存量工作负载迁到 restricted 模式

存量集群迁移这一步,是所有人最容易头大的环节。我把这个过程拆成了四步,照着做基本不会出大岔子。

第一步,全局审视。把所有namespace先全部标记为audit=restricted,然后跑一天,在审计日志里看有哪些工作负载被记录。也可以用命令直接搜所有违规事件,对照namespace逐个排查。

第二步,改造Pod。给每个工作负载的Pod模板加标准的securityContext。一个典型的restricted配置长这样:

securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 2000 seccompProfile: type: RuntimeDefault capabilities: drop: ["ALL"] allowPrivilegeEscalation: false

注意runAsUser不能省略,因为很多基础镜像默认以root运行。如果不想一个一个改镜像,可以在Deployment级别统一加,但这样不同团队并行开发时会很不方便,所以我更建议把安全配置在模板里写清楚,镜像尽量默认非root启动。

第三步,临时豁免。有些系统级组件确实需要特权模式运行,比如网络插件、存储插件、日志采集器。PSA支持exemptions功能,可以通过在apiserver配置中指定免于强制的命名空间、用户、运行时类。我当时把部分系统组件所在的namespace放进了exemptions,但要定期复核,确保这个豁免名单不会无限扩大。

第四步,切换enforce。确认所有业务Pod都满足restricted要求之后,再给namespace打上enforce=restricted标签,同时保留audit来持续记录。如果后续有新应用接入,我们在CI阶段就会跑一遍PSA的检查,而不是等上线了才发现不合规。

这里有一个我踩过的坑:有些Java应用以低用户启动时,启动脚本尝试绑定1024以下的端口,会报权限失败。这样的应用需要在Pod模板里指定NET_BIND_SERVICE能力,但不能违反drop ALL的限制。restricted策略允许显式添加回NET_BIND_SERVICE,前提是你把其他capabilities都drop掉。所以并不是说一个cap都不能加,而是必须“先全drop,再按需加回最小集”。

5. 核心资产保护:Secret 管理的正确姿势

5.1 Secret 并不等于加密

很多人的第一个误区就是把Secret当成了加密存储。实际上,Secret只是用base64编码之后存到etcd里,base64不是加密,它只是一种编码格式,任何人只要能访问etcd、或者有secrets的读取权限,就能直接解码看到明文。

更让人头疼的是,从Kubernetes 1.24开始,Secret在etcd中的存储默认仍然是明文(除非你显式开启了静态加密)。而且即便你在集群里为Secret开了EncryptionConfiguration,如果旧的Secret一直没被更新,它们依然会以明文躺在etcd里。这一点我在后面会具体讲。

还要提醒一个容易被忽略的细节:通过环境变量方式注入Secret,会直接在Pod的spec以及运行时的进程环境变量里暴露。容器被攻破后,攻击者可能直接读/proc/1/environ拿到开具的参数,连etcd都不用碰。所以我尽量不用环境变量注入敏感信息,改成文件方式挂载,而且只挂载需要的那个key,而不是整个Secret目录。

比如:

volumeMounts: - name: app-secret mountPath: /etc/app/db-password subPath: db-password readOnly: true

这会只把Secret里的db-password这一个key挂到容器里,其他key完全不可见。同时,给挂载文件设置合适的权限,默认是0644,如果应用不需要读,就不要让其他用户能读。

5.2 静态加密的配置流程

要让Secret真正在存储层面加密,你需要给kube-apiserver配置EncryptionConfiguration,并重启API Server。

首先,生成一个随机的数据加密密钥,并转为base64,比如:

head -c 32 /dev/urandom | base64

然后写一个EncryptionConfiguration文件,放到API Server所在的节点上。假设路径是/etc/kubernetes/encryption/encryption.yaml,内容大致如下:

apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aescbc: keys: - name: key1 secret: <上面生成的base64> - identity: {}

这里的顺序有讲究:第一个provider负责加密新写入的数据;后面的identity是明文provider,用来读取已经存在的明文数据。如果你先把identity放前面,会把新secret也以明文写入,等于白配。

改好配置后,需要在kube-apiserver的启动参数里加上--encryption-provider-config=/etc/kubernetes/encryption/encryption.yaml,并把配置文件挂载进apiserver所在Pod的Volume。如果是二进制方式部署modify你的启动systemd unit,如果是容器方式部署则编辑静态Pod的Manifest。改完会触发API Server滚动重启。

重启完成不等于旧Secret自动加密。因为旧的Secret已经在etcd里以明文存在,只有等到它被下一次update时,才会按新的provider重新编码。所以你需要主动触发一次更新,比如:

kubectl annotate secret my-secret force-encryption="$(date +%s)" --overwrite -n demo

这个annotate操作本质上是一次update,API Server会用第一个provider重新加密并写入etcd。对所有存量Secret都执行一遍之后,才能在etcd里完全消除明文。验证方法也简单,直接去etcd里看数据,如果值开头是k8s:enc:aescbc:v1:...,说明已经加密过了。

5.3 密钥轮换与外部集成

密钥轮换是Secret管理中另一件容易被忽视的事。理论上你可以通过kubectl create secret重建同名Secret,但麻烦在于所有引用这个Secret的Pod怎么更新。文件挂载方式下,kubelet会定期同步Secret到Pod的卷目录,所以覆盖写入后,Pod内的文件会更新,但进程可能不会自动重新读取。环境变量方式更麻烦,必须重建Pod才能生效。

比较稳妥的做法是,在应用层实现配置热加载,或者让Secret变化触发Deployment的滚动更新。比如用一个hash校验和作为Deployment模板的annotation,当Secret内容变化时,把新的hash写进annotation,触发滚动重启。这个方法很简单,却很实用。

再进一步,生产环境如果要合规和强安全,建议把Secret的静态加密密钥托管到外部的密钥管理系统。我不展开讲具体厂商,但思路是:利用云厂商的KMS或专用的密钥管理方案生成解密密钥,K8s的EncryptionConfiguration里配置对应的KMS Provider,这样即使etcd文件被拖走,没有外部KMS的授权也解不开。

有一点要提前说清楚:静态加密保护的是etcd这个存储层,它防不住应用层泄露。真正要防的不是“库里的人拿了一把key”,而是“攻击者搞到一个应用权限后,能否顺藤摸瓜拿到数据库密码”。所以Secret的管理必须和RBAC、网络隔离、容器安全配合,才能形成闭环。单独加密一个etcd,只是打好了地基。

6. 让攻击无处遁形:审计日志配置与关键事件解读

6.1 审计策略如何配置

K8s的审计日志由kube-apiserver输出,记录了所有对API Server的请求。默认情况下审计策略是没有开启的,或者沿用一套极其简单的规则,导致你事后想排查都找不到线索。

审计策略文件大致长这样:

apiVersion: audit.k8s.io/v1 kind: Policy rules: - level: Metadata resources: - group: "" resources: - secrets - configmaps - level: Request verbs: ["delete"] resources: - group: "" resources: - namespaces - pods - level: None users: - system:apiserver - system:kube-scheduler - system:kube-controller-manager - level: Metadata resources: - group: "" resources: ["pods"] verbs: ["create"]

这个配置的含义是:对Secrets和ConfigMaps的访问记录元数据;对删除namespace或Pod的请求记录完整的请求对象;排除系统自身组件的流量;对Pod的创建记录元数据。注意,审计规则是从上到下匹配,第一个匹配到的规则生效,所以排障用的规则要放在前面,系统组件的排除规则要放在尽量靠后的位置。

改完策略后,需要在kube-apiserver启动参数里加上几个参数:

--audit-policy-file=/etc/kubernetes/audit-policy.yaml --audit-log-path=/var/log/kubernetes/audit.log --audit-log-maxage=30 --audit-log-maxsize=100 --audit-log-maxbackup=7

maxage是保留天数,maxsize是单个文件大小阈值(MB),maxbackup是最大备份文件数。这几个参数建议根据你的磁盘空间和合规要求调一下,我一般生产环境会保留至少30天,因为大多数安全事件是在几周甚至几个月后才被发现的。

6.2 安全事件重点盯住哪些

审计日志是全量记录,大量噪音请求会淹没真正的高危事件。我平时重点关注这么几类。

第一类是privileged Pod的创建。一旦有人创建了特权容器,往往意味着攻击者在尝试逃逸。可以通过审计日志筛选出所有创建Pod时spec里包含privileged: true的请求。

第二类是pods/exec操作。kubectl exec进入Pod等于拿到了一个容器内的shell,如果这个Pod不是你的正常调试流程,那很可能就是入侵者在执行命令。筛选规则就是找resource=pods且subresource=exec的create请求。

第三类是Secrets和ConfigMaps的查看请求。有人批量读取这些敏感资源,通常就是试图提取凭据。第四类是RBAC变更,包括创建/修改Role、ClusterRole、RoleBinding的行为,因为攻击者会通过修改授权给自己留后门。第五类是删除操作,删除namespace、删除Pod、删除PersistentVolume等,常见于清理痕迹或者破坏业务。

拿到审计日志后,用jq可以快速过滤,比如想找到所有pods/exec的调用:

jq 'select(.objectRef.resource=="pods" and .objectRef.subresource=="exec" and .verb=="create")' /var/log/kubernetes/audit.log

如果是边缘环境没有jq,直接用grep也一样:

grep '"resource":"pods"' /var/log/kubernetes/audit.log | grep '"subresource":"exec"'

日志里的字段能追溯到发起人、目标对象、源IP、UserAgent。一般看到源IP是外部地址、UserAgent是非kubectl的奇怪客户端,就可以直接视为可疑行为,拉高告警级别。

6.3 安全加固自查清单

我在接手每一个集群时都会做一个快速的自查,照着这个清单过一遍,基本能把集群的底裤翻出来看看。建议你也存一份,每季度跑一次。

检查项风险等级检查方法/目标
API Server暴露范围高危确认6443端口不对公网开放,防火墙仅允许内网/管理网访问
匿名访问高危`kubectl get clusterrolebinding
RBAC过度授权高危定期审计ClusterRoleBinding和RoleBinding,重点看*权限
ServiceAccount自动挂载中危不需要访问API的命名空间关闭automountServiceAccountToken
特权容器数量高危审计日志里搜索创建privileged容器的事件
Secret静态加密高危检查EncryptionConfiguration存在且已生效,存量Secret已重写
NetworkPolicy覆盖中危确认核心命名空间有default-deny策略
Pod Security Admission中危确认新namespace默认带restricted标签
审计日志留存中危确认audit策略已配置,日志保留周期符合合规要求
镜像来源高危确认镜像仓库有鉴权,关键应用配置了ImagePullPolicy和摘要校验

这张表不是一遍过就完事的,它是持续的过程。新业务不断上线,新组件不断引入,集群规模变大后,原本合理的权限配置可能就不再合理了。安全加固不是一次性交付,而是像打补丁一样,要跟着系统演化持续维护。

我个人在实际操作中有一个体会:做安全加固最怕一步到位的心态。如果你在一个繁忙的生产集群里直接上restricted等级、直接全网全线default-deny,大概率会把现有业务搞得鸡飞狗跳。我习惯采用渐进式收敛的策略:先低成本地把高危项堵上,比如关掉匿名访问、清理过大的RBAC、开启静态加密,然后花一段时间观察业务影响,再把安全水位逐步拉高。每改一步,都去验证业务的连通性和核心链路,确保加固动作本身不会成为新的故障源。

最后再分享一个小技巧:在每次大版本升级前,把集群的安全参数导出来做一次备份,升级后做一个diff。很多安全配置在升级过程中会被默认值吞掉或者被废弃字段影响,提前做好对比,能少踩很多“升级完才发现安全策略没生效”的坑。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进