Kubernetes 基础体系 · 第 51/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes 多租户:Namespace、RBAC、NetworkPolicy、Quota 和隔离极限

Kubernetes 多租户(multi-tenancy)不是一个单独的 API,而是把多个控制面和数据面机制组合起来,使多个团队、项目、客户或环境共享一个集群时,能够分别控制:

  • 资源作用域:对象属于哪个租户;
  • 谁能做什么:用户和工作负载可以访问哪些 API;
  • 谁能和谁通信:Pod 之间以及 Pod 到外部的网络流量;
  • 最多能消耗多少资源:CPU、内存、存储、对象数量;
  • 隔离失败时的影响范围:一个租户是否能影响其他租户或整个集群。

Namespace、RBAC、NetworkPolicy 和 ResourceQuota 分别解决不同问题。它们叠加后可以形成较好的“软多租户”边界,但通常不能单独提供强安全边界,更不能自动抵御内核漏洞、容器逃逸或恶意集群管理员。


一、先建立正确的多租户模型

设一个共享集群中有租户集合:

T={t1,t2,,tn}T = \{t_1, t_2, \ldots, t_n\}

对每个租户 tit_i,我们通常希望满足四类条件:

  1. 对象作用域隔离

    租户 tit_i 只能管理自己的 namespaced 对象:

    manage(ti,oj)=truenamespace(oj)=Ni\text{manage}(t_i, o_j) = \text{true} \Rightarrow \text{namespace}(o_j) = N_i

  2. 权限隔离

    租户 tit_i 不能执行未授权的 API 动作:

    allow(u,v,r,a,namespace)=RBACMatch(u,v,r,a,namespace)\text{allow}(u, v, r, a, \text{namespace}) = \text{RBACMatch}(u, v, r, a, \text{namespace})

    其中 uu 是主体,vv 是动词,rr 是资源,aa 是 API 组和资源名称。

  3. 网络隔离

    未被允许的 Pod 到 Pod 或 Pod 到外部流量应被拒绝:

    traffic(ps,pd)AllowedPolicies(pd)\text{traffic}(p_s, p_d) \in \text{AllowedPolicies}(p_d)

  4. 资源预算

    租户的实际使用量不得超过预算:

    usageti(r)hardti(r)\text{usage}_{t_i}(r) \leq \text{hard}_{t_i}(r)

这几个条件并不等价:

  • Namespace 能划分对象作用域,但不决定网络;
  • RBAC 能阻止 API 操作,但不阻止已经运行的 Pod 访问网络;
  • NetworkPolicy 能限制网络,但不阻止租户读取 Kubernetes API;
  • ResourceQuota 能限制对象创建和资源申请,但不防止权限越权;
  • 即使四者都配置正确,具有特权的 Pod 仍可能访问节点或破坏更高层边界。

因此,多租户设计首先应明确隔离目标:

隔离目标 主要机制 仍然依赖
团队不能修改其他团队的 Deployment Namespace + RBAC 管理员不授予越权 ClusterRole
Pod 不能跨租户访问 NetworkPolicy CNI 正确实现并执行策略
单个团队不能耗尽集群资源 ResourceQuota + LimitRange 调度器、节点容量和配额配置
租户不能创建特权容器 Pod Security Admission 等 Admission 配置和策略治理
对抗恶意租户或容器逃逸 独立集群、虚拟机、硬件和运行时边界 云平台或基础设施隔离

二、Namespace:对象作用域,不是完整安全边界

2.1 Namespace 解决什么问题

Namespace 是 Kubernetes 中对一组 namespaced 资源进行逻辑划分的对象。常见 namespaced 资源包括:

  • Pod;
  • Deployment、StatefulSet、DaemonSet;
  • Service、Ingress;
  • ConfigMap、Secret;
  • ServiceAccount;
  • Role、RoleBinding;
  • ResourceQuota、LimitRange;
  • NetworkPolicy;
  • PVC。

创建一个 Namespace:

apiVersion: v1
kind: Namespace
metadata:
  name: tenant-a
  labels:
    tenant.example.com/id: tenant-a
kubectl apply -f namespace.yaml
kubectl get namespace tenant-a --show-labels

Namespace 的核心效果是:API 请求中,许多资源需要携带 namespace,资源名称只需在该 Namespace 内唯一。

例如,tenant-atenant-b 都可以各自创建名为 api 的 Deployment:

tenant-a/api
tenant-b/api

它们不是同一个对象,因为完整标识包含:

(apiVersion,kind,namespace,name)(\text{apiVersion}, \text{kind}, \text{namespace}, \text{name})

