Kubernetes Multi-Tenancy: How Much Access Should a Kubernetes Tenant Actually Have?

All posts

This is the third article in a series on Kubernetes multi-tenancy. The first two articles cover very important questions:

Once you have decided on your physical or virtual isolation model, the next thing you have to think about is setting up Role-based access control (RBAC).

You can build the most expensive, isolated cluster in the world, but if your API permissions are wide open, you have no security.

Out of the box, Kubernetes namespaces are strictly organizational folders, not secure barriers. They only become actual security boundaries when you actively layer controls on top of them

Without RBAC, any user or pod inside your cluster can make arbitrary API calls to view, modify, or completely delete another tenant's resources.

This article covers exactly what each tenant is allowed to do once those boundaries are in place, how much of the cluster's CPU and memory they can safely consume, and how to declaratively onboard a new team without any of your configurations drifting apart.

Try it yourself: a test cluster on Rackspace Spot runs for as little as $7.20 a month, cheap enough to set up a multi-tenant cluster of your own.

Access control, security, and governance

Think of it this way: your logical boundaries (like namespaces) decide where a tenant's applications are allowed to live. But access control decides what those tenants are actually allowed to do once they are inside.

You have to be very careful when setting up access control for tenants. If you give a tenant too much permission, they could reach straight past their namespace and read other teams' configurations and secrets.

If you give them too little control, you end up with a useless cluster, where developers cannot deploy their own workloads or maybe manage their own secrets.

RBAC design for multi-tenant Kubernetes clusters

RBAC maps API permissions to identities using four native objects, which come in two pairs:

  • Role and RoleBinding, which work inside a single namespace.
    • A Role lists the resources and the verbs allowed on them.
    • A RoleBinding grants that Role to a user, group, or service account in that namespace.
  • ClusterRole and ClusterRoleBinding, which work across the whole cluster.
    • A ClusterRole covers resources that do not belong to any namespace, such as nodes, PersistentVolumes, and CRDs.
    • A ClusterRoleBinding grants it in every namespace at once.

Say a tenant team has two kinds of people in it, a tenant manager and a tenant user. Defining a standardized role for each of them, rather than one role for everybody, is what the principle of least privilege looks like in practice:

  • The tenant manager role grants full read-and-write rights over standard application objects inside their own namespace. In YAML that means a wide set of verbs, get, list, watch, create, update, patch, and delete, targeting core namespaced resources such as deployments, replicasets, pods, services, and configmaps.
  • The tenant user role is a highly restricted, read-only role for developers or automated workflows that only need to see the state of things without changing them. It strips write actions entirely, granting only get, list, and watch on a limited set of resources such as pods and services.

The manager Role looks like this:

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

And the user Role:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tenant-user
  namespace: tenant-a
rules:
  - apiGroups: [""]
    resources: ["pods", "services"]
    verbs: ["get", "list", "watch"]

Both roles are bound to specific users, groups, or service accounts using RoleBindings within the target namespace:

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

At the end of the day, logical tenant isolation is never about a single setting. You start by using namespaces to segment resources, then layer on restrictive RBAC to govern API access.

Who Gets the Keys? Authentication in a Shared Cluster

Those standard Roles and RoleBindings you just wrote need some kind of identity to bind to. But here is the thing: Kubernetes does not manage user accounts. Tenant identity has to come from outside your cluster.

In a production environment, you should never manage tenant users manually. Instead, the industry standard is to connect your cluster to an external identity provider using OpenID Connect (OIDC).

  • By deploying an identity service like Dex, you can bridge your cluster with cloud-based directory services like Microsoft Entra ID.
  • This allows you to map your company's existing Active Directory groups directly to Kubernetes RBAC roles.
  • When a developer joins a team, they automatically inherit the correct namespace permissions because tenant membership is managed globally, right where the rest of your company's access is controlled.

While you can technically use client certificates to authenticate developers, certificates age incredibly badly in a multi-tenant setup. Kubernetes has no native mechanism for certificate revocation, meaning a leaked tenant certificate remains fully valid and usable until the day it officially expires.

To completely eliminate the risk of leaked, static credentials, platform teams are shifting away from perpetual tokens and certificates. Instead, they leverage cloud-native Workload and Managed Identities. These systems grant time-limited, passwordless access to resources, ensuring that workloads authenticate dynamically without any static keys for an attacker to steal.

Finally, no matter how tight your authentication is, you must keep API request auditing turned on. In a shared cluster, an audit log is your only historical record of which tenant called what.

