This post is for platform teams running multi-tenant Kubernetes on GCP, where each team or customer gets its own namespace. Network policies are the right tool for isolating services from each other, but they isolate only the pods a policy selects. The real work is setting a default-deny baseline in every tenant namespace and then allowing traffic on purpose. Below I cover what to turn on, the order I’d roll it out in, and where policies stop protecting you.
The default is open
Nothing is isolated out of the box. The Kubernetes documentation on network policies says a pod is non-isolated for both ingress and egress by default. A pod becomes isolated in a direction only when some policy selects it and lists that direction in its policyTypes. Policies are additive: the allowed connections are the union of what the applicable policies allow. A connection between two pods succeeds only if the source’s egress and the destination’s ingress both permit it.
The additive model has a consequence that catches people out. Isolation is a side effect of a policy selecting a pod, and it is tracked separately for each direction. If you delete the last policy that applies to a pod for ingress, that pod is non-isolated for ingress again. Inbound connections that were blocked before can now get through, unless the source pod’s egress policies still stop them. A per-namespace default-deny ingress policy prevents this for ingress, because it keeps every pod selected for ingress no matter what else changes. It does nothing for egress. That direction needs its own default-deny egress baseline.
Make sure something enforces it
A NetworkPolicy object does nothing by itself. The Kubernetes docs say that creating one without a controller to implement it has no effect. As of November 2026, Google’s GKE network policy guide lists two mutually exclusive managed plugins:
- GKE Dataplane V2, based on Cilium, recommended for all clusters and the default for Autopilot.
- Calico, available only in Standard clusters.
Without a managed plugin, you need a self-managed one to enforce network policy. With no plugin configured at all, policies aren’t enforced. Google recommends removing them in that case, because they might cause unexpected restrictions once you later install a plugin that does enforce them.
I’d use Dataplane V2. It is Google’s recommendation and the Autopilot default, so you stay on the path the platform expects. If you have an older Standard cluster and turn on Calico enforcement instead, plan for the cost. Google says it adds approximately 128 MB to the kube-system memory footprint and needs approximately 300 millicores of CPU. Enabling it also re-creates your nodes, and if you have an active maintenance window, that waits until the window.
The order I’d roll it out in
Google describes the target directly: a tenant-per-namespace model, where policy keeps pods and Services in one namespace from reaching those in another. That is the shape I’d build, in this order:
Default-deny ingress in every tenant namespace. Policies are namespaced, so you need one per namespace. Put it in whatever creates namespaces, not in a runbook. An empty
podSelectorselects all pods in the namespace, though on GKE it skips pods withhostNetwork: true.1 2 3 4 5 6 7 8apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-ingress spec: podSelector: {} policyTypes: - IngressAllow traffic within the namespace, then narrow it as you learn which services actually talk to each other.
Allow cross-namespace traffic explicitly. To allow specific pods in specific namespaces, put
namespaceSelectorandpodSelectorin a singlefromentry. The Kubernetes docs warn that the YAML syntax here is easy to get wrong.Egress last. Egress lockdown breaks things in less obvious ways. On GKE you must allow egress to the metadata server.
Decide who owns the allow-list
Monzo’s write-up on isolating over 1,500 services, published November 6, 2019, is useful here. Its platform had over 9,300 unique connections between services. Its first trial put the list of allowed callers on the receiving service, and two problems followed. Rolling back that service could also roll back a newly allowed caller and suddenly block its traffic. And there was no safe way to test a policy before enforcing it. To get a log-but-allow dry run, Monzo used Calico features that aren’t available through Kubernetes NetworkPolicy.
I’d settle ownership before writing service-level rules. The namespace boundary policies belong to the platform team. Rules inside a namespace belong to the tenant who owns those services. Before you enforce anything, also decide how you will find out what traffic a new policy would drop.
Where it stops
Network policies work at layer 3 and 4: IP addresses and ports for TCP, UDP and SCTP. Traffic from the node a pod runs on is always allowed. On Dataplane V2 you can’t use pod or namespace selectors, or IP blocks, to match traffic to or from hostNetwork pods. The Kubernetes docs say plainly that “isolation” here means “some restrictions apply”, not an absolute boundary.
So when tenants genuinely don’t trust each other, I would not make network policy the only thing separating them. Google frames it as defense in depth, and that is the right way to think of it: one layer among several.
If you run tenants on shared GKE clusters today, first confirm that an enforcing plugin is actually on. Then ship default-deny ingress as part of namespace creation, before you write a single allow rule.
