上周,我们生产集群发生了一起典型的“邻居噪音”事件。一个刚入职的开发同学在测试环境部署了一个有内存泄漏的微服务,因为它所在的Namespace没有设置合理的MemoryRequest,导致该Pod直接OOMKill后反复重启,触发了kubelet的驱逐机制,顺带把同节点上几个生产核心业务也挤挂了。这次事故让我们不得不重新审视多租户隔离的每一个细节。
很多团队认为,只要把不同业务分到不同的Namespace,事情就完事了。但这仅仅是开始,甚至可以说是“伪隔离”。真正的多租户架构,需要在计算资源、存储权限、网络访问、安全上下文以及镜像策略等多个层面构建纵深防御。今天,我们就把这个过程中踩过的坑、积累的实战配置,毫无保留地梳理一遍。
深入Namespace:它不仅仅是个文件夹
如果你把Namespace理解为Linux里的文件夹,那你就想简单了。在Kubernetes中,Namespace是实现了名称空间隔离的逻辑边界。默认情况下,不同Namespace之间的资源是完全不可见的。比如,A团队的web-frontend服务无法直接通过服务名访问B团队的database服务,除非通过复杂的Service解析配置或NetworkPolicy打通。
但这种隐式隔离非常脆弱。默认情况下,只要用户拥有某个Namespace的访问权限(比如通过RBAC授权),该用户可以几乎为所欲为。他们没有CPU限制,没有内存限制,可以创建任何类型的资源(Pod、Deployment、StatefulSet、Job等),也可以访问该Namespace下的所有Secret和ConfigMap。这意味着,一个被授权的管理员账号一旦泄露,或者一个恶意用户通过权限提升拿到了Namespace的admin权限,整个Namespace的安全防线就形同虚设。
为了强化Namespace级别的隔离,我们通常会结合PodSecurityAdmission(PSA)或早已弃用的PodSecurityPolicy(PSP)。PSA现在通过Namespace上的注解来强制实施安全策略。比如,我们可以强制要求所有Pod必须运行在非特权模式下,不允许容器以root用户身份运行,禁止主机命名空间或IPC的共享。这能从源头阻断很多高危操作。
另外,对于高敏感度的业务,我们还会引入项目(Project)的概念。虽然Kubernetes官方没有Project这个资源对象,但像OpenShift这样的发行版提供了更细粒度的项目抽象,它本质上是一套封装了Namespace、配额、限制策略和用户角色的综合安全单元。在原生K8s环境中,我们可以通过Operator或自动化脚本模拟这一逻辑,确保每个“项目”都有统一的安全基线。
ResourceQuota:资源限制的“紧箍咒”
资源隔离的核心在于ResourceQuota。这是Kubernetes提供的最基础也是最强大的配额管理工具。它允许你限制一个Namespace内所有资源对象的总资源消耗。比如,你可以规定A团队Namespace的CPU总量不超过16核,内存不超过32Gi。任何试图超出这个上限的创建请求都会被API Server直接拒绝。
然而,ResourceQuota本身存在一个巨大的陷阱:它只管总和,不管个体。
假设我们设置了CPU上限为16核。如果只有一个应用,它可能没问题。但如果有10个应用,每个应用只设置了requests: 100m(即0.1核),那么理论上这10个应用总共只请求了1核,远未触及16核的上限。但如果这些应用实际上都跑满了1核(因为没设置limits),那么总消耗可能会达到10核,甚至更高。更糟糕的情况是,如果某个恶意用户创建了100个Pod,每个Pod只申请100m CPU,虽然总量还在限制内,但调度器可能会因为这些Pod过于碎片化而导致节点资源无法有效利用,或者通过某种方式绕过限制。
因此,单纯依赖ResourceQuota是不够的。我们需要配合LimitRange来设置“默认值”和“默认请求值”。LimitRange可以确保:
- 如果用户在创建Pod时没有指定资源request/limit,系统会自动注入默认值。
- 设置单个容器的资源上限,防止单个Pod占用过多资源。
- 设置单个容器的资源下限,保证业务最低可用资源。
下面是一个典型的配置组合示例,展示了如何在Namespace中同时使用这三者:
apiVersion: v1
kind: Namespace
metadata:
name: team-alpha
labels:
name: team-alpha
env: production
---
# 1. LimitRange: 设置单个容器的资源边界和默认值
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: team-alpha
spec:
limits:
- default: # 如果未指定,默认使用这些值作为limit
cpu: "500m"
memory: "512Mi"
defaultRequest: # 如果未指定,默认使用这些值作为request
cpu: "200m"
memory: "256Mi"
max: # 单个容器的最大值
cpu: "2"
memory: "4Gi"
min: # 单个容器的最小值
cpu: "50m"
memory: "64Mi"
type: Container
---
# 2. ResourceQuota: 限制整个Namespace的总资源
apiVersion: v1
kind: ResourceQuota
metadata:
name: total-quota
namespace: team-alpha
spec:
hard:
requests.cpu: "16" # 总CPU请求上限
requests.memory: "32Gi" # 总内存请求上限
limits.cpu: "32" # 总CPU限制上限
limits.memory: "64Gi" # 总内存限制上限
pods: "50" # 最多允许50个Pod
services: "10" # 最多允许10个Service
secrets: "20" # 最多允许20个Secret
configmaps: "20" # 最多允许20个ConfigMap
---
# 3. 一个典型的Deployment配置,遵循上述限制
apiVersion: apps/v1
kind: Deployment
metadata:
name: critical-app
namespace: team-alpha
spec:
replicas: 3
selector:
matchLabels:
app: critical-app
template:
metadata:
labels:
app: critical-app
spec:
containers:
- name: app
image: team-alpha/critical-app:v1.2
# 注意:这里没有显式指定resources,会使用LimitRange的默认值
# 如果想强制指定,必须遵守LimitRange的max/min范围
resources:
requests:
cpu: "200m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
在这个例子中,我们不仅限制了总量,还确保了每个容器都有一个合理的默认配置,防止用户因为疏忽或恶意行为导致资源耗尽。同时,我们还限制了Pod、Service、Secret等非计算资源的数量,防止通过资源耗尽攻击(Resource Exhaustion Attack)来影响集群稳定性。比如,如果允许无限创建Pod,攻击者可以创建海量小Pod,占满etcd存储或调度器内存。
网络隔离:打破Namespace的“透明墙”
如前所述,不同Namespace之间的网络默认是互通的。在Kubernetes中,Pod可以通过<service-name>.<namespace>.svc.cluster.local直接访问其他Namespace的服务。这对于需要集中化管理和监控的场景是有意义的,但对于多租户环境,这绝对是个安全隐患。
想象一下,团队A的数据库端口暴露在了集群内,团队B的任何一个Pod都可以直接连接并读取数据。这在生产环境中是绝对不可接受的。
解决这个问题的核心工具是NetworkPolicy。它允许你基于标签(Label)定义入站和出站流量的规则。需要注意的是,NetworkPolicy只在支持它的CNI插件(如Calico, Cilium, Weave Net等)环境下生效。如果你的集群使用的是Flannel默认配置,NetworkPolicy可能不起作用,除非你开启了特定的模式或替换了CNI。
一个标准的“零信任”网络策略配置思路是:默认拒绝所有流量,然后只允许必要的通信。
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: team-alpha
spec:
podSelector: {} # 空选择器表示匹配该Namespace下的所有Pod
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: team-alpha
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {} # 允许同Namespace内的所有Pod访问
egress:
- to:
- podSelector: {} # 允许访问同Namespace内的所有Pod
- to:
- namespaceSelector:
matchLabels:
name: kube-system # 允许访问kube-system,用于DNS解析等
这段配置首先创建了一个deny-all-ingress策略,默认拒绝所有入站流量。然后创建一个更细致的策略,允许同Namespace内的Pod互相通信,以及访问kube-system(用于DNS等服务发现)。这样,团队A的Pod就无法访问团队B的任何服务,除非团队B显式地开放了跨Namespace的NetworkPolicy(这通常需要通过额外的RBAC和策略管理来控制)。
此外,对于更细粒度的控制,还可以结合CNI的高级功能,比如Calico的GlobalNetworkPolicy或Cilium的网络策略,实现基于IP、端口、甚至应用层协议(如HTTP方法)的精细化控制。
RBAC:权限的最小化原则
ResourceQuota和NetworkPolicy解决了“能做什么”和“能访问什么”的问题,但RBAC(Role-Based Access Control)解决的是“谁能做”的问题。在多租户环境中,RBAC的配置往往是重灾区。
很多集群管理员习惯于给开发团队授予cluster-admin角色,或者创建自定义Role但过于宽松。这会导致权限滥用。正确的做法是遵循最小权限原则(Principle of Least Privilege)。
首先,每个Namespace应该有一个独立的Admin角色,但这个角色的权限应该仅限于该Namespace内部。例如:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: team-alpha
name: team-alpha-admin
rules:
- apiGroups: ["", "apps", "batch", "extensions"]
resources: ["pods", "deployments", "jobs", "services", "configmaps", "secrets", "pods/log", "pods/status"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources: ["networkpolicies"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: team-alpha
name: team-alpha-admin-binding
subjects:
- kind: Group
name: team-alpha-devs # 假设这是一个在LDAP或内部身份系统中定义的组
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: team-alpha-admin
apiGroup: rbac.authorization.k8s.io
在这个Role中,我们明确列出了允许操作的资源列表和动词。注意,我们没有授予delete某些关键资源如namespace的权限,也没有授予访问集群级别资源(如Nodes, PersistentVolumes)的权限。
更重要的是,要防止权限提升。一个常见的漏洞是允许用户对Secret和ConfigMap进行get操作,但没有限制他们不能读取其他Namespace的Secret。虽然RBAC本身是Namespace级别的,但如果用户有cluster-admin权限,他们就可以跨Namespace访问所有Secret。因此,必须严格限制cluster-admin的使用,仅保留给少数核心运维人员,并通过审计日志监控其操作。
另外,建议使用Pod Security Standards来限制哪些ServiceAccount可以运行特权Pod。可以通过在Role或RoleBinding中引入selfsubjectrulesreview等机制来动态评估权限,或者简单地通过标签和注解来声明安全级别。
安全上下文与镜像策略:最后一道防线
即使前面的资源、网络和权限都配置好了,如果Pod可以随意运行特权容器,或者从不受信任的镜像仓库拉取镜像,整个安全体系依然存在巨大漏洞。
Pod Security Context允许你控制容器的运行身份。在生产环境中,强烈建议强制要求容器以非root用户运行。虽然Linux内核的capabilities机制可以允许非root用户执行某些特权操作,但这增加了配置的复杂性。更简单的做法是直接拒绝运行root用户的Pod。
可以通过PodSecurityAdmission来实现:
apiVersion: v1
kind: Namespace
metadata:
name: team-alpha
labels:
name: team-alpha
pod-security.kubernetes.io/enforce: restricted # 强制执行最严格的安全策略
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
restricted级别是目前最严格的标准,它要求:
- 容器不能以root用户运行。
- 不能提升capabilities。
- 不能使用hostPath卷。
- 不能使用hostNetwork、hostPID或hostIPC。
- 必须设置
readOnlyRootFilesystem为true。 - 必须设置
allowPrivilegeEscalation为false。
如果某个Pod违反了这些规则,API Server会直接拒绝创建请求。
对于镜像来源,ImagePolicyWebhook或Kyverno/OPA Gatekeeper是更好的选择。它们允许你在 admission control 阶段对镜像进行验证。例如,可以强制要求所有镜像必须来自内部受信任的私有仓库,禁止使用latest标签,或者禁止使用包含已知漏洞的镜像版本。
下面是一个使用Kyverno的策略示例,强制镜像必须来自内部仓库:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-internal-registry
spec:
validationFailureAction: Enforce
background: false
rules:
- name: require-internal-registry
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Images must be from the internal registry (registry.example.com)."
pattern:
spec:
containers:
- image: "registry.example.com/*"
这个策略会拦截所有来自非内部仓库的镜像拉取请求,从而防止镜像供应链攻击。
存储隔离:让数据各归其位
存储隔离往往被忽视,但它同样重要。不同租户可能需要不同的存储性能等级(如SSD vs HDD),或者需要数据在逻辑上完全隔离。
Kubernetes通过StorageClass和PersistentVolume(PV)/PersistentVolumeClaim(PVC)来实现存储抽象。你可以为每个租户创建独立的StorageClass,指向不同的后端存储池。例如,团队A使用高性能SSD存储,团队B使用低成本HDD存储。
更重要的是,要实现存储数据的逻辑隔离。虽然PVC在技术上可以被任何有权限的Pod访问,但可以通过VolumeSnapshot和StorageQuota来加强管理。此外,对于数据库等敏感数据,建议在应用层实现加密,而不是仅仅依赖存储层的隔离。
还可以通过CSI(Container Storage Interface)插件的特性,实现更细粒度的存储访问控制。例如,某些CSI驱动支持基于节点的选择器,确保数据只存储在特定的节点上,从而物理隔离数据。
监控与审计:看见才能管理
最后,没有监控和审计的安全配置是盲目的。在生产环境中,必须启用详细的审计日志,记录所有API请求,包括谁、在什么时候、做了什么操作。Kubernetes的审计日志可以发送到外部系统(如Elasticsearch、Splunk)进行长期存储和分析。
同时,部署Prometheus等监控工具,实时监控各Namespace的资源使用情况。设置告警规则,当某个Namespace的资源使用接近配额上限时,立即通知相关人员。这不仅有助于发现问题,还能在资源被恶意耗尽前进行干预。
此外,建议定期进行安全扫描,使用工具如Trivy扫描镜像漏洞,使用kube-bench检查集群配置是否符合CIS Kubernetes Benchmark标准。
总结:安全是一个持续的过程
多租户Kubernetes集群的安全隔离不是一个一劳永逸的配置任务,而是一个持续的过程。从Namespace的规划、ResourceQuota和LimitRange的精细配置,到NetworkPolicy的网络隔离、RBAC的权限最小化,再到Pod Security和镜像策略的最后防线,每一步都需要精心设计。
在实际操作中,我们建议采用“纵深防御”策略,层层叠加安全措施。同时,保持对最新安全漏洞和Kubernetes版本更新的关注,及时调整配置。毕竟,在云端,安全不是功能,而是基础。希望这些实战经验能帮助大家在构建多租户集群时少走弯路,打造出既灵活又安全的云原生环境。