Kubernetes multi-tenancy: namespace-isolatie, ResourceQuota en LimitRange

Meerdere teams of omgevingen op een enkel Kubernetes-cluster draaien bespaart infrastructuurkosten, maar zonder expliciete grenzen kan een namespace alle resources opeisen. Deze gids loopt door het inrichten van een tenant-namespace met ResourceQuota voor totaallimieten, LimitRange voor standaardwaarden per container, NetworkPolicy voor netwerkisolatie, RBAC voor toegangscontrole op de API, en Pod Security Standards voor runtime-restricties.

Inhoudsopgave

Doel

Aan het einde van deze gids heb je een volledig ingerichte tenant-namespace waarin resourceverbruik begrensd is, netwerkverkeer standaard geblokkeerd wordt, API-toegang beperkt is tot een team, en runtime-privileges zijn ingeperkt. Hetzelfde patroon werkt voor een team, een omgeving (staging, productie) of een intern product.

Vereisten

  • Een Kubernetes-cluster met v1.30 of nieuwer, kubectl-toegang en rechten om namespaces, ResourceQuotas, LimitRanges, NetworkPolicies, Roles en RoleBindings aan te maken
  • Een CNI-plugin die NetworkPolicy afdwingt (Calico, Cilium of Antrea). Zonder zo'n plugin worden NetworkPolicy-objecten wel opgeslagen in de API maar hebben ze geen enkel effect op verkeer. Flannel en kubenet dwingen ze niet af.
  • Bekendheid met resource requests en limits. Als het verschil tussen een request en een limit niet helder is, lees dan eerst dat artikel; kort gezegd: requests sturen scheduling, limits sturen runtime-handhaving.

Wat namespaces wel en niet isoleren

Namespaces bieden logische segmentatie van API-resources binnen een enkel control plane. Ze scopen namen (twee namespaces kunnen allebei een Service api hebben), ze scopen RBAC Roles en RoleBindings, en ze scopen ResourceQuotas, LimitRanges en NetworkPolicies.

Dat is het.

Namespaces isoleren standaard geen netwerkverkeer. Elke pod kan elke andere pod in elke namespace bereiken zonder beperking. Ze isoleren geen node-resources (workloads uit verschillende namespaces delen dezelfde nodes). Ze isoleren de host-kernel niet. Een namespace op zichzelf is een label, geen hek. Alles op deze pagina bestaat om dat label om te zetten in daadwerkelijke isolatie.

Stap 1: maak de tenant-namespace aan

kubectl create namespace team-payments
kubectl label namespace team-payments \
  team=payments \
  cost-center=FIN-200

De labels team en cost-center zijn niet verplicht voor Kubernetes, maar ze geven NetworkPolicy namespaceSelector-regels iets om op te matchen en maken kostenallocatie-tooling (zoals Kubecost) bruikbaar. Sinds Kubernetes 1.21+ voegt het cluster automatisch kubernetes.io/metadata.name: team-payments toe aan elke namespace, dus dat label hoef je niet handmatig te zetten.

Verwachte output:

namespace/team-payments created
namespace/team-payments labeled

Stap 2: pas een LimitRange toe

Een LimitRange beperkt individuele containers en pods, niet namespace-totalen. Het dient hier twee doelen: standaard resource-requests en -limits injecteren voor containers die ze weglaten, en een plafond instellen zodat een enkele container niet 64 GiB geheugen claimt in een namespace die op 20 GiB staat.

Pas de LimitRange voor de ResourceQuota toe. De volgorde doet ertoe. Wanneer er een ResourceQuota bestaat voor cpu of memory, weigert de admission controller elke pod die geen resource-requests specificeert. LimitRange-defaults worden geïnjecteerd voor de quotacheck, dus door de LimitRange eerst neer te zetten voorkom je dat bestaande workloads breken als de quota erbij komt.

# limitrange.yaml
apiVersion: v1
kind: LimitRange
metadata:
  name: default-limits
  namespace: team-payments