If you are implementing GitOps, you also get a massive security bonus because every resource change must be declared as code and your Git commit history acts as a clean, chronological audit trail of your cluster's desired state, giving your security team a transparent record of who approved every single deployment.

Admission control in multi-tenant environments

Relying on RBAC alone only controls who can talk to your API; it does not control what those users are allowed to run. Without active admission control, a tenant with standard deployment rights can spin up a highly privileged container that mounts the physical host's filesystem, giving them an immediate path to compromise the entire physical node.

To lock this down without adding external operational complexity, leverage the built-in Pod Security Admission (PSA) controller. It enforces three highly standardized security profiles at the namespace level using simple labels:

ProfileWhat it allowsUse it for
PrivilegedEverything, no restrictionsInfrastructure namespaces you control, never a tenant
BaselineBlocks known privilege escalations, allows most common workloadsSemi-trusted internal teams
RestrictedEnforces non-root, dropped capabilities, seccomp, no privilege escalationAny tenant running code you did not write
apiVersion: v1
kind: Namespace
metadata:
  name: tenant-a
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted

When the three native PSA profiles are too rigid for your business needs, deploy a policy-as-code engine like Kyverno or OPA to enforce highly specific, context-aware rules:

EnginePolicies written inRuns asReach for it when
KyvernoKubernetes resourcesA webhook you operateYou want policies that generate objects as well as validate them, such as a default-deny NetworkPolicy for every new tenant namespace
OPA GatekeeperRegoA webhook you operateYour team already runs OPA elsewhere
ValidatingAdmissionPolicyCEL, a small expression languageInside the API server, built in since 1.30The check is a common one and you would rather not operate a webhook at all

By stacking built-in Pod Security Admission with declarative Kyverno policies, you guarantee that even if a tenant manager has full RBAC write-access inside their namespace, they can never deploy a malicious container that compromises the underlying host node.

Security policies and compliance in multi-tenant clusters

Compliance frameworks, whether you are dealing with SOC 2, PCI-DSS, HIPAA, or GDPR, care about demonstrable, enforced separation rather than whatever clever Kubernetes feature you happen to be running.

Regulatory auditors do not care how clean your YAML is; they want to see absolute proof of who accessed what, that tenant data is cryptographically isolated, and that your security boundaries are actively enforced rather than simply assumed.

In a shared cluster, meeting these compliance standards boils down to establishing four critical operational controls:

  • Continuous API auditing. You must keep API request auditing permanently turned on at the control plane level to record every single tenant interaction with the API server. If you are practicing GitOps, you also inherit a massive compliance superpower: your Git commit history acts as a clean, chronological, and unalterable audit trail of your cluster's desired state, giving your compliance team a transparent record of who approved every single deployment.
  • Tenant-scoped encryption at rest. The control plane's etcd store contains sensitive credentials, access keys, and configuration data for every tenant in the cluster. You must protect this central database by encrypting data at rest and securing your network transport layers using TLS/SSL.
  • Enforced image registry restrictions. Never let tenants pull unverified, community-supported container images from public registries, like untrusted Docker Hub repositories, which can introduce severe vulnerabilities or malicious payloads into your cluster. To prevent this, use a policy engine like Kyverno to validate image metadata and actively restrict pod deployments so they can only pull scanned, verified images from private registries under your control.
  • Per-tenant secret backends. Because native Kubernetes secrets are merely base64-encoded plain text, storing them directly in your repositories is a critical security risk. Decouple your secrets completely from Git and the cluster control plane by using the External Secrets Operator. This operator connects to secure external vaults, like Azure Key Vault or AWS Secrets Manager, using managed, passwordless identities, pulling and injecting credentials dynamically at runtime.

Write your incident and forensics procedure before you actually need it. In a multi-tenant cluster, isolating a single compromised container for forensic analysis without touching the neighboring pods sharing that node's kernel takes pre-planned, exact steps. If you are trying to invent your isolation and containment scripts during a live breach, you have already lost.

Resource management and performance isolation

Permissions decide what a tenant may create. Nothing in RBAC decides how much they may consume, so a tenant with correct permissions can still take every CPU on a node and leave everyone else waiting.

Resource quotas and LimitRanges for tenant fairness

Securing API access with RBAC stops tenants from tampering with each other's configurations, but it does nothing to stop a buggy, runaway application from swallowing up the entire cluster's hardware. In a shared cluster, you are constantly fighting the "noisy neighbor" problem. If left unconstrained, one tenant's workloads can greedily consume excessive CPU, memory, or disk space, degrading performance and starving out other critical applications sharing those physical nodes.

