Skip to main content
cd ../config-traps
risk/register/namespace-migration-erases-networkpolicy-isolation.html
Kubernetes Networkinghigh severityKubernetes

Namespace Migration Erases NetworkPolicy Isolation

Overview

Namespace renames or GitOps cutovers leave NetworkPolicy objects bound to the old namespace name, so the new namespace has none. Kubernetes defaults to allow-all networking, silently exposing PCI-scoped pods to unauthenticated traffic from any namespace, with no admission error to flag it.

At a glance

Unsafe setting
NetworkPolicy manifests hardcode metadata.namespace instead of deriving it from the namespace generator
Failure trigger
Namespace rename via kustomize overlay, Argo CD app rename, or Helm cutover leaves policy orphaned in old namespace
Blast radius
Pods in the new namespace accept unauthenticated L3/L4 traffic from any pod on the CNI overlay, bypassing mesh mTLS entirely
Recommended control
Generate default-deny-all NetworkPolicy per namespace via Kyverno with synchronize:true, plus CI gate checking policy presence pre-cutover

The Trap

Namespace-scoped NetworkPolicy objects orphaned during a namespace rename or recreation, silently restoring the cluster’s native default-allow networking posture for every pod that lands in the new namespace.

The Default State

Kubernetes ships with no built-in network isolation. Without a NetworkPolicy object whose podSelector matches a given pod, every CNI that implements the NetworkPolicy API — Calico, Cilium, Weave — permits all ingress and egress for that pod by default. Platform teams write these policies with a literal metadata.namespace field, for example payments-prod. When the workload migrates to payments-prod-v2 via a kustomize overlay rename, an Argo CD application rename, or a Helm release cutover, the NetworkPolicy manifest frequently isn’t re-templated to the new namespace because the pipeline treats it as a static resource rather than a value derived from the namespace generator. kubectl get networkpolicy -n payments-prod-v2 returns nothing, and no admission controller in a stock cluster requires one to exist.

The Blast Radius

Pods in payments-prod-v2 are now reachable from any other pod on the same CNI overlay, including lower-trust namespaces sharing the same IPAM range. PCI-scoped services that were previously locked behind explicit allow lists in payments-prod now accept unauthenticated TCP from anywhere in the cluster, and there is no signal to catch it: kube-apiserver logs nothing, because an absent policy isn’t a denied request, it’s just the platform’s default state. A compromised debug pod in an unrelated namespace can reach the database service directly on port 5432, bypassing service mesh mTLS entirely because that enforcement sits at L7 and never inspects raw L3/L4 socket permissions. Compliance drift accumulates for weeks before an auditor, not a monitoring system, notices the isolation boundary is gone.

The Lead Mechanic Fix

Stop treating NetworkPolicy as a manifest that travels with the workload. Bind a default-deny-all policy to namespace creation itself using a Kyverno generate rule with synchronize: true, so the policy is stamped out automatically and can’t be removed without deleting the namespace:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: generate-default-deny-netpol
spec:
  rules:
  – name: default-deny-all
    match:
      resources:
        kinds: [Namespace]
    generate:
      kind: NetworkPolicy
      synchronize: true
      data:
        spec:
          podSelector: {}
          policyTypes: [Ingress, Egress]

Layer explicit allow rules on top as separate objects, then add a CI gate that runs before any migration cutover: kubectl get networkpolicy –all-namespaces -o json piped through jq to confirm every namespace carries at least one deny-all baseline plus the expected allow rules, failing the pipeline if the new namespace comes up naked.