想象一下,你是一家初创公司的运维负责人,手里只有一台“超级计算机”(也就是你的 Kubernetes 集群),但你们公司有五个不同的业务线:前端、后端、数据、算法实验、还有那个总是乱跑测试脚本的实习生团队。如果大家都挤在一起,那场面绝对混乱——算法团队跑个深度学习模型,内存占满,直接把前端的 Pod 给挤挂了;实习生一激动,kubectl delete --all 敲下去,整个生产环境瞬间清零。
这就是多租户场景下的真实噩梦。今天,我们不谈那些干巴巴的理论,而是像老程序员在代码review会上那样,手把手带你把这套“隔离+限制+安全”的组合拳打到位。我们要把 Kubernetes 从一个“大杂烩”变成一个井井有条的“高级公寓”。
第一层防线:Namespace——给每家公司发一个“虚拟公寓房”
很多新手有个误区,觉得 Namespace 只是给资源起个名字的前缀,错了。Namespace 的本质是逻辑隔离边界。在 Kubernetes 里,绝大多数资源都是 Namespace-scoped(命名空间相关)的,这意味着 A 团队的 Pod 根本看不见 B 团队的 Service,除非你们显式地搞跨域访问。
咱们先看怎么优雅地创建租户空间。别再用 kubectl create namespace dev 这种裸奔的方式了,我们要写 YAML,因为基础设施即代码(IaC)才是专业选手的标配。
apiVersion: v1
kind: Namespace
metadata:
name: frontend-team
labels:
team: frontend
env: prod
isolation-level: strict # 给运维看的标签,方便后续策略匹配
spec:
finalizers:
- kubernetes # 确保清理彻底,防止误删
---
apiVersion: v1
kind: Namespace
metadata:
name: data-team
labels:
team: data
env: dev
isolation-level: medium
你看,这里加 finalizers 和 labels 是有深意的。finalizers 能防止运维手抖直接把 Namespace 删了导致资源残留;而 labels 则是给后续的 RBAC(权限控制)和 NetworkPolicy(网络策略)用的“身份证”。
但是,光有 Namespace 就够了吗?远远不够。这就好比给你发了个房间,但没给你锁,也没限制你用电,隔壁老王还能随意串门。这时候,RBAC 必须跟上。
第二层防线:RBAC——严格把控“门禁卡”和“活动范围”
在 Kubernetes 中,身份认证(是谁)和授权(能干什么)是分开的。多租户的核心就是:前端团队只能操作 frontend-team 命名空间里的资源,绝对碰不到 data-team 的一根毛。
我们来配置一个针对 frontend-team 的严格角色。这个角色允许他们管理 Deployment、Pod、Service,但严禁访问 Secret(密码)、ConfigMap(配置)和 PersistentVolumeClaims(存储)。
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: frontend-team
name: frontend-developer-role
rules:
# 允许操作核心工作负载
- apiGroups: [""]
resources: ["pods", "services", "replicationcontrollers"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# 严禁访问敏感资源
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: [] # 空列表表示拒绝
接下来,把这个角色绑定给前端团队的用户组(假设你们用 LDAP 或 OIDC 对接,用户组叫 frontend-devs):
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: frontend-dev-bind
namespace: frontend-team
subjects:
- kind: Group
name: frontend-devs # 对应你们的身份认证系统里的用户组
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: frontend-developer-role
apiGroup: rbac.authorization.k8s.io
这里有个极其重要的细节:注意 roleRef 和 subjects 里的名字要和上面完全一致。很多坑都出在这里——比如把 Group 写成了 User,或者命名空间搞错了。你可以用这个命令验证权限是否生效:
# 以前端开发者的身份(或者用 serviceaccount 模拟)测试权限
kubectl auth can-i create deployments --namespace frontend-team
kubectl auth can-i create deployments --namespace data-team
第一个命令应该返回 yes,第二个返回 no。这才是真正的隔离。
第三层防线:ResourceQuota & LimitRange——防止“大户人家”饿死“小老百姓”
就算 RBAC 把门关好了,如果 frontend-team 里某个开发人员不小心写了一个死循环,或者启动了一个内存泄漏的怪物,把整个 Namespace 的资源吃光,同 Namespace 下的其他合法 Pod 怎么办?Kubernetes 的调度器是按 Node 来调度的,但如果一个 Namespace 的资源配额没设限,它可以在集群里肆意妄为。
这时候,我们需要两层限制:ResourceQuota(总额控制)和 LimitRange(单个容器限制)。
想象一下,公司每个月给每个部门 10 万块预算(ResourceQuota),同时又规定每笔报销不能超过 5000 块(LimitRange)。这样既控制了总量,又防止了单笔挥霍。
apiVersion: v1
kind: ResourceQuota
metadata:
name: frontend-quota
namespace: frontend-team
spec:
hard:
# CPU 限制:总共 8 核
requests.cpu: "8"
limits.cpu: "16"
# 内存限制:总共 32Gi
requests.memory: 32Gi
limits.memory: 64Gi
# Pod 数量限制:最多跑 50 个 Pod,防止启动大量容器拖垮节点
pods: "50"
# 服务数量限制
services: "20"
# 禁用 NodePort 类型服务,防止占用宿主机端口引发冲突
services.nodeport: "0"
---
apiVersion: v1
kind: LimitRange
metadata:
name: frontend-limits
namespace: frontend-team
spec:
limits:
# 默认限制:如果用户没指定 resources,就自动套用这个
- type: Container
default:
cpu: "500m"
memory: 512Mi
defaultRequest:
cpu: "250m"
memory: 256Mi
# 最大值:单个容器不能超过这个
max:
cpu: "4"
memory: 8Gi
# 最小值:单个容器不能低于这个
min:
cpu: "100m"
memory: 64Mi
这段配置里有几个实战中的血泪教训:
requestsvslimits:requests是调度器判断把 Pod 放在哪个节点时的依据(保证够用的最小资源),limits是容器实际能用的最大上限(超出会被限流或 Kill)。一定要分开设置,否则节点压力不可控。services.nodeport: "0":这是一个非常实用的安全技巧。NodePort 会占用宿主机的物理端口,一旦前端团队开了一个 NodePort 占用了 30000-32767 之间的端口,其他团队的 Pod 就没法用了,甚至引发端口冲突。直接禁止 NodePort 是最安全的做法,对外暴露统一用 Ingress。- Default Limit:如果你不设置 default,开发者忘了写 resource 字段,Pod 可能拿到无限资源,导致其他 Pod 饿死。有了 LimitRange,K8s 会自动帮他们填上默认值。
第四层防线:SecurityContext——给容器穿上一层“防弹衣”
资源限制管的是“量”,SecurityContext 管的是“质”。很多安全漏洞源于容器以 root 用户运行,一旦容器被攻破,攻击者就能拥有宿主机的 root 权限。
在多租户环境下,这是底线问题。我们必须强制所有 Pod 以非 root 用户运行,并且禁止容器拥有特权(privileged)。
apiVersion: v1
kind: Pod
metadata:
name: secure-app
namespace: frontend-team
spec:
# 安全上下文:应用到 Pod 下所有容器
securityContext:
runAsNonRoot: true # 强制非 root
runAsUser: 1000 # 指定具体 UID,避免使用默认 root
runAsGroup: 1000
fsGroup: 2000 # 文件系统的 group,用于共享卷权限
seccompProfile: # 启用 Seccomp 过滤,限制系统调用
type: RuntimeDefault
containers:
- name: app
image: nginx:latest
securityContext:
allowPrivilegeEscalation: false # 禁止提权
readOnlyRootFilesystem: true # 根文件系统只读,防止写入恶意文件
capabilities:
drop: ["ALL"] # 丢弃所有 Linux 能力,只留必要的
add: ["NET_BIND_SERVICE"] # 如果需要监听 80 端口,补这一个
这里重点讲讲 Seccomp 和 Capabilities。
- Seccomp Profile: RuntimeDefault:这是 Kubernetes 1.19+ 推荐的默认配置。它会拦截掉绝大多数不需要的系统调用(比如
reboot,swapon等),大大减少攻击面。如果你的应用比较老,可能需要手动调整 profile,但在多租户场景下,默认值通常是安全的。 - Capabilities drop ALL:Linux 的能力(Capabilities)机制是把 root 权限拆散。默认情况下,容器有一些能力(如
NET_BIND_SERVICE允许绑定 80 端口)。为了安全,我们把所有能力都扔掉,只补上应用真正需要的那个。这能防止攻击者利用 Linux 内核的能力机制进行提权。
第五层防线:NetworkPolicy——零信任网络的“防火墙规则”
这是多租户隔离中最容易被忽视,也最关键的一环。前面四步虽然做得很好,但如果没人管网络,frontend-team 的 Pod 仍然可以 ping 通 data-team 的数据库,甚至横向移动到 control plane。
Kubernetes 默认的网络模型是所有 Pod 互通。你需要引入 NetworkPolicy 来实现“默认拒绝,白名单放行”。
假设我们有一个微服务架构:
frontend-svc:公开服务,允许来自互联网(通过 Ingress Controller)和backend-svc的流量。backend-svc:内部服务,只允许被frontend-svc访问,严禁访问数据库。database-svc:核心数据,只允许被backend-svc访问,严禁被其他任何 Pod 访问。
我们来看具体的 NetworkPolicy 配置:
1. 后端服务(Backend)—— 最严格的中转站
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
namespace: frontend-team # 假设都在同一个租户内,或者跨租户时去掉 namespace 限制
spec:
podSelector:
matchLabels:
app: backend-svc
policyTypes:
- Ingress
- Egress
ingress:
# 只允许来自 frontend-svc 的流量
- from:
- podSelector:
matchLabels:
app: frontend-svc
ports:
- protocol: TCP
port: 8080
egress:
# 只允许访问 database-svc 的 5432 端口,以及必须的 DNS(UDP 53)
- to:
- podSelector:
matchLabels:
app: database-svc
ports:
- protocol: TCP
port: 5432
- to:
- namespaceSelector: {} # 允许访问 kube-system 命名空间(用于 DNS 解析)
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
2. 数据库(Database)—— 铁公鸡,谁都不让进
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: database-policy
namespace: data-team
spec:
podSelector:
matchLabels:
app: postgres-db
policyTypes:
- Ingress
ingress:
# 只允许来自 frontend-team 的 backend-svc 访问
- from:
- namespaceSelector:
matchLabels:
team: frontend # 通过标签选择跨命名空间的源
podSelector:
matchLabels:
app: backend-svc
ports:
- protocol: TCP
port: 5432
这里展示了跨命名空间的隔离能力。注意 namespaceSelector 和 podSelector 的组合使用。namespaceSelector: {} 表示匹配所有命名空间(如果不加这个,默认只匹配当前命名空间)。通过标签匹配,我们可以精确控制哪个团队的哪个服务可以访问我的数据库。
实战建议:在生产环境中,建议为每个命名空间都创建一个默认的“Deny All”策略,防止新部署的服务默认开放网络。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: frontend-team
spec:
podSelector: {} # 匹配所有 Pod
policyTypes:
- Ingress
- Egress
这就好比公寓楼的每层门都锁上了,只有持有特定钥匙(符合 Label 的 Pod)的人才能进入。
进阶:PodDisruptionBudget——守护“可用性”的最后一道门
隔离做完了,资源限制了,网络控了,但还有一个问题:如果运维人员执行 kubectl drain 或者节点自动缩容,Pod 被意外删除,业务怎么办?
PodDisruptionBudget (PDB) 允许你定义“最少要保留几个 Pod 存活”,防止人为或自动操作导致服务不可用。
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: frontend-pdb
namespace: frontend-team
spec:
minAvailable: 2 # 无论发生什么,至少要有 2 个 Pod 活着
selector:
matchLabels:
app: frontend-svc
或者用 maxUnavailable: 1(最多允许 1 个 Pod 不可用)。对于多租户场景,确保每个核心业务都有 PDB,是专业运维的体现。
总结:一套完整的“隔离与安全”思维模型
回顾一下,我们从 Namespace 开始,层层递进:
- Namespace:划定边界,物理/逻辑隔离。
- RBAC:定义权限,谁能做什么,不能做什么。
- ResourceQuota & LimitRange:控制用量,防止资源争抢和单点膨胀。
- SecurityContext:加固容器,防止提权和逃逸。
- NetworkPolicy:网络微隔离,实现零信任架构。
- PodDisruptionBudget:保障可用性,防止非预期中断。
这六层构成了 Kubernetes 多租户隔离的完整闭环。记住,没有任何一层是银弹,必须组合使用才能构建真正安全、稳定、可控的多租户集群。
最后,给你一个检查清单,每次新团队入驻集群前,对着这个清单过一遍:
- [ ] Namespace 创建完毕,并打上了团队标签?
- [ ] RBAC Role 和 RoleBinding 是否只授予了最小必要权限?
- [ ] ResourceQuota 是否覆盖了 CPU、内存、Pod 数量和服务数量?
- [ ] LimitRange 是否设置了默认资源和上下限?
- [ ] 所有 Pod 是否配置了 SecurityContext(非 root、只读文件系统、丢弃所有 capabilities)?
- [ ] 是否部署了默认的 Deny All NetworkPolicy?
- [ ] 关键服务是否配置了 NetworkPolicy 白名单?
- [ ] 核心服务是否配置了 PDB?
当你把这些都落实到位,你的 Kubernetes 集群就不再是一个“高风险的共享池”,而是一个让各个团队可以放心交付、安心运维的“安全堡垒”。这就是现代云原生架构下,运维工程师应有的专业素养。