但 Namespace 不是所有资源的作用域。以下对象是典型的集群级资源:

  • Node;
  • Namespace 本身;
  • PersistentVolume;
  • StorageClass;
  • ClusterRole;
  • ClusterRoleBinding;
  • CustomResourceDefinition;
  • Webhook 配置;
  • APIService。

因此,“给租户一个 Namespace”并不等于“给租户集群管理员权限”。如果给租户绑定了能够读取或修改集群级资源的 ClusterRole,Namespace 作用域就无法挽救这个越权授权。

2.2 Namespace 对 RBAC 的含义

Role 是 namespaced 对象,通常描述某个 Namespace 内的权限:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tenant-developer
  namespace: tenant-a
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "deployments/scale"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods", "services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

Role 只存在于 tenant-a。即使 Role 中写了 pods,它也只能匹配 tenant-a 中的 Pod,不能匹配 tenant-b 中的 Pod。

但是,ClusterRole 可以被 RoleBinding 引用:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-developer-binding
  namespace: tenant-a
subjects:
  - kind: Group
    name: tenant-a-developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: tenant-developer
  apiGroup: rbac.authorization.k8s.io

也可以引用 ClusterRole:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-view-binding
  namespace: tenant-a
subjects:
  - kind: Group
    name: tenant-a-developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: view
  apiGroup: rbac.authorization.k8s.io

这里的关键点是:

  • RoleBinding 本身位于 tenant-a
  • 它把引用的 ClusterRole 权限限制在 tenant-a
  • 这和 ClusterRoleBinding 不同;
  • 如果改成 ClusterRoleBinding,权限通常会作用于整个集群,风险显著扩大。

2.3 Namespace 标签也是安全输入

NetworkPolicy 可以根据 Namespace 标签选择通信来源,因此 Namespace 标签不是普通装饰信息。

例如:

namespaceSelector:
  matchLabels:
    tenant.example.com/id: tenant-a

如果一个租户能够修改 Namespace 标签,它可能把自己的 Namespace 改成受信任标签,从而满足其他租户 NetworkPolicy 的选择条件。因此:

  • 租户开发者通常不应拥有 update namespaces
  • Namespace 创建和标签变更应由平台控制器或管理员完成;
  • 不能把“标签值正确”当成不可伪造的身份认证,除非有专门的准入策略保护它。

2.4 Namespace 删除是异步过程

删除 Namespace 不是立即删除目录,而是一个控制器驱动的清理流程:

kubectl delete namespace tenant-a
kubectl get namespace tenant-a

常见状态变化是:

Active -> Terminating -> 删除完成

Terminating 期间,Namespace 控制器会发现并清理其中的 namespaced 对象。若某个对象或 API 资源无法删除,Namespace 可能长时间停留在 Terminating

常见原因包括:

  • 对象带有无法执行的 finalizer;
  • 某个 CRD 被删除后,剩余自定义资源无法被正确发现;
  • 某个聚合 API 服务不可用;
  • 控制器或 webhook 阻塞了删除请求。

排查:

kubectl describe namespace tenant-a
kubectl get namespace tenant-a -o json
kubectl api-resources --verbs=list --namespaced -o name

强制移除 Namespace finalizer 是高风险操作。它可能使 Namespace 对象消失,但遗留资源、外部云资源或控制器状态未必已经清理。生产环境应先确定阻塞者和残留资源,再决定是否处理 finalizer,而不是把强制 finalize 当作常规删除命令。


三、RBAC:控制 Kubernetes API,不控制所有运行时行为

3.1 RBAC 的授权输入

Kubernetes 请求通常经过以下逻辑:

flowchart LR
    A[客户端或 Pod] --> B[认证 Authentication]
    B --> C[授权 Authorization]
    C --> D[准入 Admission]
    D --> E[API Server 持久化]
    E --> F[控制器/调度器/节点执行]

各阶段职责不同:

  • 认证:请求者是谁,例如用户、组、ServiceAccount;
  • 授权:请求者能否对某个资源执行某个动作;
  • 准入:对象是否满足额外策略,例如 Pod Security、ResourceQuota、ValidatingAdmissionPolicy;
  • 持久化和执行:API Server 保存对象,控制器和 kubelet 进一步执行。

RBAC 授权规则通常由以下字段匹配:

  • apiGroups:API 组,核心 API 组写成 ""
  • resources:资源名称,例如 podssecrets
  • resourceNames:可选,限定具体对象名;
  • verbs:动作,例如 getlistwatchcreateupdatepatchdelete
  • 作用域:Role 绑定到某个 Namespace,ClusterRoleBinding 则可作用于集群范围。