To prevent this resource contention, you must enforce strict compute and storage boundaries directly at the namespace level.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-a-quota
  namespace: tenant-a
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    persistentvolumeclaims: "10"
    requests.storage: 500Gi
    fast-ssd.storageclass.storage.k8s.io/requests.storage: 100Gi
    fast-ssd.storageclass.storage.k8s.io/persistentvolumeclaims: "4"
    count/services.loadbalancers: "2"

Compute isolation: requests against limits

To keep your shared nodes stable, every container deployment should define resource requirements using two distinct values:

  • Requests are the minimum amount of CPU and memory the Kubernetes scheduler (kube-scheduler) guarantees to reserve for a container. A standard, lightweight baseline for a microservice is requesting 100m CPU and 100Mi memory.
  • Limits are the hard, physical ceiling the container is allowed to consume on a host machine, enforced by the kernel through cgroups. If a container exceeds its CPU limit, the system throttles its usage. If it tries to burst past its memory limit, the host kernel terminates the container with an Out-of-Memory (OOM) kill. Standard limits are typically capped around 150m CPU and 150Mi memory.

Storage isolation: protecting the backing store

Physical node separation is only half the battle; you must also manage tenant-specific storage. In shared clusters, administrators configure different StorageClasses to partition storage tiers, such as high-performance database volumes against low-cost archival storage, to match varying tenant priorities.

However, because dynamic provisioning allows applications to request storage automatically, a single tenant can easily monopolize and exhaust your physical backing store. To enforce fair allocation, you must apply namespace resource quotas that cap the total storage capacity a tenant is allowed to dynamically allocate, ensuring no single team starves the rest of the cluster.

One quota covers one namespace, and a tenant frequently needs more than one. A team wanting dev, staging, and prod, or a namespace per service, gets a separate quota in each and no cap on the total, so five namespaces at 20 CPU each is a tenant who can take 100. Kubernetes has no cross-namespace quota. Capsule adds one across the namespaces it groups under a tenant, and capping the tenant rather than the namespace is the main reason to run it.

Resource sharing strategies and efficiency optimization

The idle cost of dedicated node pools

Sharing resources is the entire point of a multi-tenant cluster, but every physical isolation boundary you add takes back some of those hard-earned financial savings. Dedicated node pools are the clearest example: if you isolate a tenant's workloads onto their own physical hardware using taints, tolerations, and node affinity, you run the risk of sizing a node pool for peak load that sits completely idle the rest of the time

This idle capacity is the "tax" you pay for strict kernel separation. To make per-tenant pools financially viable, your platform team should leverage spot instances as an automated resource optimization technique to drastically cut your cloud spend on those idle nodes.

Overcommitment and the density trade-off

If you choose to maximize savings by packing multiple tenants onto shared nodes instead of isolating them, you are entering the world of node overcommitment. Overcommitment is a fantastic way to raise density, but it is a double-edged sword.

When multiple tenant workloads experience a burst in demand at the exact same time, you risk severe performance degradation and resource starvation.

Managing this density risk requires defining strict container-level resource requests and limits for every workload to ensure no single application can consume excessive host resources.

Platform components compete for the same capacity

Don't forget that CoreDNS, ingress controllers, logging agents, and monitoring stacks run on the same shared host cluster and compete for the exact same physical node capacity as your tenants.

If you fill the cluster completely with tenant pods, these critical system components won't have the resources to run or reschedule, causing cluster-wide outages where every tenant loses access to fundamental platform services simultaneously.

Bin-packing against spreading

Ultimately, deciding how tightly to pack your nodes is a direct business decision. Bin-packing tenants tightly onto fewer shared nodes saves massive amounts of infrastructure spend, while spreading them across different node pools prevents a single hardware failure from taking down multiple teams.

To balance this dynamically in production, deploy a Horizontal Pod Autoscaler (HPA) to scale your replicas up or down based on actual CPU utilization, allowing your resource footprint to adapt dynamically to real-world demand.

Performance isolation: who loses when a node fills up

Even with an HPA in place, nodes will eventually reach capacity. When memory on a node is exhausted, Kubernetes has to decide which pods live and which pods die.

Everything up to this point decides how much a tenant is allowed to have, and this section is about that moment of decision, when the node cannot give everyone what they asked for and something has to be taken away.

A ResourceQuota is no help here. A quota is accounting across a whole namespace, and it is checked when a pod is created rather than while it runs.

By the time pods are competing for memory on one machine, the quota has already done its job and has no further say in the outcome.