spec:
  limits:
  - type: Container
    default:          # geïnjecteerd als limits wanneer de container er geen heeft
      cpu: 500m
      memory: 512Mi
    defaultRequest:   # geïnjecteerd als requests wanneer de container er geen heeft
      cpu: 100m
      memory: 128Mi
    max:              # plafond per container
      cpu: "4"
      memory: 8Gi
    min:              # bodem per container
      cpu: 50m
      memory: 64Mi
kubectl apply -f limitrange.yaml

Verwachte output:

limitrange/default-limits created

Twee dingen om op te letten. Houd precies een LimitRange per namespace aan. Meerdere LimitRange-objecten in dezelfde namespace leiden tot niet-deterministische default-injectie, en uitzoeken welke defaults winnen is geen productieve manier om een middag door te brengen.

Daarnaast: wanneer default en defaultRequest identiek zijn (beiden bijvoorbeeld 500m CPU), krijgt elke pod die resource-specs weglaat Guaranteed QoS, wat invloed heeft op eviction-prioriteit bij geheugendruk op de node. Wil je Burstable QoS als standaard (de gebruikelijkere keuze voor generieke workloads), zet defaultRequest dan lager dan default. Voor een volledige uitleg van QoS-klassen, zie resource requests en limits.

De request-overschrijdt-limit-val

Als een developer requests.cpu: 700m specificeert zonder een limit, injecteert de LimitRange limits.cpu: 500m vanuit het default-veld. Validatie faalt vervolgens omdat de request (700m) de limit (500m) overschrijdt. De foutmelding ziet er zo uit:

spec.containers[0].resources.requests: Invalid value: "700m":
must be less than or equal to cpu limit

De developer heeft zelf geen limit gezet. De LimitRange deed dat. De oplossing: specificeer altijd zowel requests als limits in de pod-spec, of zorg dat de LimitRange default-waarden minstens zo hoog zijn als defaultRequest.

Stap 3: pas een ResourceQuota toe

ResourceQuota begrenst het totale resourceverbruik over de hele namespace. Het wordt afgedwongen bij admission door de ResourceQuota-admission controller (standaard ingeschakeld in de meeste distributies). Elk verzoek dat het verbruik boven de harde limiet zou duwen, wordt geweigerd met HTTP 403 Forbidden.

# resourcequota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-payments-quota
  namespace: team-payments
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    pods: "100"
    services: "10"
    persistentvolumeclaims: "5"
kubectl apply -f resourcequota.yaml

Verwachte output:

resourcequota/team-payments-quota created

Controleer het verbruik direct:

kubectl describe resourcequota team-payments-quota -n team-payments

Je zou Used: 0 moeten zien voor elke resource. Als er al pods draaien in de namespace, wordt hun verbruik hier weergegeven maar ze worden niet verwijderd; quota is niet retroactief.

Een gedrag dat mensen verrast. Het aanmaken van een Deployment die de quota overschrijdt laat de Deployment zelf gewoon slagen. Het Deployment-object wordt aangemaakt. Alleen de Pod-creatie daarbinnen faalt. kubectl get deployment toont de resource, maar kubectl describe deployment of kubectl get events -n team-payments onthult de 403-fout. Controleer altijd events wanneer een deployment er gezond uitziet maar nul ready replicas heeft.

Stap 4: vergrendel netwerkverkeer

Zonder NetworkPolicy kunnen pods in team-payments elke pod in elke andere namespace bereiken, en andersom. Een default-deny-policy dicht dat gat.

# deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: team-payments
spec:
  podSelector: {}   # selecteert alle pods in de namespace
  policyTypes:
  - Ingress
  - Egress

Dit blokkeert al het verkeer in beide richtingen. Inclusief DNS. Pas direct een DNS-egress-regel toe, anders breekt de namespace:

# allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: team-payments
spec:
  podSelector: {}
  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
kubectl apply -f deny-all.yaml -f allow-dns.yaml

Verwachte output:

networkpolicy.networking.k8s.io/default-deny-all created
networkpolicy.networking.k8s.io/allow-dns created

Voeg van hier uit specifieke ingress- en egress-regels toe voor de flows die de workloads van de tenant daadwerkelijk nodig hebben. De NetworkPolicy-gids behandelt ingress-regels, egress-regels en namespace-selectors in detail.

Stille faalmodus. Als je cluster Flannel of kubenet gebruikt, bestaan deze NetworkPolicy-objecten in de API maar doen ze niets. Verifieer dat je CNI policies afdwingt voordat je erop vertrouwt. Een snelle test: deploy een pod in een andere namespace en probeer een service in team-payments te curlen. Als de verbinding lukt na het toepassen van de deny-all-policy, dan dwingt je CNI niets af.

Stap 5: beperk RBAC tot de namespace

RBAC is de fundamentele isolatielaag voor het control plane. Zonder RBAC kan elk teamlid met kubectl-toegang resources over namespaces heen lezen of wijzigen, en kan elk ander isolatiemechanisme op deze pagina omzeild worden.

# role.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: developer
  namespace: team-payments
rules:
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
  resources: ["pods", "pods/log", "services", "configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["pods/exec"]
  verbs: ["create"]    # sta kubectl exec toe voor debugging
# rolebinding.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payments-developers
  namespace: team-payments
subjects:
- kind: Group
  name: team-payments-devs    # uit je identity provider
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io
kubectl apply -f role.yaml -f rolebinding.yaml

Een namespace-scoped Role geeft nul rechten buiten team-payments. Het team kan geen pods listen in team-orders, geen secrets lezen in kube-system, en niet de ResourceQuota verwijderen die je net hebt aangemaakt (tenzij je expliciet die verb op resourcequotas-resources toekent, wat je dus niet moet doen).

Gebruik voor workload-pods aparte ServiceAccounts met automountServiceAccountToken: false tenzij de pod daadwerkelijk API-toegang nodig heeft. Het RBAC-artikel behandelt serviceaccountpatronen uitgebreid.

De quota beschermen tegen namespace-admins

Als een tenant de ingebouwde admin-ClusterRole gebonden heeft in zijn namespace, kan die de ResourceQuota verwijderen en alle limieten opheffen. Voorkom dit met een ValidatingAdmissionPolicy (stabiel sinds Kubernetes 1.30) of een Kyverno/Gatekeeper-policy die DELETE op ResourceQuota-objecten blokkeert voor niet-cluster-admins.

Stap 6: handhaaf Pod Security Standards

Pod Security Standards beperken wat een pod mag doen tijdens runtime: host-namespaces, privileged containers, capability-escalatie. Ze toepassen per namespace is een enkel label:

kubectl label namespace team-payments \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/warn=restricted

Dit dwingt het baseline-profiel af (blokkeert bekende privilege-escalatievectoren) en waarschuwt bij schendingen van het strengere restricted-profiel zodat het team geleidelijk kan migreren. Voor een volledig overzicht van alle drie de profielen en handhavingsmodi, zie de Pod Security Standards-gids.

Verwachte output:

namespace/team-payments labeled

Stap 7: controleer de volledige stack

Voer deze checks uit om te bevestigen dat elke laag op zijn plek zit:

# ResourceQuota toegepast
kubectl describe resourcequota -n team-payments

# LimitRange toegepast
kubectl describe limitrange -n team-payments

# NetworkPolicy-regels aanwezig
kubectl get networkpolicy -n team-payments

# RBAC-bindings beperkt tot namespace
kubectl get rolebinding -n team-payments

# Pod Security Standard-labels
kubectl get namespace team-payments --show-labels | grep pod-security

Deploy een testpod om de hele admission-pipeline te testen:

kubectl run test-pod --image=nginx:1.27 -n team-payments
kubectl describe pod test-pod -n team-payments

Controleer in de podbeschrijving dat Requests en Limits aanwezig zijn (geïnjecteerd door de LimitRange). Als de pod start, heeft je quota ruimte en staat het securityprofiel het toe. Ruim op:

kubectl delete pod test-pod -n team-payments

Rolling updates en quota-ruimte

Tijdens een RollingUpdate maakt Kubernetes nieuwe pods aan voordat oude worden beëindigd (gestuurd door maxSurge). Als de quota precies op het steady-state-verbruik staat, duwen de nieuwe pods het verbruik tijdelijk boven het plafond en loopt de update vast. De Deployment-controller probeert pod-creatie opnieuw met exponential backoff, en het kan tot 16 minuten duren voor de volgende poging.

Drie aanpakken:

  1. Voeg ruimte toe. Zet de quota 20-30% boven het steady-state-verbruik. Dit is de simpelste oplossing en werkt voor de meeste teams.
  2. Zet maxSurge: 0. Er worden geen nieuwe pods gemaakt voordat oude stoppen, dus verbruik gaat nooit boven steady-state. De keerzijde: rolling updates veroorzaken korte downtime.
  3. Automatiseer quota-bumps. Je CI/CD-pipeline verhoogt de quota tijdelijk voor de deploy en herstelt ze daarna. Meer bewegende delen, maar het houdt quota's strak buiten deploymentvensters.

Quotagebruik monitoren

Wanneer een namespace zijn quota raakt, falen nieuwe pods stil op Deployment-niveau. Proactief monitoren voorkomt onverwachte uitval.

# Snelle check over alle namespaces
kubectl get resourcequotas -A

# Events voor quotaschendingen in de namespace
kubectl get events -n team-payments --field-selector reason=FailedCreate

Als je Prometheus draait met kube-state-metrics, geeft de kube_resourcequota-metric verbruik versus harde limieten per namespace. Een handige alertdrempel:

kube_resourcequota{type="used"} / kube_resourcequota{type="hard"} > 0.85

Dit vuurt wanneer een quotadimensie 85% benutting bereikt, zodat het team tijd heeft om op te ruimen of een quotaverhoging aan te vragen. De Prometheus-monitoringgids behandelt de installatie van kube-state-metrics en alertingregels.

Soft vs. hard multi-tenancy

Alles op deze pagina is soft multi-tenancy: een enkel gedeeld control plane met namespace-isolatie via RBAC, NetworkPolicy, quota's en admission policies. De Kubernetes-documentatie noemt dit geschikt voor vertrouwde tenants binnen dezelfde organisatie.

Voor onvertrouwde tenants (SaaS-klanten, externe contractors, compliance-geïsoleerde workloads) zijn namespaces niet genoeg. Een gecompromitteerde pod kan nog steeds kernel-exploits proberen tegen de gedeelde host, en een verkeerd geconfigureerde ClusterRoleBinding kan toegang lekken over namespaces heen. Hard multi-tenancy betekent dat elke tenant zijn eigen API-server krijgt via virtuele clusters (vcluster) of dedicated fysieke clusters.

Het beslismoment is vertrouwen. Als de tenants interne teams zijn die niet bewust proberen uit hun boundary te ontsnappen, is soft multi-tenancy met alles op deze pagina een verdedigbare architectuur. Als een tenantbreuk onder geen enkele omstandigheid andere tenants mag bereiken, heb je harde isolatie nodig.

Wanneer escaleren

Als je alle lagen hebt toegepast en nog steeds cross-tenant-toegang of resource-uithongering ziet, verzamel dan deze informatie voordat je je platformteam inschakelt:

  • kubectl describe resourcequota -n <namespace> output
  • kubectl describe limitrange -n <namespace> output
  • kubectl get networkpolicy -n <namespace> -o yaml
  • kubectl get events -n <namespace> --field-selector reason=FailedCreate
  • CNI-pluginnaam en -versie (kubectl get pods -n kube-system en controleer de CNI-daemonset)
  • Pod Security Standard-labels op de namespace (kubectl get namespace <name> --show-labels)
  • Clusterversie (kubectl version)

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.