# Kubernetes Multi-Tenancy with Namespaces and Network Policies: A Practical Guide for GitOps Teams Source: https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html Published: 2026-03-15T03:12:00.000-07:00 Updated: 2026-10-10T21:38:18.342-07:00 Author: Unknown Topics: **Listen to this article** AI-narrated article - 13 min 50 sec. Prose is preserved; code and diagram cues refer to the examples below. Your browser does not support audio playback. [Open the MP3](https://manomatt.github.io/devopsdreams-audio/audio/post-2836711951267268511-v1.mp3). [Read the narration transcript](https://manomatt.github.io/devopsdreams-audio/transcripts/post-2836711951267268511-v1.html) - [Open audio](https://manomatt.github.io/devopsdreams-audio/audio/post-2836711951267268511-v1.mp3) # Kubernetes Multi-Tenancy with Namespaces and Network Policies: A Practical Guide for GitOps Teams Namespaces are only the first layer of tenant isolation. This guide shows how to combine RBAC, Pod Security Admission, quotas, NetworkPolicies, and Flux service-account impersonation so teams can share a cluster without sharing blast radius. ## TL;DR [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-1) Kubernetes multi-tenancy works only when you treat namespaces as one control in a larger isolation stack. A shared cluster needs per-tenant namespaces, namespace-scoped RBAC, Pod Security Admission labels, ResourceQuota and LimitRange defaults, and default-deny NetworkPolicies with explicit exceptions. If you use Flux CD, you also need controller lockdown and service-account impersonation so one tenant's GitOps objects cannot reach across namespaces. The practical goal is not perfect isolation from a namespace alone, but predictable blast-radius control for teams that share a cluster. ![Generated diagram showing tenant namespaces, default-deny network policy, and Flux GitOps service-account boundaries in a shared Kubernetes cluster.](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjAwIDYzMCIgd2lkdGg9IjEyMDAiIGhlaWdodD0iNjMwIiByb2xlPSJpbWciIGFyaWEtbGFiZWxsZWRieT0idGl0bGUgZGVzYyIgc3R5bGU9ImRpc3BsYXk6YmxvY2s7d2lkdGg6MTAwJTtoZWlnaHQ6YXV0bzsiPgogIDx0aXRsZSBpZD0idGl0bGUiPkt1YmVybmV0ZXMgbXVsdGktdGVuYW5jeSBib3VuZGFyeSBkaWFncmFtPC90aXRsZT4KICA8ZGVzYyBpZD0iZGVzYyI+QSBnZW5lcmF0ZWQgaGVybyBncmFwaGljIHNob3dpbmcgdGVuYW50IG5hbWVzcGFjZXMsIG5ldHdvcmsgcG9saWN5IGNvbnRyb2xzLCBhbmQgRmx1eCBHaXRPcHMgc2VydmljZS1hY2NvdW50IGltcGVyc29uYXRpb24gaW5zaWRlIGEgc2hhcmVkIEt1YmVybmV0ZXMgY2x1c3Rlci48L2Rlc2M+CiAgPGRlZnM+CiAgICA8bGluZWFyR3JhZGllbnQgaWQ9ImJnIiB4MT0iMCIgeDI9IjEiIHkxPSIwIiB5Mj0iMSI+CiAgICAgIDxzdG9wIG9mZnNldD0iMCUiIHN0b3AtY29sb3I9IiMwZjE3MmEiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSI1NSUiIHN0b3AtY29sb3I9IiMxZDRlZDgiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxMDAlIiBzdG9wLWNvbG9yPSIjMGY3NjZlIi8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGxpbmVhckdyYWRpZW50IGlkPSJjYXJkQSIgeDE9IjAiIHgyPSIxIiB5MT0iMCIgeTI9IjEiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZWZmNmZmIi8+CiAgICAgIDxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2RiZWFmZSIvPgogICAgPC9saW5lYXJHcmFkaWVudD4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iY2FyZEIiIHgxPSIwIiB4Mj0iMSIgeTE9IjAiIHkyPSIxIj4KICAgICAgPHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2VjZmVmZiIvPgogICAgICA8c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNjY2ZiZjEiLz4KICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICA8ZmlsdGVyIGlkPSJzaGFkb3ciIHg9Ii0yMCUiIHk9Ii0yMCUiIHdpZHRoPSIxNDAlIiBoZWlnaHQ9IjE0MCUiPgogICAgICA8ZmVEcm9wU2hhZG93IGR4PSIwIiBkeT0iMTYiIHN0ZERldmlhdGlvbj0iMTgiIGZsb29kLWNvbG9yPSIjMDIwNjE3IiBmbG9vZC1vcGFjaXR5PSIwLjIyIi8+CiAgICA8L2ZpbHRlcj4KICA8L2RlZnM+CgogIDxyZWN0IHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjYzMCIgcng9IjI4IiBmaWxsPSJ1cmwoI2JnKSIvPgogIDxjaXJjbGUgY3g9IjEwODAiIGN5PSI5MiIgcj0iMTUwIiBmaWxsPSIjNjdlOGY5IiBvcGFjaXR5PSIwLjA4Ii8+CiAgPGNpcmNsZSBjeD0iMTI4IiBjeT0iNTM0IiByPSIxNzAiIGZpbGw9IiM5M2M1ZmQiIG9wYWNpdHk9IjAuMDciLz4KCiAgPHRleHQgeD0iNzQiIHk9IjkyIiBmaWxsPSIjYmZkYmZlIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjIyIiBmb250LXdlaWdodD0iNzAwIiBsZXR0ZXItc3BhY2luZz0iNCI+U0hBUkVEIENMVVNURVIgSVNPTEFUSU9OPC90ZXh0PgogIDx0ZXh0IHg9Ijc0IiB5PSIxNTIiIGZpbGw9IiNmZmZmZmYiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iNDgiIGZvbnQtd2VpZ2h0PSI4MDAiPkt1YmVybmV0ZXMgTXVsdGktVGVuYW5jeTwvdGV4dD4KICA8dGV4dCB4PSI3NCIgeT0iMjA0IiBmaWxsPSIjZGJlYWZlIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjI0Ij5UZW5hbnQgbmFtZXNwYWNlcywgZGVmYXVsdC1kZW55IHRyYWZmaWMsIGFuZCBHaXRPcHMgYXV0b21hdGlvbiB0aGF0IHN0YXlzIGluc2lkZSB0aGUgYm91bmRhcnkuPC90ZXh0PgoKICA8ZyBmaWx0ZXI9InVybCgjc2hhZG93KSI+CiAgICA8cmVjdCB4PSI3MiIgeT0iMjYwIiB3aWR0aD0iNDg0IiBoZWlnaHQ9IjI5MiIgcng9IjI4IiBmaWxsPSJ1cmwoI2NhcmRBKSIvPgogICAgPHJlY3QgeD0iNjQ0IiB5PSIyNjAiIHdpZHRoPSI0ODQiIGhlaWdodD0iMjkyIiByeD0iMjgiIGZpbGw9InVybCgjY2FyZEIpIi8+CiAgPC9nPgoKICA8dGV4dCB4PSIxMTAiIHk9IjMxNCIgZmlsbD0iIzFkNGVkOCIgZm9udC1mYW1pbHk9IlNlZ29lIFVJLCBBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIzMCIgZm9udC13ZWlnaHQ9IjgwMCI+VGVuYW50IE5hbWVzcGFjZXM8L3RleHQ+CiAgPHRleHQgeD0iMTEwIiB5PSIzNDYiIGZpbGw9IiMxZTNhOGEiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjAiPk9uZSBuYW1lc3BhY2UgcGVyIHRlYW0gd2l0aCBwb2xpY3kgbGFiZWxzIGFuZCBxdW90YXM8L3RleHQ+CgogIDxyZWN0IHg9IjExMiIgeT0iMzg0IiB3aWR0aD0iMTUyIiBoZWlnaHQ9IjExNiIgcng9IjIwIiBmaWxsPSIjOTNjNWZkIi8+CiAgPHRleHQgeD0iMTg4IiB5PSI0MjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMxZTNhOGEiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI4MDAiPnRlbmFudC1hPC90ZXh0PgogIDx0ZXh0IHg9IjE4OCIgeT0iNDUyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMWUzYThhIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4Ij5hcHBzPC90ZXh0PgogIDx0ZXh0IHg9IjE4OCIgeT0iNDc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMWUzYThhIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4Ij5yYmFjICsgcXVvdGE8L3RleHQ+CgogIDxyZWN0IHg9IjMwMCIgeT0iMzg0IiB3aWR0aD0iMTUyIiBoZWlnaHQ9IjExNiIgcng9IjIwIiBmaWxsPSIjYmZkYmZlIi8+CiAgPHRleHQgeD0iMzc2IiB5PSI0MjQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMxZTNhOGEiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMjAiIGZvbnQtd2VpZ2h0PSI4MDAiPnRlbmFudC1iPC90ZXh0PgogIDx0ZXh0IHg9IjM3NiIgeT0iNDUyIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMWUzYThhIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4Ij5hcHBzPC90ZXh0PgogIDx0ZXh0IHg9IjM3NiIgeT0iNDc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMWUzYThhIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4Ij5wc2EgcmVzdHJpY3RlZDwvdGV4dD4KCiAgPHBhdGggZD0iTTI2NCA0NDJoMjQiIHN0cm9rZT0iIzFkNGVkOCIgc3Ryb2tlLXdpZHRoPSIxMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgPHBhdGggZD0iTTQ1MiA0NDJoNDgiIHN0cm9rZT0iIzk0YTNiOCIgc3Ryb2tlLXdpZHRoPSIxMCIgc3Ryb2tlLWRhc2hhcnJheT0iMTIgMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxjaXJjbGUgY3g9IjUyMiIgY3k9IjQ0MiIgcj0iMTYiIGZpbGw9IiNmOGZhZmMiIHN0cm9rZT0iIzQ3NTU2OSIgc3Ryb2tlLXdpZHRoPSI2Ii8+CiAgPHBhdGggZD0iTTUxMiA0NDJoMjAiIHN0cm9rZT0iIzQ3NTU2OSIgc3Ryb2tlLXdpZHRoPSI2IiBzdHJva2UtbGluZWNhcD0icm91bmQiLz4KCiAgPGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDg2IDMzNCkiPgogICAgPHBhdGggZD0iTTM2IDBsNDggMTh2NDJjMCA0Mi0yNiA4MC00OCA5NEMxNCAxNDAtMTIgMTAyLTEyIDYwVjE4TDM2IDB6IiBmaWxsPSIjMjJjNTVlIi8+CiAgICA8cGF0aCBkPSJNMzYgMThsMzIgMTJ2MjhjMCAyOC0xNiA1NC0zMiA2OEMyMCAxMTIgNCA4NiA0IDU4VjMwbDMyLTEyeiIgZmlsbD0iI2RjZmNlNyIvPgogICAgPHBhdGggZD0iTTIwIDYwbDEwIDEwIDIyLTI4IiBmaWxsPSJub25lIiBzdHJva2U9IiMxNjY1MzQiIHN0cm9rZS13aWR0aD0iMTAiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgogIDwvZz4KCiAgPHRleHQgeD0iNjgyIiB5PSIzMTQiIGZpbGw9IiMwZjc2NmUiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMzAiIGZvbnQtd2VpZ2h0PSI4MDAiPlRyYWZmaWMgYW5kIEdpdE9wcyBDb250cm9sPC90ZXh0PgogIDx0ZXh0IHg9IjY4MiIgeT0iMzQ2IiBmaWxsPSIjMTY2NTM0IiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjIwIj5EZWZhdWx0IGRlbnkgYnkgcG9saWN5LCBleHBsaWNpdCBleGNlcHRpb25zLCBzY29wZWQgRmx1eCByZWNvbmNpbGlhdGlvbjwvdGV4dD4KCiAgPHJlY3QgeD0iNjg0IiB5PSIzODgiIHdpZHRoPSIxNTYiIGhlaWdodD0iMTA0IiByeD0iMjAiIGZpbGw9IiM2N2U4ZjkiLz4KICA8dGV4dCB4PSI3NjIiIHk9IjQyNiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzEzNGU0YSIgZm9udC1mYW1pbHk9IlNlZ29lIFVJLCBBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxOSIgZm9udC13ZWlnaHQ9IjgwMCI+TmV0d29ya1BvbGljeTwvdGV4dD4KICA8dGV4dCB4PSI3NjIiIHk9IjQ1NCIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZmlsbD0iIzEzNGU0YSIgZm9udC1mYW1pbHk9IlNlZ29lIFVJLCBBcmlhbCwgc2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNyI+ZGVmYXVsdCBkZW55PC90ZXh0PgogIDx0ZXh0IHg9Ijc2MiIgeT0iNDc4IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBmaWxsPSIjMTM0ZTRhIiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE3Ij5hbGxvdyBETlMgYW5kIGluZ3Jlc3M8L3RleHQ+CgogIDxwYXRoIGQ9Ik04NDIgNDQwaDY0IiBzdHJva2U9IiMwZjc2NmUiIHN0cm9rZS13aWR0aD0iMTIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDxwb2x5Z29uIHBvaW50cz0iOTE2LDQ0MCA4OTAsNDI0IDg5MCw0NTYiIGZpbGw9IiMwZjc2NmUiLz4KCiAgPHJlY3QgeD0iOTMwIiB5PSIzODgiIHdpZHRoPSIxNTQiIGhlaWdodD0iMTA0IiByeD0iMjAiIGZpbGw9IiNhN2YzZDAiLz4KICA8dGV4dCB4PSIxMDA3IiB5PSI0MjYiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMxNjY1MzQiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTkiIGZvbnQtd2VpZ2h0PSI4MDAiPkZsdXggS3VzdG9taXphdGlvbjwvdGV4dD4KICA8dGV4dCB4PSIxMDA3IiB5PSI0NTQiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMxNjY1MzQiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTciPnNlcnZpY2VBY2NvdW50TmFtZTwvdGV4dD4KICA8dGV4dCB4PSIxMDA3IiB5PSI0NzgiIHRleHQtYW5jaG9yPSJtaWRkbGUiIGZpbGw9IiMxNjY1MzQiIGZvbnQtZmFtaWx5PSJTZWdvZSBVSSwgQXJpYWwsIHNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTciPnRlbmFudC1yZWNvbmNpbGVyPC90ZXh0PgoKICA8ZyBmaWxsPSIjMTY2NTM0IiBmb250LWZhbWlseT0iU2Vnb2UgVUksIEFyaWFsLCBzYW5zLXNlcmlmIiBmb250LXNpemU9IjE4Ij4KICAgIDxjaXJjbGUgY3g9IjcwMCIgY3k9IjUyNiIgcj0iNiIgZmlsbD0iIzE0YjhhNiIvPgogICAgPHRleHQgeD0iNzE4IiB5PSI1MzIiPk5vIGNyb3NzLW5hbWVzcGFjZSByZWZzPC90ZXh0PgogICAgPGNpcmNsZSBjeD0iODk2IiBjeT0iNTI2IiByPSI2IiBmaWxsPSIjMTRiOGE2Ii8+CiAgICA8dGV4dCB4PSI5MTQiIHk9IjUzMiI+TmFtZXNwYWNlLXNjb3BlZCBSQkFDPC90ZXh0PgogIDwvZz4KPC9zdmc+) A shared-cluster multi-tenancy model needs namespace boundaries, default-deny network policy, and GitOps reconciliation scoped to tenant service accounts. ## Namespace-Per-Tenant Is a Starting Point, Not a Complete Isolation Model [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-2) The original draft was directionally right: namespaces and NetworkPolicies matter, and Flux CD needs extra care in a shared cluster. The missing part is that namespace isolation by itself is not the same thing as tenant isolation. The official Kubernetes multi-tenancy guidance is explicit about the trade-off. You can share a cluster by separating workloads into namespaces, or you can give tenants stronger isolation with separate clusters or virtual control planes. A namespace-based model is practical when teams trust the platform team and mostly need blast-radius reduction, policy separation, and self-service. It is the wrong model when tenants need hard isolation from cluster-scoped APIs, infrastructure admins, or each other's control planes. For most internal platform teams, the safer framing is this: **A tenant boundary in Kubernetes is a stack, not a single object.** - `Namespace`: resource scoping and organization - `RBAC`: who can read or mutate namespaced resources - `Pod Security Admission`: what pods are allowed to run - `ResourceQuota` and `LimitRange`: how much a tenant can consume by default - `NetworkPolicy`: which pods can talk to which pods - `Flux impersonation and controller lockdown`: how GitOps automation stays inside the tenant boundary If one of those layers is missing, the cluster may still look multi-tenant in diagrams while behaving like a shared free-for-all in production. ## 1. Build a Namespace Boundary the API Server Can Enforce [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-3) Namespaces are still the right first move because most application resources are namespaced. They also give you a natural unit for quotas, RBAC, and policy labels. The Kubernetes docs note that namespaces are intended for environments with many users spread across multiple teams or projects, and the platform automatically applies the immutable label `kubernetes.io/metadata.name` to each namespace. That label is useful later in `NetworkPolicy` selectors because it gives you a stable namespace identity without inventing another naming convention. What namespaces do **not** isolate: - cluster-scoped resources such as CRDs, nodes, and storage classes - overly broad `ClusterRoleBinding` permissions - network paths when the CNI is not enforcing `NetworkPolicy` - unsafe pod settings if you do not apply Pod Security Admission or equivalent policy controls Start with a tenant namespace that carries security labels from day one: ```yaml apiVersion: v1 kind: Namespace metadata: name: tenant-a-prod labels: tenant.platform.example/name: tenant-a pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restricted ``` Then attach quota and default sizing controls to the same namespace. This prevents the classic shared-cluster failure mode where one noisy tenant consumes all schedulable capacity because requests and limits were never standardized. ```yaml apiVersion: v1 kind: ResourceQuota metadata: name: tenant-a-quota namespace: tenant-a-prod spec: hard: requests.cpu: "8" requests.memory: 16Gi limits.cpu: "16" limits.memory: 32Gi pods: "100" persistentvolumeclaims: "20" --- apiVersion: v1 kind: LimitRange metadata: name: tenant-a-defaults namespace: tenant-a-prod spec: limits: - type: Container defaultRequest: cpu: 100m memory: 128Mi default: cpu: 500m memory: 512Mi ``` That is the minimum baseline for a tenant namespace. Without it, multi-tenancy becomes an honor system. ## 2. NetworkPolicy Works Best When You Start From Default Deny [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-4) Kubernetes `NetworkPolicy` rules are often explained too casually. Two details matter in practice: 1. Policies are additive, not first-match. 2. For traffic between two pods to be allowed, the effective egress rules on the source and ingress rules on the destination both need to allow that path. That means tenant isolation should start with deny-by-default policy and then add only the flows a tenant actually needs. The Kubernetes multi-tenancy guide also warns against broad selectors that accidentally include many namespaces. In particular, an empty `namespaceSelector: {}` effectively means "all namespaces," which is the opposite of tenant isolation. Begin with a namespace-wide deny: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny namespace: tenant-a-prod spec: podSelector: {} policyTypes: - Ingress - Egress ``` Then add the minimum safe exceptions. Most tenants need at least same-namespace traffic and DNS: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-same-namespace-and-dns namespace: tenant-a-prod spec: podSelector: {} policyTypes: - Ingress - Egress ingress: - from: - podSelector: {} egress: - to: - podSelector: {} - to: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: kube-system podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 - protocol: TCP port: 53 ``` If tenants must consume a shared ingress or observability namespace, make that exception explicit instead of falling back to an open namespace selector: ```yaml apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-from-shared-ingress namespace: tenant-a-prod spec: podSelector: matchLabels: app.kubernetes.io/part-of: tenant-a policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-shared ``` Two operational caveats are easy to miss: - `NetworkPolicy` only works if your CNI plugin implements it. - DNS pod labels vary by distribution, so the `k8s-app: kube-dns` selector may need to be adjusted for your cluster. That is why production multi-tenancy needs policy tests in CI or conformance checks after every cluster upgrade. A YAML file that applied successfully is not proof that isolation still works. ## 3. RBAC Defines Whether a Tenant Can Stay Inside Its Namespace [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-5) A namespace is only meaningful if the identities acting inside it are namespace-scoped too. The Kubernetes RBAC guidance is clear about minimizing privileged tokens and avoiding broad bindings. In a multi-tenant cluster, the common failure is not malicious behavior. It is accidental over-permissioning through convenience roles or inherited cluster-admin access. For tenant automation, prefer a dedicated service account with a custom `Role` instead of handing out a namespace-wide admin pattern by default: ```yaml apiVersion: v1 kind: ServiceAccount metadata: name: tenant-reconciler namespace: tenant-a-prod --- apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: tenant-reconciler namespace: tenant-a-prod rules: - apiGroups: [""] resources: ["configmaps", "secrets", "services", "serviceaccounts"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["deployments", "statefulsets", "daemonsets", "replicasets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["batch"] resources: ["jobs", "cronjobs"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["networking.k8s.io"] resources: ["ingresses", "networkpolicies"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: tenant-reconciler namespace: tenant-a-prod subjects: - kind: ServiceAccount name: tenant-reconciler namespace: tenant-a-prod roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: tenant-reconciler ``` This pattern keeps the tenant's automation powerful enough to do useful work while still stopping it at the namespace boundary. ## 4. Flux CD Can Preserve or Break Tenant Isolation [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-6) This is the part the original post only hinted at. In a shared cluster, GitOps can either reinforce tenancy or punch straight through it. Flux provides the primitives to keep tenant reconciliations inside a namespace, but they only help if you configure them intentionally: - `Kustomization.spec.serviceAccountName` lets a reconciliation run as a chosen service account. - `HelmRelease.spec.serviceAccountName` does the same for Helm-based workloads. - Flux security best practices recommend lockdown flags such as `--no-cross-namespace-refs=true` and `--no-remote-bases=true` for shared clusters. - Platform admins can also enforce a default impersonation account with `--default-service-account` so tenants do not silently reconcile with broader privileges than intended. The tenant-facing object should reference the namespace-scoped service account you created earlier: ```yaml apiVersion: kustomize.toolkit.fluxcd.io/v1 kind: Kustomization metadata: name: tenant-a-apps namespace: tenant-a-prod spec: interval: 10m path: ./clusters/prod/tenant-a prune: true serviceAccountName: tenant-reconciler sourceRef: kind: GitRepository name: tenant-a-config ``` At the platform layer, the controller install should be patched with shared-cluster safeguards. The exact bootstrap mechanism differs by install method, but the intent should be unambiguous: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: kustomize-controller namespace: flux-system spec: template: spec: containers: - name: manager args: - --no-cross-namespace-refs=true - --no-remote-bases=true - --default-service-account=default ``` If you skip this layer, you have not really built multi-tenant GitOps. You have built shared automation with tenant-flavored namespaces. ## 5. Why This Layered Model Holds Up Better Than Namespace-Only Guidance [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-7) The reason platform teams struggle with Kubernetes multi-tenancy is that they keep asking one primitive to do every job. Namespaces are asked to provide identity isolation, network isolation, workload hardening, resource fairness, and GitOps authorization all at once. They cannot. The layered model works because each control addresses a different escape path: | Layer | Protects Against | Common Failure | | --- | --- | --- | | Namespace | accidental object overlap | everything lands in `default` | | RBAC | API misuse across tenant boundaries | broad `ClusterRoleBinding` | | Pod Security Admission | privileged or unsafe pod specs | tenants can request risky pod settings | | ResourceQuota and LimitRange | noisy-neighbor resource exhaustion | requests and limits omitted | | NetworkPolicy | lateral traffic between tenants | flat east-west connectivity | | Flux impersonation | GitOps reconciling with excessive privileges | controller runs effectively as cluster admin | That is also why the Kubernetes docs present separate clusters or virtual control planes as valid alternatives. When a tenant needs cluster-scoped API control, materially different compliance policy, or strong distrust boundaries, namespace-based sharing stops being the pragmatic option. ## What To Do Next [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-8) If you already run a shared cluster, the fastest hardening sequence is usually: 1. Create one namespace per tenant and label each namespace with Pod Security Admission policy. 2. Add `ResourceQuota` and `LimitRange` objects before opening self-service onboarding. 3. Enforce default-deny ingress and egress, then add explicit allow rules for DNS and approved shared services. 4. Replace cluster-wide tenant automation with namespace-scoped service accounts and `RoleBinding`. 5. Patch Flux controllers for shared-cluster lockdown and require `serviceAccountName` in tenant reconciliations. 6. Revisit whether high-trust and low-trust tenants should really share the same cluster at all. That sequence is more useful than debating whether namespaces are "enough." The real question is whether the whole isolation stack is coherent. ## Frequently Asked Questions [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-9) **Q: Are namespaces enough for Kubernetes multi-tenancy?** No. Namespaces scope namespaced resources, but shared-cluster safety also depends on RBAC, Pod Security Admission, resource controls, and network isolation. Without those layers, tenants still share too much blast radius. **Q: What is the safest default `NetworkPolicy` posture for tenants?** Default-deny ingress and egress, then add narrow allow rules for same-namespace traffic, DNS, ingress, and approved platform services. That aligns with how Kubernetes evaluates policies and reduces accidental cross-tenant reachability. **Q: How should Flux CD be configured in a multi-tenant cluster?** Run tenant reconciliations with namespace-scoped service accounts and enable controller safeguards that block cross-namespace references and remote bases where appropriate. In shared clusters, GitOps permissions should be at least as constrained as a human operator's permissions inside that namespace. **Q: When should I stop sharing a cluster and move to a stronger boundary?** Move to separate clusters or virtual control planes when tenants require hard isolation, cluster-scoped customization, materially different compliance controls, or stronger distrust assumptions. Namespace-based sharing is an efficiency choice, not a universal security boundary. ## Resources [Section link](https://devopsdreams.blogspot.com/2026/03/kubernetes-multi-tenancy-with_01212499564.html#dd-section-10) - [Kubernetes multi-tenancy](https://kubernetes.io/docs/concepts/security/multi-tenancy/) - [Kubernetes namespaces](https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/) - [Kubernetes NetworkPolicy](https://kubernetes.io/docs/concepts/services-networking/network-policies/) - [Pod Security Admission](https://kubernetes.io/docs/concepts/security/pod-security-admission/) - [ResourceQuota](https://kubernetes.io/docs/concepts/policy/resource-quotas/) - [LimitRange](https://kubernetes.io/docs/concepts/policy/limit-range/) - [RBAC good practices](https://kubernetes.io/docs/concepts/security/rbac-good-practices/) - [Flux security best practices](https://fluxcd.io/flux/security/best-practices/) - [Flux Kustomizations](https://fluxcd.io/flux/components/kustomize/kustomizations/) - [Flux HelmReleases](https://fluxcd.io/flux/components/helm/helmreleases/)