How to Manage Namespaces in Kubernetes

How to Manage Namespaces in Kubernetes

When I first started working with Kubernetes, namespaces felt like an afterthought — just a way to organize things in kubectl get pods. It didn’t take long running a real multi-team cluster to realize namespaces are actually one of the most important organizational and security primitives in the whole system. In this guide, I’ll walk through everything from the basics to advanced namespace-based multi-tenancy patterns on AWS EKS.

What Is a Namespace?

A namespace is a way to divide cluster resources between multiple users, teams, or applications within a single physical cluster. Kubernetes objects like Pods, Services, Deployments, ConfigMaps, and Secrets are namespaced — meaning their names only need to be unique within a namespace, not across the whole cluster. Cluster-scoped objects like Nodes, PersistentVolumes, and ClusterRoles exist outside any namespace.

Every cluster ships with four namespaces by default:

  • default — where objects land if you don’t specify a namespace
  • kube-system — core Kubernetes system components (kube-dns, kube-proxy, etc.)
  • kube-public — readable by all users, including unauthenticated ones; rarely used directly
  • kube-node-lease — holds Lease objects used for node heartbeats

Why Namespaces Matter

Namespaces give you four practical levers:

  1. Resource isolation — via ResourceQuotas and LimitRanges scoped per namespace
  2. Access control — RBAC Roles and RoleBindings are namespace-scoped
  3. Network isolation — NetworkPolicies commonly scope traffic rules by namespace
  4. Organizational clarity — separating dev, staging, prod, or per-team/per-product boundaries

Creating and Managing Namespaces

The simplest way:

kubectl create namespace team-payments

Or declaratively, which is what you want for any real environment:

apiVersion: v1
kind: Namespace
metadata:
  name: team-payments
  labels:
    team: payments
    environment: production
    pod-security.kubernetes.io/enforce: restricted
  annotations:
    owner: payments-team@company.com
kubectl apply -f team-payments-namespace.yaml

List all namespaces:

kubectl get namespaces
NAME              STATUS   AGE
default           Active   200d
kube-node-lease   Active   200d
kube-public       Active   200d
kube-system       Active   200d
team-payments     Active   2m

Set a default namespace for your current context so you don’t have to type -n team-payments on every command:

kubectl config set-context --current --namespace=team-payments

Delete a namespace (this cascades and deletes everything inside it — be careful):

kubectl delete namespace team-payments

Resource Quotas

A ResourceQuota constrains aggregate resource consumption per namespace — critical in any shared, multi-tenant cluster.

apiVersion: v1
kind: ResourceQuota
metadata:
  name: payments-quota
  namespace: team-payments
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    persistentvolumeclaims: "10"
    services.loadbalancers: "2"
    pods: "50"

Apply and check usage:

kubectl apply -f payments-quota.yaml
kubectl describe resourcequota payments-quota -n team-payments
Name:                   payments-quota
Namespace:              team-payments
Resource                Used   Hard
--------                ----   ----
limits.cpu              12     40
limits.memory           24Gi   80Gi
pods                    18     50
requests.cpu            6      20
requests.memory         12Gi   40Gi
services.loadbalancers  1      2

Once a ResourceQuota exists in a namespace, every pod created there must specify resource requests and limits, or the API server will reject it — this is a common source of confusion for teams that add a quota to an existing namespace without updating their manifests.

LimitRanges

While ResourceQuota caps the namespace total, LimitRange sets defaults and bounds per individual pod/container — useful so a single misconfigured deployment doesn’t eat the whole quota.

apiVersion: v1
kind: LimitRange
metadata:
  name: payments-limits
  namespace: team-payments
spec:
  limits:
    - type: Container
      default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      max:
        cpu: "2"
        memory: 2Gi
      min:
        cpu: 50m
        memory: 64Mi

RBAC Within Namespaces

Namespaces become genuinely powerful when combined with RBAC. Here’s a Role scoped to team-payments that lets developers manage Deployments and read Pods, but not touch Secrets:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: team-payments
  name: developer
rules:
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch"]
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-binding
  namespace: team-payments
subjects:
  - kind: Group
    name: payments-devs
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: developer
  apiGroup: rbac.authorization.k8s.io

On EKS, this pairs with aws-auth ConfigMap mappings (or the newer EKS access entries API) that map IAM identities to Kubernetes groups like payments-devs.

Namespace-Scoped Network Policies

By default, all pods can talk to all other pods across namespaces — Kubernetes networking is flat unless you restrict it. A common production pattern is default-deny plus explicit allow rules:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: team-payments
spec:
  podSelector: {}
  policyTypes:
    - Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-gateway
  namespace: team-payments
spec:
  podSelector:
    matchLabels:
      app: payments-api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              team: gateway
      ports:
        - protocol: TCP
          port: 8080

Note that this requires a CNI that enforces NetworkPolicy — the default AWS VPC CNI historically didn’t, though it now supports NetworkPolicy enforcement in recent versions; otherwise you’d typically pair EKS with Calico or Cilium for this.