What decides the outcome is the relationship between each pod's requests and its limits. Kubernetes reads those two numbers and sorts every pod into one of three Quality of Service classes:

  • Guaranteed, where requests equal limits. These are evicted last.
  • Burstable, where requests are set but limits are higher. These go after BestEffort.
  • BestEffort, where neither is set. These are evicted first.

A tenant does not choose BestEffort, they land in it by leaving requests out of their manifest. So the team that paid the least attention to their resource configuration is the one whose pods get killed first, and nothing tells them that is why. This is the real reason a LimitRange matters: it supplies defaults so that nobody ends up in BestEffort by accident. Our guide to Kubernetes resource optimization covers how to size those numbers from real usage.

Even with all of that configured correctly, two kinds of interference stay outside your control.

Disk I/O has no equivalent of a CPU limit. A tenant running a database that saturates a shared volume slows down every other pod on that node, and there is no field in the pod spec that stops it.

Network bandwidth is not a pod-level setting either. Capping it per tenant depends on shaping at the CNI layer, which most clusters never configure.

For tenants where either of those genuinely matters, no amount of policy will fix it, and dedicated node pools are the practical answer. That is the same conclusion the security argument reaches, arrived at from a completely different direction.

Implementation, challenges, and best practices

Everything below has to be true for every tenant, not just the first one you set up carefully.

Here is the full checklist in one place, so you can hold a new tenant against it before handing over access.

Best practices for secure and scalable Kubernetes multi-tenancy

To build a truly secure, automated, and scale-ready platform, here is your definitive checklist, combining native isolation layers with GitOps operational best practices:

  • Default-deny networking, and double-checking the CNI. Always apply a default-deny NetworkPolicy to every tenant namespace from day one. But remember the CNI catch: network policy objects are completely ignored unless your underlying Container Network Interface plugin actively supports and enforces them. If you run Calico or Cilium, you are safe; if you use Flannel alone, your policies are completely silent and do absolutely nothing.
  • PSA set to restricted. For any namespace running application code you did not write or cannot inspect, set your built-in Pod Security Admission (PSA) label to restricted. This built-in controller instantly blocks any container that runs as root, keeps capabilities it should have dropped, or runs without a seccomp profile.
  • A quota and a LimitRange in every namespace. Never deploy a ResourceQuota without a corresponding LimitRange. Once a namespace has a quota on CPU or memory requests, Kubernetes immediately rejects any pod that arrives without those values set. A LimitRange acts as your silent safety net, automatically injecting default resource requests and limits to ensure developer manifests deploy smoothly without starving shared nodes.
  • Keep RBAC localized and strip Secrets access. Keep your tenant RBAC strictly bound to their assigned namespace using standard Roles and RoleBindings. Never use wildcards (*), which automatically inherit permissions when new APIs are installed, and never grant read access to secrets. Since Kubernetes Secrets are merely base64-encoded rather than encrypted, read access hands a tenant every credential that namespace stores, including database passwords, API keys, and TLS private keys.
  • Bridge auth to an external identity provider. Stop managing manual certificates or local credentials inside the cluster. Certificates age terribly because Kubernetes has no native revocation mechanism. Instead, run an OIDC connector like Dex to bridge your cluster with your enterprise directory, such as Microsoft Entra ID. This lets you map directory groups onto the RBAC groups your bindings already reference, so team onboarding and offboarding are managed globally in a single place.
  • Manage secrets declaratively, with no secrets in Git. If practicing GitOps, committing plain-text Secret manifests to Git is a massive security hazard. Force your tenants to use Sealed Secrets, encrypting sensitive data locally with kubeseal into custom resources that are completely safe to version-control in Git. Alternatively, use the External Secrets Operator to dynamically pull and inject credentials from a secure cloud vault at runtime using managed, passwordless identities.
  • Prune resources and enable self-healing. To run a zero-drift environment, treat your running cluster as strictly read-only for humans. Configure your GitOps engine's sync policies with prune: true, to instantly delete rogue resources manually injected into the cluster, and selfHeal: true, to automatically reconcile manual hotfixes back to the desired Git source of truth.
  • Turn off auto-created namespaces. Letting your delivery controller automatically spin up namespaces on the fly, such as setting CreateNamespace=true in Argo CD, is a major operational risk, because Argo CD does not delete namespaces it created for an application and you are left with orphaned resources. Every namespace used by a tenant must be explicitly declared, peer-reviewed, and lifecycle-managed as code in your Git repository.
  • Mandate horizontal pod autoscaling. Do not let your tenants run static replicas that waste cloud budget during quiet hours or fall over during high-traffic surges. Require teams to deploy a Horizontal Pod Autoscaler alongside their compute limits, so replicas scale up and down with real demand. Size the namespace quota with that ceiling in mind, because an HPA that tries to scale past the quota simply fails to create the pods.
  • Use dedicated node pools for real physical boundaries. If your tenants share a host node, they ultimately share an operating system kernel. If kernel-level separation is a hard requirement for your security team, use taints, tolerations, and node affinity to schedule sensitive workloads onto physically isolated, dedicated node pools. To keep the cost of these isolated pools affordable, run them on spot-priced infrastructure to minimize what your idle, peak-allocated hardware costs.
  • Add real-time runtime auditing. Admission controllers only screen configurations before they run. To protect your nodes once containers are active, deploy a runtime security monitor like Falco, which hooks directly into Linux kernel system calls to track container behavior in real time and alerts your security team if an attacker exploits a running container to open an unauthorized shell or read sensitive host files.
  • Know when to graduate to virtual clusters. If your logical namespace boundaries are becoming too complex, or if your developers are hitting a wall because global CustomResourceDefinitions are conflicting across teams, stop adding more policy layers. Move high-risk tenants to a virtual control plane like vCluster. Giving each tenant their own virtual API server and data store isolates their custom resources completely, while still reusing a single host platform stack rather than paying for a cluster each.