RBAC 默认是“无匹配即拒绝”,但授权规则是累加的,没有显式 deny 规则。用户通过多个 RoleBinding 获得的权限会并集化。

3.2 最小权限示例

为租户创建一个只能管理自己 Namespace 中应用对象的角色:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tenant-app-editor
  namespace: tenant-a
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "deployments/scale", "replicasets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods", "services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]

将权限绑定给用户组:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-app-editor-binding
  namespace: tenant-a
subjects:
  - kind: Group
    name: tenant-a-developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: tenant-app-editor
  apiGroup: rbac.authorization.k8s.io

应用和验证:

kubectl apply -f tenant-a-rbac.yaml

kubectl auth can-i create deployments \
  --namespace tenant-a \
  --as alice \
  --as-group tenant-a-developers

# 预期:yes

kubectl auth can-i get secrets \
  --namespace tenant-a \
  --as alice \
  --as-group tenant-a-developers

# 预期:no

kubectl auth can-i delete deployments \
  --namespace tenant-b \
  --as alice \
  --as-group tenant-a-developers

# 预期:no

kubectl auth can-i 只验证授权判断,不会替代真实 API 请求验证,也不检查后续 Admission、Webhook、ResourceQuota 或 NetworkPolicy。

3.3 Secret 权限需要单独审慎处理

能够读取 Secret 的主体通常可以获得应用凭据、数据库密码、TLS 私钥或云平台访问令牌。因此不应因为“用户能查看 Pod”就顺便授予:

resources: ["secrets"]
verbs: ["get", "list", "watch"]

还要注意,Pod 的创建权限本身可能产生间接能力。例如,若用户可以在某个 Namespace 创建 Pod,并能引用该 Namespace 中的 Secret,那么即使用户没有直接 get secret 权限,工作负载也可能把 Secret 读取出来并暴露给用户。更危险的是,如果用户可以创建带任意 ServiceAccount 的 Pod,就可能继承该 ServiceAccount 的权限。

因此,评估 RBAC 时不能只查看显式的 secrets/get,还要检查:

  • 是否能创建 Pod;
  • 是否能指定任意 serviceAccountName
  • 是否能创建 RoleBinding;
  • 是否能修改现有高权限 Pod 或 Deployment;
  • 是否能创建能访问节点或云元数据的工作负载。

3.4 不能把“只读”理解成“低风险”

内置 vieweditadmin ClusterRole 方便快速授权,但它们适合经过审查后使用,不应自动等价为租户模型。

例如:

  • 能读取 ConfigMap 可能获取配置中的敏感信息;
  • 能创建 Deployment 通常意味着能运行任意镜像;
  • 能修改 Deployment 的 Pod 模板,通常意味着能指定任意容器参数;
  • 能管理 RoleBinding 可能把自己提升为管理员;
  • 能读取某些自定义资源,可能泄露租户信息。

RBAC 权限应按工作流拆分,而不是仅以“开发者”“运维者”这种职位名称授权。


四、ResourceQuota 和 LimitRange:预算与默认值是两件事

4.1 ResourceQuota 的语义

ResourceQuota 是 Namespace 级别的配额对象。它在 API 请求的准入阶段检查该 Namespace 的资源使用量和对象数量。

一个基础配额:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-a-quota
  namespace: tenant-a
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    requests.storage: 100Gi
    persistentvolumeclaims: "10"
    pods: "50"
    services: "20"
    services.loadbalancers: "2"

查看配额和当前使用量:

kubectl apply -f quota.yaml
kubectl get resourcequota -n tenant-a
kubectl describe resourcequota tenant-a-quota -n tenant-a

典型结果类似:

Resource            Used   Hard
--------            ----   ----
limits.cpu          2      8
limits.memory       4Gi    16Gi
pods                3      50
requests.cpu        1      4
requests.memory     2Gi    8Gi
requests.storage    20Gi   100Gi

其中:

  • Used 是当前被配额统计的使用量;
  • Hard 是上限;
  • requests.cpurequests.memory 统计调度请求;
  • limits.cpulimits.memory 统计容器限制;
  • podsservices 等统计对象数量;
  • requests.storage 通常统计 PVC 的存储请求,而不是实际磁盘写入量。

4.2 配额的计算过程

假设 Namespace 中有两个 Pod:

Pod p1:
  requests.cpu = 1
  limits.cpu   = 2

Pod p2:
  requests.cpu = 2
  limits.cpu   = 3

则:

used(requests.cpu)=1+2=3\text{used(requests.cpu)} = 1 + 2 = 3

used(limits.cpu)=2+3=5\text{used(limits.cpu)} = 2 + 3 = 5

若配额为:

requests.cpu = 4
limits.cpu   = 8

再创建一个:

requests.cpu = 2
limits.cpu   = 2

API Server 会计算预计值:

3+2=5>43 + 2 = 5 > 4

因此创建请求被拒绝。此时 Pod 不会进入 Pending;它通常根本不会被持久化,客户端会收到类似:

exceeded quota: tenant-a-quota, requested: requests.cpu=2,
used: requests.cpu=3, limited: requests.cpu=4

这是“准入拒绝”,不是调度失败。

对比而言,如果配额允许,但集群没有足够可调度节点,Pod 可能创建成功后处于 Pending。两者诊断路径不同:

kubectl get pod -n tenant-a
kubectl describe pod <pod-name> -n tenant-a
  • 出现 exceeded quota:检查 ResourceQuota;
  • 出现 Insufficient cpuInsufficient memory:检查调度和节点资源;
  • 出现镜像拉取失败:检查 Registry、凭据和网络。

4.3 配额不等于节点预留

如果租户配额为:

requests.cpu = 4

它表示该 Namespace 最多可以声明 4 个 CPU 的调度请求,不表示集群已经为它预留了 4 个 CPU。

因此:

  • 配额控制的是允许提交的总量;
  • requests 影响调度器的放置决策;
  • 节点是否有可用容量仍由调度器决定;
  • 多个 Namespace 的 requests 可能共同竞争同一批节点。

如果需要保证租户的最低资源或避免租户之间的节点级竞争,通常还要组合:

  • 节点池或专用节点;
  • taint/toleration;
  • node affinity;
  • PriorityClass;
  • 集群级调度策略;
  • 云平台或虚拟机隔离。

ResourceQuota 本身不会给租户提供 CPU 性能保证,也不会阻止节点内核资源争用。

4.4 LimitRange 的职责:默认值和单对象约束

LimitRange 作用于 Namespace 内的单个 Pod、容器或 PVC,可以设置默认 requests/limits,也可以设置最小和最大值:

apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-a-limits
  namespace: tenant-a
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      default:
        cpu: 500m
        memory: 512Mi
      min:
        cpu: 10m
        memory: 16Mi
      max:
        cpu: "2"
        memory: 4Gi

对于一个没有资源字段的容器:

containers:
  - name: app
    image: example/app:1.0

如果 LimitRange 设置了默认值,准入阶段可能把它补成:

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi

于是它既能被调度器合理处理,也能满足基于 requests.*limits.* 的配额统计。

两者的关系可以形式化为:

  • LimitRange 作用于单个对象:

    ccontainers(p):min(rc)rcmax(rc)\forall c \in \text{containers}(p): \quad \text{min}(r_c) \leq r_c \leq \text{max}(r_c)

  • ResourceQuota 作用于 Namespace 总量:

    pNcprcquota(N)\sum_{p \in N}\sum_{c \in p} r_c \leq \text{quota}(N)

因此:

  • LimitRange 防止单个容器过大或没有默认资源;
  • ResourceQuota 防止整个 Namespace 累积过多资源;
  • LimitRange 不能限制租户创建多少个 Pod;
  • ResourceQuota 不能把单个容器限制在合理范围内。

4.5 ResourceQuota 和 LimitRange 的顺序影响错误表现

假设配额包含:

hard:
  requests.cpu: "4"

但 Namespace 没有 LimitRange,用户创建:

containers:
  - name: app
    image: example/app:1.0

在需要 requests.cpu 才能纳入配额统计的场景中,请求可能被拒绝,错误通常要求显式声明 CPU request。配置 LimitRange 后,默认 request 被注入,之后才进行配额检查。

所以一个可运行的租户 Namespace 通常至少需要:

  1. ResourceQuota;
  2. LimitRange;
  3. 明确要求工作负载声明 requests 和 limits,或提供可靠默认值。

默认值不能随意设置。若默认 requests.memory 过大,小型服务会快速消耗配额;若默认值过小,调度、QoS 和 OOM 风险又会变差。


五、NetworkPolicy:基于 Pod 的 L3/L4 流量允许模型

5.1 NetworkPolicy 的核心对象

NetworkPolicy 使用 podSelector 选择被保护的 Pod,并分别描述:

  • policyTypes: [Ingress]:入站策略;
  • policyTypes: [Egress]:出站策略;
  • ingress:哪些来源可以连接到该 Pod;
  • egress:该 Pod 可以连接到哪些目的地;
  • ports:允许的协议和端口。

一个重要规则是:同一方向的多个 NetworkPolicy 是累加允许关系,不是最后一条覆盖前一条。

对某个 Pod pp

