要聊Kubernetes安全机制,得先有个共识:K8s默认的安全配置,在真实的生产环境里基本是不够看的。如果你只是用kubeadm装了个集群,或者托管到某云平台直接开箱即用,那你会发现API Server默认允许匿名访问、Pod之间可以随意通信、ServiceAccount的token挂在每一个运行的容器里。真正到位的安全机制,不是某一个安全组件,而是从控制面到工作负载再到节点,一层层叠出来的防线。我帮几个项目排查过安全问题,最深的体会是:K8s安全不是一次性配置,而是理解它的信任模型之后,持续沉淀的过程。
这篇内容适合刚摸过K8s、开始操心生产环境安全的人,也适合已经踩过坑、想系统梳理一遍安全维度的运维和开发。我尽量从机制原理讲到实操配置,把认证、授权、准入控制、网络安全、数据加密这几个核心层面都拆开说明白。
1. 先摸清K8s安全机制的整体脉络
1.1 安全边界在哪里:控制面才是真正的钥匙
Kubernetes的安全机制,核心都围绕一个东西展开:API Server。所有变更集群状态的操作,无论你是用kubectl、编写Controller,还是某个Pod里的ServiceAccount在调API,最终都要经过API Server的认证、授权和准入控制三层关卡。这个设计很像大厦的门禁系统:你不光要有门禁卡(认证通过),还得被允许进入某个楼层(授权通过),最后还得过安检,不能带违禁品上楼(准入控制)。
所以理解K8s安全机制的第一步,是把注意力从单个Pod、单个节点上稍微抬高一点,看向控制面。节点和Pod的安全当然重要,但控制面的API Server是所有管理动作的枢纽,这里失守等于整栋大楼的门禁卡被人批量复制了。我在实际工作中见过不少团队,花大力气给Pod加了各种安全限制,却把kubeconfig文件当成普通文件存放在代码仓库里,这等于门禁卡贴在门口,再多的楼层门禁都是摆设。
1.2 默认安全模型的三个盲区
K8s默认的安全模型至少有三种“过于开放”的地方值得注意。
第一是Pod间通信默认全互通。默认的NetworkPolicy是不存在的,意味着任何Pod可以用任意协议访问任意Pod,这跟经典网络安全里“默认拒绝”的原则完全相反。
第二是ServiceAccount的token会默认挂载到每个Pod的容器文件系统里。也就是说,只要Pod能被攻破,攻击者就能拿到一个合法的集群身份凭据。虽然绑定的权限可能很小,但如果你用默认的ServiceAccount并且没做额外授权,安全边界其实很模糊。
第三是绝大多数资源默认命名空间隔离,但网络层面不做隔离。一个命名空间里的Pod可以自由访问另一个命名空间的Service,这在多租户场景下尤其危险。
这三个盲区叠加起来,得出一个结论:K8s安全机制必须要主动建设,而不是开箱即得。
2. 认证与授权:把住API Server这道门
2.1 三类客户端,三条不同的认证路径
K8s认证体系里,请求API Server的客户端分为三类,经常有人搞混。
第一类是普通的“人”,也就是运维、开发,通常通过kubeconfig文件里的客户端证书、token或者OIDC身份来认证。这一层我建议大家优先启用OIDC或者使用云平台托管的身份认证,避免长期有效的静态token散落在各处。
第二类是K8s集群内部的服务账号,也就是ServiceAccount。这类身份是给Pod里的进程用的。K8s会把ServiceAccount对应的JWT token挂载到Pod内,Pod里的应用用这个token访问API。很多人会忽略一个细节:ServiceAccount挂载的是长期有效的token(除非用了TokenRequest API让它短期化),这意味着攻破一个Pod,就等同于获得一个长期有效的身份凭据。
第三类是无身份匿名请求。生产环境务必关闭匿名访问,或者精细化配置匿名策略。默认情况下,K8s允许部分匿名请求访问只读接口,谨慎起见建议显式禁止。
表格式区分一下三者差异:
| 客户端类型 | 典型场景 | 主要认证方式 | 风险点 |
|---|---|---|---|
| 自然人用户 | kubectl操作、对接企业SSO | 客户端证书、OIDC token | 静态证书容易泄露 |
| ServiceAccount | Pod内应用访问API | JWT token | 长期token随Pod泄露 |
| 匿名请求 | 健康检查、探测接口 | 无 | 默认开放部分接口 |
2.2 ServiceAccount的隐藏风险:automountServiceAccountToken
如果你的应用真的不需要通过API去操作K8s资源,那就别让那个token出现在容器里。K8s提供了一行关键配置:automountServiceAccountToken: false。这行配置可以写在ServiceAccount上,也可以写在Pod模板上,设置为false后K8s不会把token文件挂载进容器。
我强烈建议:在Pod模板中显式声明这个字段,不要默认依赖ServiceAccount级别的设置。因为你没法保证每一个ServiceAccount都被正确配置,但你可以保证每一个业务Pod都不挂载无用的token。之前在某公司检查项目时,发现有个数据同步的Pod竟然带着一个高权限ServiceAccount在跑,且token从未使用过。容器一旦被攻破,攻击者首先会翻这个文件,带来的后果不言而喻。
2.3 RBAC最小权限实操:一个可落地的配置示例
RBAC(基于角色的访问控制)是K8s授权的核心机制。理解它要抓住两个维度:一个是读操作还是写操作,另一个是作用范围是某个命名空间还是整个集群。Role授权某个命名空间内的资源,ClusterRole授权整个集群范围的资源;RoleBinding把Role绑定到用户或ServiceAccount,且只在指定命名空间生效,ClusterRoleBinding则在整个集群范围生效。
下面我写一个最小权限的示例。假设有个应用只允许读取某个命名空间下的Pod日志,不允许修改任何资源。先创建一个只有只读权限的角色:
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: namespace: demo name: pod-log-reader rules: - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "list", "watch"]再绑定给应用用的ServiceAccount:
apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: namespace: demo name: read-pod-logs subjects: - kind: ServiceAccount name: demo-app namespace: demo roleRef: kind: Role name: pod-log-reader apiGroup: rbac.authorization.k8s.io注意这里我只给了get、list、watch三个只读操作,完全没有create、update、patch、delete。如果这个应用需要读取ConfigMap,就在resources里增加"configmaps"。最小权限原则的核心不是一次配得多全,而是随着业务需求一点点扩,而不是一开始就把一个拥有所有权限的ClusterRole绑上去。
这里有个容易踩的坑:很多人图省事,直接把system:aggregate-to-admin或者cluster-admin这类高权限角色绑给业务ServiceAccount,这是大忌。某个应用需要读Pod列表,正常应该给list pods权限,但被误配成edit角色后,它能直接删掉别人的工作负载。这种事故在高频发布的项目里已经出现过不止一次。
2.4 快速验证权限:kubectl auth can-i
配置完RBAC后,怎么验证?用kubectl auth can-i命令最直接。
# 验证某个ServiceAccount能否在demo命名空间里列出Pod kubectl auth can-i list pods --as=system:serviceaccount:demo:demo-app -n demo # 验证当前用户能否删除所有命名空间的Deployment kubectl auth can-i delete deployments --all-namespaces第一个命令返回yes,第二个返回no,说明权限边界符合预期。这个命令还可以结合--list查看某个身份的全部权限,排查问题的时候特别有用:
kubectl auth can-i --list --as=system:serviceaccount:demo:demo-app -n demo它会输出这个ServiceAccount在这个命名空间下所有可执行的操作,非常直观。我把这个命令写进了团队的安全自查脚本里,每次变更RBAC后跑一遍,心里才有底。
3. 准入控制:Pod诞生前的最后一道安检
3.1 为什么光有RBAC还不够
RBAC管的是“谁能做什么”,但管不了“做的事情本身是否合规”。这就像你给了员工进机房的权限,但没法阻止他把服务器硬盘拿走。准入控制器就是那个设卡检查的人,它在API Server处理请求的最后阶段拦截请求,执行一系列策略校验,决定资源是否被允许创建或修改。
常见的准入控制器包括:NamespaceLifecycle、LimitRanger、ResourceQuota、PodSecurity等等。但真正值得在安全语境里注意的是Pod Security标准和自定义准入策略。
3.2 Pod Security Standards的三个级别
K8s官方把Pod安全划分成三个标准:
privileged(特权):不做任何限制,等同于完全信任这个Pod。系统组件或者需要访问宿主机设备的特殊应用才可能用到。
baseline(基线):禁止明显危险的配置,比如禁止privileged容器、禁止hostNetwork直接共享宿主机网络、禁止挂载宿主机的敏感目录。它适合大多数普通业务,但还不够严格。
restricted(受限):最严格级别,强制执行更多安全限制,比如必须以非root用户运行、必须声明只读根文件系统、禁止capabilities添加高危能力等。这个级别下,如果把常见镜像直接拉起来跑,很多会因为权限问题失败,需要你对镜像做适配。
生产环境的建议是:默认走restricted,确有特殊需求再个别豁免,不能让privileged成为默认。
3.3 用Pod Security Admission落地策略
从K8s 1.23版本开始,PodSecurityPolicy(PSP)已经进入废弃流程,取而代之的是Pod Security Admission(PSA),它通过给命名空间打标签来实施策略。配置非常简单但容易理解错。
apiVersion: v1 kind: Namespace metadata: name: production labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted这个标签组合的含义是:在production命名空间下,创建Pod时不符合restricted标准的请求会被直接拒绝;同时对于已存在的Pod会生成审计信息和警告,方便你逐步排查存量问题。
部署时有个现实问题:如果直接把restricted强制应用到存量业务命名空间,马上会有一堆Pod创建失败。稳妥的做法是先用warn和audit模式观察一段时间,把不兼容的应用逐步改造,最后再切换到强制模式。我在一次迁移中就是这样操作,把几十个存量Deployment逐个排查,解决掉镜像内写目录、root运行的问题后,才把标签改成enforce。
3.4 更灵活的策略即代码:Gatekeeper与OPA
Pod Security Admission只能管Pod自身的属性,管不了更复杂的业务逻辑。比如你想实现“所有含mysql关键字的镜像禁止部署到生产环境”,PSA就做不了。这类需求要用OPA Gatekeeper这一类策略引擎,把策略当作代码在集群内执行。
Gatekeeper的核心概念有两个:ConstraintTemplate(约束模板)和Constraint(约束)。模板定义校验逻辑,约束把这个逻辑应用到具体的资源范围。
举个例子,禁止生产命名空间部署有mysql标签的镜像。先定义一个模板:
apiVersion: templates.gatekeeper.sh/v1 kind: ConstraintTemplate metadata: name: block-mysql-image spec: crd: spec: names: kind: BlockMysqlImage targets: - target: admission.k8s.gatekeeper.sh rego: | package k8sblockmysql violation[{"msg": msg}] { input.review.object.apiVersion == "apps/v1" input.review.object.kind == "Deployment" container := input.review.object.spec.template.spec.containers[_] contains(container.image, "mysql") msg := sprintf("镜像不允许包含mysql关键字:%v", [container.image]) }再创建一条约束,指定作用到生产命名空间:
apiVersion: constraints.gatekeeper.sh/v1beta1 kind: BlockMysqlImage metadata: name: block-mysql-in-production spec: match: kinds: - apiGroups: ["apps"] kinds: ["Deployment"] namespaces: ["production"]之后任何包含mysql镜像的Deployment只要尝试创建或更新,都会被拒绝。这种方案适合团队已经沉淀出明确安全规范之后,把规范固化成自动化校验,能有效避免人为疏忽。
4. 网络隔离:从全互通到精细化管控
4.1 NetworkPolicy生效的位置,比你想象的低
K8s的NetworkPolicy由CNI插件来执行,比如Calico、Cilium、Weave。API Server只负责存储策略,真正拦截流量的是节点上的数据面组件。理解这一点很重要:如果集群网络插件不支持NetworkPolicy,写了策略也不会生效。所以安装集群时,选型CNI就要把是否支持NetworkPolicy考虑进去。
NetworkPolicy默认行为是allow-all。当你创建第一条策略之后,它的行为变为白名单制,即只允许策略明确放行的流量。这个语义反转很容易让刚上手的人踩坑:加了第一条策略,业务流量全断了。
4.2 先写默认拒绝策略,再逐步放行
推荐的做法是先建立一个安全的底座:默认拒绝所有入站和出站流量,然后按业务需要开白名单。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: demo spec: podSelector: {} policyTypes: - Ingress - Egress这条策略只允许命名空间内的Pod之间互访;如果加上Egress和Ingress两个策略类型并定义空规则,hips相当于啥都不放行。但要注意在还没放行DNS的时候,Pod之间的服务发现就会失败,因为无法访问kube-dns。
所以紧接着要放行DNS解析:
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: - port: 53 protocol: UDP这个坑我踩过一次:写完默认拒绝策略后,发现业务Pod互相调不了,排查半天,最后是DNS被策略拦住了。所以放行DNS的规则要跟默认拒绝策略一起部署,否则业务全瞎。
4.3 按命名空间和标签做精细隔离
生产环境里通常按命名空间划分环境边界,比如dev、staging、production。真正的多租户隔离,必须在网络策略层面体现:production只能被特定命名空间访问。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-from-shared-svcs namespace: production spec: podSelector: {} policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: tier: shared-services规定只有打了tier: shared-services标签的命名空间里的Pod,才能访问production命名空间中的Pod。除此之外全部拒绝。这种基于命名空间标签的选择器,比写死IP网段更动态也更稳定。
5. 数据与密钥安全:Secrets的加密与管理
5.1 Secret不是加密,只是编码
这点必须反复强调:K8s的Secret只是把内容做了Base64编码,不是加密。任何能读取etcd或者有权限查看Secret的人,都能直接还原内容。Base64是传输编码,不是安全编码。
很多人误以为把数据库密码放进Secret就等于加密了,其实不然。Secret的保密性依赖三个层面:etcd访问权限控制、API Server的RBAC控制、以及其他能读取etcd的组件安全。前提是开启了RBAC,且默认ServiceAccount没有get secret的权限,否则一个Pod被攻破,secret就跟着泄露。
5.2 开启etcd静态加密,让存储层不再是裸奔
Secret在etcd里默认是明文存储的。虽然etcd本身有访问控制,但文件落盘时内容本身没有保护。K8s支持通过EncryptionConfiguration给Secret等资源做静态加密,这才是真正意义上的加密。
下面是一个AES-CBC加密的配置示例(生产环境建议换用AES-GCM):
apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: - secrets providers: - aesgcm: keys: - name: key1 secret: c2VjcmV0LWUtY2N0LWtleS1wYXJ0LTAwMQ== - identity: {}然后修改API Server启动参数,把加密配置路径挂进去:
--encryption-provider-config=/etc/kubernetes/encryption/config.yaml如果用的是云托管的K8s服务,一般会有更简单的开关,直接在控制台上开启etcd加密。这个开关一定要打开,尤其是集群里跑着数据库密码、API密钥这类敏感数据的时候。
注意:静态加密只保护etcd存储,不保护API响应。任何有权限读取Secret的用户,通过kubectl get secret看到的依然是解密后的明文,这没问题,因为RBAC已经控制了谁能访问Secret。
5.3 Secret的轮换与消费:别让密钥挂在容器里
生产环境的Secret轮换是高频操作。很多团队配置Secret后几乎不换,等发现泄露才慌忙处理。更现实的做法是把Secret的消费方式从“挂载文件”改成“API读取 + 内存使用”,轮换时只需要更新Secret并重启Pod或触发更新。
如果用到外部密钥管理系统,比如云平台的KMS或者开源Vault,可以让K8s在启动时向外部系统拉取密钥,这样K8s里根本不存在长期密钥副本。这种方案的集成度更高,但也需要额外的运维成本。项目初期如果团队规模不大,先把RBAC、etcd静态加密做好,再把Pod里的Secret文件权限收紧,基本就能阻挡绝大多数低权限攻击路径了。
5.4 对存量集群的加密切换顺序
给一个已经运行了很久的集群突然开启etcd加密,会不会出问题?加密配置的provider列表里identity放在最后通常是为了支持存量数据解密。开启加密后,新写入的数据会按前面provider加密,老数据依然用identity方式存储,K8s会自动重新加密吗?
实际上,普通Secret在没有被修改的情况下不会自动重写。所以要彻底完成存量加密,需要触发一次Secret的更新,比如给每个Secret添加一个annotation并K8s会自动把新写入的数据加密。
实际操作时,我写过一个小脚本,遍历所有命名空间的Secret,加上一个时间戳annotation触发更新,观察etcd里的存储确实变成密文后才放心。
6. 运行时与镜像安全:守住节点和容器本身
6.1 镜像供应链:你拉下来的镜像到底来自哪里
镜像安全是运行时安全的入口。一个被植入恶意程序的镜像,跑起来后第一件事往往是外连、横向扫描、读取宿主机信息。所以镜像安全要做三道防线。
第一道防线是镜像来源可信:内部镜像仓库加上签名校验。保证只有经过CI流水线构建和签名的镜像才能进入生产集群。第二道防线是镜像漏洞扫描:在CI阶段用扫描工具检查镜像内的系统包、应用依赖是否有已知漏洞。第三道防线是部署时的准入校验:结合前面说的Gatekeeper,可以限制生产环境只能从特定镜像仓库拉取镜像,从源头消除“误用了公共镜像或未知来源镜像”的风险。
6.2 别让业务容器拥有特权
特权容器(privileged: true)意味着容器内的root用户与宿主机root用户权限几乎等同,隔离机制基本被绕过。这跟“用管理员账号跑普通业务”是一样的道理。生产环境应当禁止业务容器出现privileged字段,并限制其他危险的能力。
在Pod的SecurityContext里,有一个比较实用的配置组合:
securityContext: runAsNonRoot: true runAsUser: 10001 allowPrivilegeEscalation: false capabilities: drop: - ALLrunAsNonRoot确保不能用root启动,allowPrivilegeEscalation: false防提权进程产生,capabilities.drop: ALL把危险的Linux能力全部丢弃。很多业务镜像非root不可用怎么办?那就把镜像打包脚本里改为创建普通用户运行,这是标准化镜像时必须做的事情。我记得有个Java应用迁移到该配置时,最开始起不来,排查发现是日志目录没写权限,调整镜像权限之后就好了。这类问题基本都能在镜像层解决,不值得为了一时方便放开安全限制。
6.3 Seccomp与AppArmor:内核层面的“熔断器”
Seccomp用来限制容器内进程可以调用哪些系统调用。一个Web应用绝对不需要mount、reboot这类高危系统调用,限制之后有效降低内核级漏洞被利用的风险。
K8s从1.19版本开始,默认的SeccompProfile是RuntimeDefault,也就是用容器运行时的默认配置,这个配置已经能够屏蔽一部分危险调用。如果想做得更细,可以定义自定义profile并挂载到Pod:
securityContext: seccompProfile: type: Localhost localhostProfile: profiles/deny-mount.json不过日常实践中,先确保每个Pod都设置了seccompProfile.type: RuntimeDefault,比花时间调自定义profile优先级更高。AppArmor同理,它通过强制访问控制限制程序可访问的文件和权限。这两项都比较底层,适合对Linux内核机制有一定了解的团队逐步推进。
6.4 运行时异常检测:给节点加上安全哨兵
即使做了这么多防护,总有可能出现漏网之鱼。运行时安全检测工具(比如Falco)能在节点上监控系统调用,发现异常行为时实时告警。典型告警规则包括:容器内出现了不应该出现的shell进程、写入敏感目录、意外访问宿主机设备等。
这类工具适合已经有基础监控体系的团队。先把基础的安全配置做到位,再加运行时检测才有意义。我见过有团队Falco装了一大堆,但RBAC和NetworkPolicy完全开放,警报响个不停却没法识别真正的攻击路径。
7. 常见问题与排查技巧实录
7.1 用kube-bench做一次合规自查
kube-bench是检查集群是否符合安全基线的最佳工具。它会根据业界发布的CIS Kubernetes Benchmark逐项检查,包括API Server参数、etcd配置、kubelet配置、RBAC设置等。
# 在集群节点上运行基础检查 kube-bench run --targets master,node --version 1.24跑完会输出大量FAIL信息,别被吓到。先关注“高危”级别的失败项,逐项确认是否需要修复。很多检查项在某些定制环境里不一定适用,但默认集群跑一遍能帮你快速找到明显的硬伤。
7.2 审计日志怎么定位可疑操作
开启审计日志是溯源的关键。API Server可以通过--audit-log-path配置审计日志输出,日志里能看到谁在什么时间对什么资源做了什么操作。问题在于默认配置不会记录所有操作,需要定义审计策略,把关心的操作记录下来。
一个可行的简化策略:记录所有写操作(create、update、patch、delete),记录所有涉及Secret的读操作,其余读操作只记录元数据。日志量会比默认大,但可控。真正出事的时候,如果没有审计日志,你连是谁删了命名空间都不知道。
7.3 三个高发问题的排查实录
我把实际处理过的高频问题整理成表,方便你按图索骥:
| 现象 | 可能原因 | 排查命令 |
|---|---|---|
| 新部署Pod一直Pending,调度失败 | 部署命名空间触发了Pod安全准入,镜像不支持restricted | 查看Pod事件,确认是否因违反PodSecurity而被拒绝 |
| 业务Pod互相访问超时 | 新增NetworkPolicy后未放行对应路径或DNS | 查看命名空间下NetworkPolicy列表,临时放行测试 |
| 应用读取数据库密码报权限错误 | ServiceAccount不具备读取Secret权限,RBAC未放行 | kubectl auth can-i get secrets --as=system:serviceaccount:xxx:xxx -n xxx |
踩过最深的坑是给一个全局监控组件配置了ClusterRoleBinding,绑定的是view角色,本意是只读监控集群。但这个名字叫view的内置ClusterRole允许读取所有命名空间的资源,包括Secret的元数据。在某些场景下,元数据里的字段也可能暴露敏感信息。后来又加了一层限制,才把权限真正收住。
7.4 安全落地要避开的三个认知误区
第一个误区是“容器起了就是我安全了”。容器只是进程的隔离壳,核心安全机制还是依赖内核隔离能力以及K8s这些策略组件。第二个误区是“云平台托管版本不用管安全”。托管版只解决控制面组件的运维,RBAC、网络策略、Secret加密这些依然要你自己配置。第三个误区是“安全策略一次配完就够了”。K8s集群像活物一样在持续变动,新增命名空间、新增应用、人员流动,都需要安全策略跟着演进。
我个人习惯每个季度做一次安全项的复查,拉着kube-bench跑一遍、检查一遍RBAC绑定、看一眼NetworkPolicy覆盖情况,花不了半天时间,但能让整体安全水位始终保持在可控范围内。K8s安全机制看着条目多,拆到每个配置层面其实都有清晰的目标:认证确保“你是谁”,授权确保“你能做什么”,准入控制确保“你做的事合规”,网络策略确保“你能访问谁”,数据加密确保“存储层不被裸奔”,运行时防护确保“漏进来的东西跑不远”。把这六层叠起来,生产环境的安全水位就到基准线以上了。