Multi-Tenancy Patterns Using Namespaces

For most organizations, namespace-per-team or namespace-per-environment is the pragmatic middle ground between “one shared cluster with no isolation” and “one cluster per team” (which multiplies operational overhead). A common EKS layout:

production-payments
production-checkout
production-inventory
staging-payments
staging-checkout
staging-inventory

Combine this with:

  • Hierarchical namespace controllers (like the CNCF hnc project) if you need namespace inheritance
  • Kyverno or Gatekeeper policies to enforce naming conventions and mandatory labels on namespace creation
  • Karpenter or Cluster Autoscaler node groups tagged and tainted per namespace group if you need hard compute isolation, not just logical isolation

For genuinely hostile multi-tenancy (untrusted third-party workloads), namespaces alone are not a security boundary — consider separate clusters, or EKS with Firecracker-based isolation via Fargate, since a container escape can still cross namespace boundaries at the kernel/node level.

Automating Namespace Provisioning in CI/CD

A realistic GitOps pattern: teams request namespaces via a pull request to a namespaces/ directory, and ArgoCD or Flux syncs them automatically along with their quotas, RBAC, and network policies.

# Example Argo CD Application targeting a namespaces directory
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: namespace-provisioning
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/company/k8s-platform
    path: namespaces
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
EOF

Troubleshooting

  • Error from server (NotFound): namespaces "x" not found — check kubectl get ns for typos, or confirm your kubeconfig context points at the right cluster.
  • Pods stuck Pending after adding a ResourceQuota — check kubectl describe pod for FailedCreate events; usually missing resource requests/limits.
  • Namespace stuck in Terminating — usually a finalizer on a resource inside it (often a custom resource whose controller is gone). Check with:
kubectl get namespace team-payments -o json | jq '.spec.finalizers, .status'

If genuinely stuck, you can force-remove finalizers as a last resort (understand the risk — this can leave orphaned cloud resources):

kubectl get namespace team-payments -o json | \
  jq '.spec.finalizers = []' | \
  kubectl replace --raw "/api/v1/namespaces/team-payments/finalize" -f -

Cross-Namespace Communication Patterns

Even with isolation in place, teams almost always need some cross-namespace traffic — a shared internal API, a common logging pipeline, a shared ingress controller. The cleanest pattern is a dedicated shared-services namespace with explicitly allow-listed NetworkPolicy rules pointing at it, rather than punching arbitrary holes between individual team namespaces:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-shared-services-egress
  namespace: team-payments
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: shared-services
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53

Note the explicit DNS egress rule — a very common mistake when adopting default-deny NetworkPolicies is forgetting that pods still need to resolve DNS, which itself is cross-namespace traffic to kube-system. Without an explicit allow rule for port 53, a default-deny egress policy silently breaks all outbound DNS resolution, which manifests as confusing “service not found” errors that have nothing to do with the actual target service.

Namespace Naming Conventions

A surprisingly high-leverage decision early on: settling on a consistent naming scheme before namespaces sprawl across dozens of teams. A common, readable pattern is <environment>-<team>-<optional-component>:

prod-payments
prod-payments-batch
staging-payments
dev-payments

This sorts cleanly in kubectl get ns, makes RBAC and NetworkPolicy label selectors predictable, and makes it trivial to write a Kyverno policy that validates new namespaces match the convention:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: enforce-namespace-naming
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-naming-pattern
      match:
        any:
          - resources:
              kinds:
                - Namespace
      validate:
        message: "Namespace names must follow <env>-<team>[-<component>] convention."
        pattern:
          metadata:
            name: "?(dev|staging|prod)-*"

Best Practices

  • Never deploy directly into default in any real environment — always create purpose-named namespaces.
  • Always pair ResourceQuota with LimitRange to avoid pods getting stuck due to missing defaults.
  • Label namespaces consistently (team, environment, cost-center) — this pays off enormously for cost allocation and policy targeting later.
  • Treat namespace manifests as code, reviewed and synced via GitOps, not created ad hoc with kubectl create namespace.
  • Use Pod Security Admission labels at the namespace level as your security baseline (see the companion article on Pod Security).

Summary

Namespaces are the backbone of multi-tenant Kubernetes operations — they scope RBAC, resource quotas, network policies, and organizational ownership. On their own they’re a logical boundary, not a hard security boundary, so pair them with quotas, RBAC, network policies, and (for genuinely untrusted workloads) additional isolation like separate clusters or Fargate. Treat namespace creation as a governed, automated process rather than a manual kubectl create step, and your cluster will scale far more gracefully as teams and workloads grow.

References

Total
0
Shares

Leave a Reply

Previous Post
How to Set Up a Kubernetes Development Environment with Minikube

How to Set Up a Kubernetes Development Environment with Minikube

Next Post
How to Configure PodSecurityPolicies in Kubernetes

How to Configure PodSecurityPolicies in Kubernetes

Related Posts