AllowedIngress(p)=P selects pIngressRules(P)\text{AllowedIngress}(p) = \bigcup_{P \text{ selects } p} \text{IngressRules}(P)

Egress 也同理。NetworkPolicy 没有通用的显式 deny 规则;要实现默认拒绝,通常要创建选择所有 Pod、但没有 allow 条目的策略。

5.2 默认拒绝和同 Namespace 访问

以下策略拒绝 tenant-a 中所有 Pod 的入站和出站流量:

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

podSelector: {} 表示选择该 Namespace 中的所有 Pod。

然后允许同一 Namespace 内的 Pod 互相访问:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}
  egress:
    - to:
        - podSelector: {}

这里必须分别写 ingressegress。只允许入站并不会自动允许出站,反之亦然。

5.3 NamespaceSelector 与 PodSelector 的组合语义

下面的规则只允许来自 tenant-a 中标签为 role: frontend 的 Pod:

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            tenant.example.com/id: tenant-a
        podSelector:
          matchLabels:
            role: frontend

namespaceSelectorpodSelector 写在同一个数组元素中,语义是逻辑 AND:

namespace matchespod matches\text{namespace matches} \land \text{pod matches}

如果拆成两个元素:

from:
  - namespaceSelector:
      matchLabels:
        tenant.example.com/id: tenant-a
  - podSelector:
      matchLabels:
        role: frontend

语义变成逻辑 OR:

namespace matchespod matches\text{namespace matches} \lor \text{pod matches}

后者会放宽范围,可能允许其他 Namespace 中同样带有 role: frontend 的 Pod,或者允许当前 Namespace 中所有 Pod。这个缩进和列表层级是 NetworkPolicy 中最容易造成安全误配的地方之一。

5.4 一个可运行的前后端示例

假设 tenant-a 有:

  • frontend Pod,标签 app: frontend
  • api Pod,标签 app: api
  • CoreDNS 位于 kube-system,DNS Pod 标签假设为 k8s-app: kube-dns

允许 frontend 访问 api 的 8080 端口:

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

允许 api 访问 DNS:

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

使用前必须检查实际 DNS 标签:

kubectl get pods -n kube-system --show-labels
kubectl get svc -n kube-system

不同发行版、云厂商或集群安装方式可能使用不同的 DNS 标签和 Namespace。NetworkPolicy 只能引用实际存在的标签,不能假设所有集群都使用相同标签。

验证:

kubectl run frontend \
  -n tenant-a \
  --image=curlimages/curl \
  --labels=app=frontend \
  -- sleep 3600

kubectl run outsider \
  -n tenant-b \
  --image=curlimages/curl \
  -- sleep 3600

kubectl exec -n tenant-a frontend -- \
  curl --connect-timeout 3 http://api.tenant-a.svc.cluster.local:8080

kubectl exec -n tenant-b outsider -- \
  curl --connect-timeout 3 http://api.tenant-a.svc.cluster.local:8080

预期是前者允许、后者超时或被拒绝。但能否真正得到这个结果,取决于 CNI 是否实现 NetworkPolicy,以及 Service、DNS 和网络路径是否符合该实现。

5.5 NetworkPolicy 的前置条件和边界

Kubernetes API 定义 NetworkPolicy 对象,但实际流量拦截由网络插件实现。若 CNI 不支持 NetworkPolicy,策略对象可能能够创建、也能在 kubectl get networkpolicy 中看到,但不会产生预期的阻断效果。

因此不能用以下结果证明策略生效:

kubectl get networkpolicy -A

它只证明对象存在。必须进行实际连通性测试,并确认 CNI 的实现文档和状态。

NetworkPolicy 通常是基于 IP、协议和端口的 L3/L4 策略,不是完整的身份代理或 L7 防火墙。它一般不能直接表达:

  • HTTP 方法;
  • URL 路径;
  • JWT 用户身份;
  • SQL 语句;
  • 应用层租户关系。

这些要求需要服务网格、反向代理、应用鉴权或专门的 L7 网络产品。

还存在以下边界:

  • hostNetwork: true 的 Pod 使用节点网络命名空间,行为与普通 Pod 不同;
  • 访问节点本地服务、主机网络或云平台元数据的风险不能只靠普通 Pod 间策略解决;
  • 某些 CNI 对 hostNetwork、外部 IP、NodePort、LoadBalancer、加密隧道等路径有实现差异;
  • 网络策略不能撤销已经通过 Kubernetes API 获取的 Secret;
  • 只限制 Egress 可能仍无法阻止通过允许的 DNS、HTTPS 或应用出口进行数据外传;
  • DNS 被默认拒绝后,使用 Service 名称访问服务通常会失败,即使目标端口策略正确。