Properly implementing multi-tenancy in your Kubernetes cluster

Access control, quotas, and a repeatable onboarding process are what turn a set of boundaries into something you can run with more than two tenants on it. Get the first tenant right by hand, then template it, because the tenth is where anything you skipped becomes visible.

Sign up to Rackspace Spot and get a cluster starting at $7.20 a month.

Frequently asked questions

How do you stop one tenant using all the CPU and memory?

A ResourceQuota caps the namespace total and a LimitRange sets per-container defaults and maximums. Without the LimitRange, a quota on CPU and memory rejects any pod that arrives with no requests set.

What tools help manage Kubernetes multi-tenancy?

Capsule groups namespaces into tenants with shared policy, vCluster runs virtual clusters inside a namespace, and Kamaji runs tenant control planes as pods. Kyverno and OPA Gatekeeper enforce policy across all three.

How do you scope RBAC for a tenant?

Bind namespace-scoped Roles with RoleBindings, and keep ClusterRoleBindings for emergency admin access only. Leave Secrets and wildcards out, because read access to Secrets hands a tenant every credential that namespace stores, and a wildcard silently grows every time a new API is installed.

Should tenants be allowed to create their own namespaces?

Only through something that attaches the quota, policies, and RBAC at the same time. A namespace created by hand arrives with none of them, and nothing warns you.

What happens when you delete a tenant's namespace?

Every cluster-scoped object it used stays behind, including ClusterRoleBindings, PersistentVolumes set to Retain, CRDs installed for that tenant, and external load balancers or DNS records.

What are Kubernetes multi-tenancy best practices?

Apply a default-deny NetworkPolicy in every tenant namespace and confirm your CNI enforces policy at all, set Pod Security Admission to restricted for any tenant running code you did not write, give every namespace both a ResourceQuota and a LimitRange, keep tenant RBAC namespace-scoped with no Secrets and no wildcards, and move high-risk tenants to a virtual control plane rather than adding another policy layer.

How do you optimize cost in a multi-tenant Kubernetes cluster?

Share one control plane instead of buying one per team, bin-pack tenants onto fewer nodes, and run any dedicated node pools on spot-priced infrastructure so the idle capacity they need costs less. A Horizontal Pod Autoscaler keeps replicas matched to real demand rather than sized for peak.

How do you track Kubernetes cost per tenant?

Kubernetes does not attribute cost to anything. Splitting a shared node's bill across the tenants running on it needs tooling you add yourself, such as OpenCost or Kubecost, which map usage back to namespaces.

Is a multi-tenant Kubernetes cluster secure?

It is as secure as the boundaries you configure, because a cluster arrives with none of them in place. RBAC decides which API calls a tenant can make, admission control decides what their pods may contain, and network policy decides what they can reach. Tenants running code you did not write need more than that, since they still share a kernel with everyone else.

Can you rate limit requests per tenant?

Not with a ResourceQuota, which caps objects and compute rather than request rate. Per-tenant rate limiting is a service mesh job, and API Priority and Fairness is the equivalent for protecting the API server itself from one tenant's controller.