Networking

Network Policies

● Advanced ⏱ 15 min read

By default, every pod in a Kubernetes cluster can talk to every other pod — there are no firewall rules. For a hobby project that's fine. For production, it's a problem: a compromised frontend pod can reach the database directly. Network Policies let you declare which pods are allowed to communicate, using label selectors and namespace filters instead of IP addresses.

Default Behavior

Without any Network Policies applied, Kubernetes networking is fully open:

Network Policies are additive and selective. A policy only applies to pods it selects via podSelector. Pods not selected by any policy remain fully open. Once a policy selects a pod for ingress traffic, all ingress not explicitly allowed is denied for that pod — and similarly for egress.

⚠️
Network Policies require a supporting CNI plugin

The Kubernetes API accepts NetworkPolicy objects on any cluster, but enforcement only happens if the CNI plugin supports it. Flannel does not enforce Network Policies. Calico, Cilium, WeaveNet, and others do. If you create policies on a cluster running Flannel, they are silently ignored.

NetworkPolicy Spec

A NetworkPolicy has three main parts: which pods it applies to (podSelector), which traffic directions it controls (policyTypes), and the allow-rules (ingress and/or egress).

networkpolicy-structure.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: my-policy
  namespace: default
spec:
  podSelector:         # which pods this policy applies to
    matchLabels:
      app: api
  policyTypes:
  - Ingress            # control inbound traffic to selected pods
  - Egress             # control outbound traffic from selected pods
  ingress:             # list of allowed ingress rules
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
  egress:              # list of allowed egress rules
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432

podSelector

The top-level podSelector targets pods within the same namespace that the policy belongs to. An empty selector (podSelector: {}) matches all pods in the namespace — useful for namespace-wide default-deny policies.

podSelector:
  matchLabels:
    app: api        # only pods with this label are affected

# OR — apply to all pods in the namespace:
podSelector: {}     # empty = match all pods

Inside ingress.from and egress.to blocks, podSelector and namespaceSelector are used as source/destination filters.

Ingress Rules

Each item in the ingress list is an allow rule. Traffic is permitted if it matches all conditions in that item — including both from sources and ports. Multiple items in the list are ORed together.

allow-frontend-ingress.yaml
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Ingress
  ingress:
  # Rule 1: allow traffic from frontend pods on port 8080
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
  # Rule 2: allow traffic from monitoring namespace on port 9090
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
    ports:
    - protocol: TCP
      port: 9090
💡
AND vs OR: from items vs from selectors

Two selectors inside the same from list item are ANDed — both must match. Two separate items in the from list are ORed. A common mistake: putting podSelector and namespaceSelector as siblings in one item ANDs them (pod in that namespace). Putting them as separate items ORs them (pods with that label OR pods in that namespace).

AND vs OR in ingress.from rules
Sibling selectors = AND
from:
- podSelector: {app: api}
namespaceSelector: {ns: prod}
✓ allow
pods labeled app=api
AND in namespace prod
Separate items = OR
from:
- podSelector: {app: api}
- namespaceSelector: {ns: prod}
✓ allow
pods labeled app=api
OR any pod in namespace prod
Same YAML key, different YAML structure — a common source of unintended open policies
The position of selectors in the YAML changes the logical operator. Pay close attention to indentation.

Egress Rules

Egress rules control traffic leaving the selected pods. The same AND/OR logic applies. Always include DNS egress (port 53 UDP/TCP) when restricting egress — pods can't resolve Service names without it.

api-egress.yaml
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
  - Egress
  egress:
  # Allow DNS queries (required for Service name resolution)
  - ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
  # Allow outbound to postgres pods only
  - to:
    - podSelector:
        matchLabels:
          app: postgres
    ports:
    - protocol: TCP
      port: 5432
  # Allow outbound to external payment API
  - to:
    - ipBlock:
        cidr: 203.0.113.0/24
    ports:
    - protocol: TCP
      port: 443

Default-Deny Pattern

The recommended security posture is default-deny all, then allow what's needed. Apply these two policies to every namespace:

default-deny-all.yaml
# Deny all ingress to all pods in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
---
# Deny all egress from all pods in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress

With these in place, pods in production have no connectivity until additional policies explicitly allow specific traffic flows. Add policies incrementally as you identify legitimate traffic paths.

Namespace Isolation

Isolate namespaces from each other while allowing intra-namespace traffic:

namespace-isolation.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  # Only allow ingress from pods in the same namespace
  - from:
    - podSelector: {}   # empty podSelector = all pods in this namespace

Combined with default-deny, this creates hard namespace boundaries — no cross-namespace traffic except where explicitly allowed by additional policies.

ipBlock

Use ipBlock to allow or deny traffic to/from external CIDR ranges. You can add exceptions within the CIDR:

ingress:
- from:
  - ipBlock:
      cidr: 10.0.0.0/8      # allow from the 10.x.x.x range
      except:
      - 10.0.0.0/16         # but not from this specific subnet

Note that ipBlock matches the IP seen by the network — after any NAT. In cloud environments, traffic from pods in other nodes may appear to come from the node IP, not the pod IP, depending on the CNI and network topology.

CNI Requirement

Network Policies are enforced at the kernel level by the CNI plugin — Kubernetes only stores the policy objects. Choose a CNI that supports enforcement:

CNINetwork Policy supportNotes
CalicoFullAlso has CalicoNetworkPolicy for L7 rules and egress gateways
CiliumFull + extendedeBPF-based; supports L7 (HTTP, gRPC) policies, DNS-based policies
WeaveNetFullSimple setup; performance lower than eBPF options
FlannelNoneDoes not enforce policies — requires adding Calico or Cilium alongside
kindnet (kind)NoneLocal dev only; no policy enforcement

kubectl Commands

# List all NetworkPolicies in a namespace
kubectl get networkpolicy -n <namespace>
kubectl get netpol -n <namespace>    # shorthand

# Describe a policy (shows selectors, rules)
kubectl describe networkpolicy default-deny-ingress -n production

# Apply a policy
kubectl apply -f policy.yaml

# Delete a policy
kubectl delete networkpolicy default-deny-ingress -n production

# Check which pods a policy selects
kubectl get pods -n production -l app=api

# Test connectivity from a pod (after applying policies)
kubectl exec -it -n production deploy/api -- \
  curl -m 2 http://postgres.production:5432

# Cilium: visualize policy enforcement
kubectl exec -n kube-system ds/cilium -- \
  cilium policy get