5.6 Service、Ingress 和 NetworkPolicy 的关系

Service 是稳定的虚拟访问入口,NetworkPolicy 通常最终仍然作用于后端 Pod。创建一个 Service 并不会自动允许网络访问。

例如:

apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: tenant-a
spec:
  selector:
    app: api
  ports:
    - port: 8080
      targetPort: 8080

客户端访问:

api.tenant-a.svc.cluster.local:8080

要成功,至少需要同时满足:

  1. DNS 查询被允许;
  2. 客户端 Pod 的 Egress 被允许;
  3. api Pod 的 Ingress 允许客户端;
  4. Service selector 确实选中了后端;
  5. 后端进程监听的是目标端口;
  6. CNI 正确执行策略。

因此“Service 存在但连接超时”不能直接归因于 Service。应按 DNS、Endpoint、客户端出站、服务端入站、应用监听顺序排查:

kubectl get svc,endpointslices -n tenant-a
kubectl describe networkpolicy -n tenant-a
kubectl exec -n tenant-a <client> -- nslookup api.tenant-a.svc.cluster.local
kubectl exec -n tenant-a <client> -- curl -v --connect-timeout 3 http://api:8080

六、一个租户基线的完整组合

下面把 Namespace、LimitRange、ResourceQuota、RBAC 和默认网络策略组合起来。它不是所有生产环境的最终模板,但可以作为可执行的基础实验。

apiVersion: v1
kind: Namespace
metadata:
  name: tenant-a
  labels:
    tenant.example.com/id: tenant-a
---
apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-a-container-defaults
  namespace: tenant-a
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      default:
        cpu: 500m
        memory: 512Mi
      min:
        cpu: 10m
        memory: 16Mi
      max:
        cpu: "2"
        memory: 4Gi
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-a-budget
  namespace: tenant-a
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "50"
    persistentvolumeclaims: "10"
    requests.storage: 100Gi
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tenant-a-app-editor
  namespace: tenant-a
