DevOps · Kubernetes

Kubernetes Network Policies — A Field Guide

Kubernetes allows all pod-to-pod traffic by default until you write a NetworkPolicy — and that policy silently does nothing unless your CNI plugin actually supports enforcing it.

John Kihiu12 min read

The default state of a Kubernetes cluster is that any pod can talk to any other pod, across every namespace, on every port. There's no implicit isolation — a NetworkPolicy resource is how you opt into restricting that, and until you write one, "network security" in your cluster is whatever your ingress controller and application-level auth happen to enforce. The gotcha that catches people after they've already written the policy: it may not do anything at all, because enforcement depends entirely on your CNI plugin.

Default-allow, until you say otherwise

With zero NetworkPolicy resources in a namespace, every pod accepts ingress from every other pod and can make egress to anywhere, cluster-internal or external. This surprises people coming from a traditional network model where isolation is the default and you open specific holes. In Kubernetes it's inverted: a pod running a database with no policy attached is reachable from any compromised pod anywhere else in the cluster, in any namespace, unless something else — a service mesh's mTLS, a firewall, an admission controller — is doing the restricting instead.

A NetworkPolicy with no matching CNI support is silent, not rejected

Kubernetes will happily accept a NetworkPolicy object via kubectl apply even if the cluster's CNI plugin has no enforcement engine at all. kubenet, for instance, implements zero NetworkPolicy support — your policy sits in etcd, valid and applied, doing absolutely nothing. Check what CNI your cluster actually runs before you trust a policy is enforced.

Default-deny: the pattern that actually locks things down

The standard first move in any namespace that needs isolation is a default-deny policy: a NetworkPolicy with an empty podSelector (matching every pod in the namespace) and no ingress or egress rules at all. That flips the namespace from allow-all to deny-all, and then you add specific policies granting exactly the traffic each workload needs. Writing default-deny first and additive allow-rules second is much easier to reason about than starting from allow-all and trying to enumerate everything you want to block.

yaml · default-deny-all
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-api-to-db
  namespace: payments
spec:
  podSelector:
    matchLabels: {app: postgres}
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector: {matchLabels: {app: api}}
      ports:
        - protocol: TCP
          port: 5432

podSelector, namespaceSelector, and ipBlock

Rules are built from three selector types, usually combined. podSelector matches pods by label within the policy's own namespace. namespaceSelector matches based on labels on the namespace itself, which is how you allow traffic from an entire namespace (like kube-system or a shared monitoring namespace) rather than enumerating pods. ipBlock matches CIDR ranges and is the escape hatch for traffic to or from outside the cluster — a managed database, an external API, or an on-prem network reachable via VPN. A rule can combine a namespaceSelector and podSelector in the same "from" entry, which narrows it to specific pods within a specific namespace rather than any pod in that namespace.

The CNI gotcha: not all plugins are equal

This is the part that bites teams in production: NetworkPolicy is a Kubernetes API object, but enforcing it is entirely the CNI plugin's job, and support varies a lot. Calico, Cilium, and Weave Net all implement full NetworkPolicy support, including egress rules. Flannel, in its default configuration, doesn't enforce NetworkPolicy at all — it's often paired with Calico specifically to add that layer. kubenet has no NetworkPolicy support whatsoever. Managed Kubernetes defaults vary too: EKS ships with the AWS VPC CNI, which historically needed an add-on (or a switch to Calico/Cilium) for policy enforcement; GKE and AKS have their own defaults and version-dependent support windows. Don't assume — check what's actually running with kubectl get pods -n kube-system and confirm your specific cluster's CNI supports the rule types (especially egress and named ports) you're about to write.

Test the policy, don't just apply it

After applying a default-deny policy, immediately verify with a throwaway pod: kubectl run test --rm -it --image=busybox -n payments -- wget -qO- --timeout=2 postgres:5432. If it still connects after a deny-all policy is in place, your CNI isn't enforcing NetworkPolicy at all — that's a fact you want to know before an audit finds it, not after.

Wrapping up

NetworkPolicy resources are the right tool for namespace-level network segmentation in Kubernetes, but the resource itself is only half the story — the other half is confirming your CNI plugin actually enforces what you wrote. Start every namespace that handles anything sensitive with default-deny, add narrow allow rules for the traffic that's actually needed, and verify enforcement with a real connection test rather than trusting that `kubectl apply` succeeding means the policy is live.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.