Safeguard
Container Security

Kubernetes Network Policies: A Practical Guide

By default every pod in a Kubernetes cluster can talk to every other pod. NetworkPolicies are how you replace that flat network with least-privilege segmentation — here is how to design and roll them out without breaking traffic.

Marcus Chen
Cloud Security Engineer
5 min read

The default Kubernetes network is flat and fully open. Every pod can initiate a connection to every other pod, in every namespace, on every port — the control plane assumes trust and leaves segmentation to you. That default is fine for a demo and catastrophic in production, because it means a single compromised pod can reach your database, your internal APIs, your metrics endpoints, and the cloud metadata service, all without crossing a single boundary. When attackers talk about "lateral movement" inside a cluster, this flat network is what they are moving across. NetworkPolicies are the built-in mechanism for turning that open network into a set of explicit, least-privilege paths. This guide walks through how they actually work and how to adopt them without an outage.

The prerequisite everyone forgets: your CNI must enforce them

A NetworkPolicy is just an API object. Kubernetes will happily accept one and report success even if nothing enforces it. Enforcement is the job of your Container Network Interface plugin, and not all of them do it. Calico, Cilium, and Antrea enforce NetworkPolicy; the basic flannel plugin does not. Before you write a single policy, confirm your CNI enforces them — otherwise you will build a false sense of security around rules that no component is applying.

How the default-deny switch works

The critical thing to understand is that NetworkPolicies are additive and default-allow until the first policy selects a pod. The moment any policy with a given policyTypes selects a pod, that direction flips to default-deny for that pod, and only explicitly allowed traffic passes. So the foundational move is a namespace-wide deny-all that flips the default:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}          # selects every pod in the namespace
  policyTypes:
    - Ingress
    - Egress
  # no ingress/egress rules = nothing is allowed

With this in place, every pod in payments is isolated in both directions. Now you add back only the flows the application genuinely needs.

Allowing the traffic you actually need

Rules are built from selectors: podSelector (by label), namespaceSelector (by namespace label), and ipBlock (by CIDR). Here is a policy that lets the api pods receive traffic only from the frontend pods, and only on port 8080:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-api
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Ingress]
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

Egress deserves equal attention and is more often neglected. Locking down egress is what stops a compromised pod from reaching the cloud metadata endpoint or exfiltrating data to an attacker-controlled host:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes: [Egress]
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

The DNS gotcha that breaks everything

The number-one way teams break their cluster with egress policies is by forgetting DNS. When you flip on default-deny egress, pods can no longer reach kube-dns/CoreDNS, so every hostname lookup fails and the application appears "randomly" broken even though the destination itself is allowed. Always add an explicit egress allowance for DNS:

  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

A rollout order that avoids outages

  1. Label everything first. Policies select by label, so audit that your namespaces carry the built-in kubernetes.io/metadata.name label and your workloads have consistent app labels.
  2. Start in audit, not enforce. Cilium and Calico both offer a way to log what would be denied before you actually deny it. Use it to discover the flows you forgot.
  3. Apply default-deny per namespace, then add allows. Never flip the whole cluster at once. Take one namespace, add the allow rules its apps need, verify, then move on.
  4. Add DNS egress before anything else. It is the dependency every workload shares.
  5. Watch for cross-namespace and control-plane traffic. Ingress controllers, metrics scrapers, and admission webhooks all cross boundaries you may not have mapped.

Where plain NetworkPolicy runs out

Standard NetworkPolicy operates at L3/L4 — IPs, ports, and pod labels. It cannot express "allow GET on /health but deny POST on /admin," and its ipBlock rules are awkward for egress to external FQDNs whose IPs rotate. For L7-aware rules, DNS-based egress, and cluster-wide policy, teams reach for CiliumNetworkPolicy or a service mesh. Use native NetworkPolicy as the always-on baseline and layer richer controls where a workload's risk justifies the operational cost.

Quick reference

GoalMechanism
Isolate a namespacepodSelector: {} with both policyTypes
Allow specific app-to-apppodSelector in from/to
Allow from another namespacenamespaceSelector (label the namespace)
Allow external CIDRipBlock with cidr and except
Keep DNS workingExplicit UDP/TCP 53 egress to kube-system
L7 / FQDN egressCNI-specific policy or service mesh

How Safeguard helps

NetworkPolicies are infrastructure-as-code, and the failure mode is subtle: a policy that looks restrictive but leaves an unlabeled pod fully exposed, or an egress rule that forgot to deny the metadata endpoint. Safeguard's IaC scanning analyzes your policy manifests and flags namespaces with no default-deny, workloads no policy selects, and egress rules that allow reaching sensitive destinations — before they merge. Because network exposure only matters in the context of what an image can do once reached, container security scanning ties a pod's reachable network paths to the vulnerabilities inside it, and Griffin AI prioritizes a CVE that sits on an internet-reachable, under-segmented workload over one that is boxed in by a tight policy. Teams comparing full cloud-native security suites can see how the approaches differ in Safeguard vs Wiz.

A flat network is an attacker's best friend — close it methodically. Create a free Safeguard account or read the documentation to validate your network posture as code.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.