rules:
  - apiGroups: ["apps"]
    resources: ["deployments", "deployments/scale", "replicasets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods", "services", "configmaps"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["pods/log"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-a-app-editor
  namespace: tenant-a
subjects:
  - kind: Group
    name: tenant-a-developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: tenant-a-app-editor
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tenant-a-default-deny
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tenant-a-allow-same-namespace
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}
  egress:
    - to:
        - podSelector: {}

应用:

kubectl apply -f tenant-a-baseline.yaml
kubectl get ns,limitrange,resourcequota,role,rolebinding,networkpolicy -n tenant-a

这里有一个重要的生命周期问题:如果先应用 ResourceQuota,再创建没有资源规格的 Pod,Pod 可能被配额拒绝;如果 LimitRange 能在准入阶段注入默认值,Pod 才能进入后续配额检查。实际排错时应查看:

kubectl get events -n tenant-a --sort-by=.lastTimestamp

如果一个应用需要访问 DNS、镜像仓库、数据库或监控系统,还必须显式增加对应 Egress 规则。默认拒绝策略不能直接照搬到所有系统,否则应用会以“DNS 失败”“连接超时”或“探针失败”的形式中断。


七、Pod Security 是 Namespace 多租户中不可缺少的前置控制

只配置 RBAC、NetworkPolicy 和 Quota,仍然可能允许租户创建高风险 Pod。例如:

spec:
  hostNetwork: true
  hostPID: true
  containers:
    - name: privileged
      image: example/debug
      securityContext:
        privileged: true

这类能力可能使 Pod 接触节点网络、进程或设备。即使租户无法读取 Node 对象,它仍可能通过工作负载间接影响节点。

Kubernetes 内置的 Pod Security Admission 使用 Namespace 标签选择基线,例如:

kubectl label namespace tenant-a \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted \
  --overwrite

restricted 的具体要求随 Kubernetes 版本的 Pod Security Standards 定义变化,应以目标集群版本文档为准。它通常会限制:

  • 特权容器;
  • host namespace;
  • 不安全的 capabilities;
  • 不安全的卷和用户设置;
  • 容器权限提升。

这不是 RBAC 的替代品:

  • RBAC 决定谁能提交对象;
  • Pod Security Admission 决定 Pod 安全上下文是否满足策略;
  • NetworkPolicy 决定网络流量;
  • Quota 决定资源预算。

如果允许租户修改 Namespace 上的 Pod Security 标签,租户就可能修改自己的强制策略,所以这些标签同样应由平台侧管理。


八、隔离极限:Namespace 何时不够

8.1 共享 Namespace 适合什么场景

Namespace 多租户通常适合:

  • 同一组织内部的团队;
  • 开发、测试、预发布环境;
  • 彼此具有一定信任关系的应用;
  • 平台希望统一升级、监控和调度的工作负载。

它的优势是成本低、运维简单、资源共享充分。缺点是所有租户共享:

  • API Server 和控制器;
  • etcd;
  • 节点内核;
  • 容器运行时;
  • CNI;
  • 部分集群级控制面;
  • 调度和节点资源。

8.2 不适合共享 Namespace 的场景

如果租户之间互不信任,或者租户有能力运行任意代码,应谨慎使用单集群 Namespace 作为唯一边界。以下需求通常需要更强的隔离:

  • 对抗恶意租户;
  • 处理高敏感数据;
  • 必须满足严格合规边界;
  • 允许自定义 CNI、CSI、Admission Webhook 或 CRD;
  • 允许特权容器、hostPath、hostNetwork;
  • 需要避免一个租户影响控制面可用性;
  • 需要独立升级和故障域。

常见的增强路径从弱到强大致是:

  1. Namespace;
  2. Namespace + RBAC + Quota + NetworkPolicy + Pod Security;
  3. 专用节点池、taint、节点亲和性和独立存储策略;
  4. 虚拟集群或强隔离运行时;
  5. 独立 Kubernetes 集群;
  6. 独立云账号、项目、网络和基础设施。

不能把这个顺序理解成绝对等级。专用节点能降低资源和故障干扰,却不自动防止控制面越权;独立集群能减少控制面共享,却仍可能共享云账号、镜像仓库或网络。

8.3 “租户可以创建 Pod”是一个能力放大器

允许创建 Pod 往往意味着租户可以选择:

  • 镜像;
  • 环境变量;
  • Volume;
  • ServiceAccount;
  • 网络模式;
  • 安全上下文;
  • 应用命令。

因此,RBAC 中的 create podscreate deployments 不应只按“对象写权限”理解,而应按“能运行什么代码”理解。

一个租户若同时拥有以下组合,风险会显著上升:

create pods
+ 使用高权限 ServiceAccount
+ 允许 hostPath 或 privileged
+ 能修改 RoleBinding

这可能演化成 Namespace 管理权限、节点访问甚至集群级权限。生产策略应同时审查 RBAC 和准入策略,不能只看 Role YAML。


九、常见误解与失败路径

误解一:Namespace 能阻止跨 Namespace 访问

错误。Namespace 主要划分 API 对象作用域,默认并不提供网络隔离。没有 NetworkPolicy 时,Pod 之间通常可以通过 Pod IP 或 Service 互相通信,具体还取决于网络插件和集群配置。

误解二:NetworkPolicy 能阻止所有越权

错误。它主要控制网络连接,不能阻止:

  • 通过 RBAC 允许的 API 请求;
  • Secret 被合法挂载到 Pod 后的读取;
  • 容器逃逸;
  • 节点级访问;
  • 应用层认证缺陷;
  • 允许的出口协议中的数据外传。

误解三:配额到了,节点就安全了

错误。ResourceQuota 只限制被配额统计的对象和资源使用量,不保证节点性能、不限制所有内核资源,也不自动保证公平调度。

误解四:限制了 CPU limit 就不会抢 CPU

错误。CPU limit 通常由 cgroups 执行,但实际性能还受 requests、节点超卖、CPU 管理策略、内核和运行时影响。CPU limit 不是独占 CPU。

误解五:看到 NetworkPolicy 对象就说明网络已经隔离

错误。必须确认:

kubectl get pods -A -o wide
kubectl get networkpolicy -A
kubectl get events -A

并使用测试 Pod 验证允许和拒绝路径。还要确认 CNI 插件支持并启用了 NetworkPolicy。Kubernetes API Server 不会替网络插件完成数据包过滤。

误解六:删除 Namespace 就一定能清理外部资源

错误。云盘、负载均衡器、DNS 记录和外部数据库可能由控制器通过 finalizer 管理。控制器不可用、凭据失效或 finalizer 异常时,Namespace 可能卡住,或者强制删除后留下外部资源。


十、生产诊断顺序

10.1 权限被拒绝

先确认请求身份:

kubectl auth whoami

再检查授权:

kubectl auth can-i get pods -n tenant-a
kubectl auth can-i create deployments -n tenant-b
kubectl auth can-i '*' '*' --all-namespaces

can-i 返回 no,检查:

kubectl get role,rolebinding -n tenant-a
kubectl get clusterrole,clusterrolebinding
kubectl describe rolebinding <name> -n tenant-a

重点确认:

  • RoleBinding 的 Namespace;
  • subject 的用户名和组名是否与认证系统一致;
  • roleRef 是否指向预期角色;
  • 是否误用了 ClusterRoleBinding;
  • 请求的 API group 和资源名称是否正确;
  • 子资源是否单独授权,例如 pods/logdeployments/scale

10.2 Pod 创建失败

kubectl describe pod <pod> -n tenant-a
kubectl get events -n tenant-a --sort-by=.lastTimestamp
kubectl describe resourcequota -n tenant-a
kubectl describe limitrange -n tenant-a

按错误类型区分:

  • exceeded quota:配额总量或对象数量;
  • must specify requests...:缺少资源声明,且没有默认值;
  • must be less than or equal to max:超过 LimitRange 单对象上限;
  • Insufficient cpu/memory:调度容量;
  • violates PodSecurity:安全准入策略;
  • forbidden:RBAC 或其他准入拒绝。

10.3 网络连接失败

先验证名称解析:

kubectl exec -n tenant-a <pod> -- nslookup api.tenant-a.svc.cluster.local

再验证 Service 和后端:

kubectl get svc api -n tenant-a
kubectl get endpointslice -n tenant-a \
  -l kubernetes.io/service-name=api

最后检查策略选择器:

kubectl get networkpolicy -n tenant-a -o yaml
kubectl get pod -n tenant-a --show-labels
kubectl get namespace --show-labels

要特别检查:

  • 目标 Pod 是否被 podSelector 选中;
  • 来源 Namespace 是否有正确标签;
  • ingressegress 是否都配置;
  • 端口是 Service port 还是容器 targetPort;
  • DNS 的 Namespace 和 Pod 标签是否正确;
  • CNI 是否真的执行 NetworkPolicy;
  • 是否使用了 hostNetwork、NodePort 或外部出口等特殊路径。

十一、如何验证租户边界,而不是只检查 YAML

多租户安全需要做负向测试。假设:

  • 用户 alice 属于 tenant-a-developers
  • 用户 alice 不应访问 tenant-b
  • tenant-a 中的 frontend 可以访问 api;
  • tenant-b 中的 Pod 不可以访问 tenant-a 的 api。

可以建立测试矩阵:

测试 预期
Alice 在 tenant-a 创建 Deployment 允许
Alice 在 tenant-b 删除 Pod 拒绝
Alice 读取 tenant-a Secret 按设计决定,通常最小化
tenant-a frontend -> tenant-a api:8080 允许
tenant-b Pod -> tenant-a api:8080 拒绝
tenant-a Pod -> 未允许的外部地址 拒绝或按出口策略
tenant-a 创建超过配额的 Pod 拒绝
tenant-a 创建超过 LimitRange 最大值的容器 拒绝
tenant-a 创建 privileged Pod 按 Pod Security 策略拒绝

其中,RBAC 的负向测试可以用 kubectl auth can-i,Quota 和 Admission 必须实际提交对象,NetworkPolicy 必须从测试 Pod 发起真实连接。测试结果还应记录集群版本、CNI、运行时和云厂商网络实现,因为这些组件会影响行为。


十二、最终边界:四个机制各自保证什么

可以把这四个核心机制的责任边界总结为:

Namespace
  -> “对象归属在哪里”

RBAC
  -> “谁可以通过 Kubernetes API 做什么”

NetworkPolicy
  -> “哪些被选中的 Pod 之间允许哪些网络流量”

ResourceQuota / LimitRange
  -> “Namespace 总共能申请多少,单个对象允许多大,缺省值是什么”

它们不会自动提供以下能力:

  • Namespace 之间的硬件隔离;
  • 防止节点内核漏洞;
  • 防止容器逃逸;
  • 应用层身份认证和授权;
  • 自动保证资源性能;
  • 自动发现所有数据外传路径;
  • 自动清理外部云资源;
  • 自动保护集群级资源;
  • 自动阻止租户利用高权限 ServiceAccount 提权。

一个合理的共享集群租户基线,通常是:

Namespace+RBAC+Pod Security+NetworkPolicy+ResourceQuota+LimitRange+验证与审计\text{Namespace} + \text{RBAC} + \text{Pod Security} + \text{NetworkPolicy} + \text{ResourceQuota} + \text{LimitRange} + \text{验证与审计}

如果租户之间需要对抗性隔离,则应进一步引入专用节点、隔离运行时、独立控制面或独立集群。Namespace 是 Kubernetes 多租户的组织基础,但它的隔离极限取决于整个控制面、准入、网络、节点和基础设施边界,而不是 Namespace 对象本身。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。