Network Policies

Kubernetes-native L3/L4 firewall rules that control traffic flow between pods, namespaces, and external CIDRs. Network policies are declarative, namespace-scoped, and enforced by the CNI plugin — not by kube-proxy.

What This Page Covers
  • NetworkPolicy resource model: podSelector, policyTypes, ingress, egress rules
  • Peer selectors: podSelector, namespaceSelector, ipBlock, and their combinations
  • AND vs OR logic within a single rule entry
  • Default-deny and default-allow patterns for ingress and egress
  • Ports: named ports, protocol (TCP/UDP/SCTP), endPort range
  • DNS egress: why CoreDNS must be explicitly allowed under default-deny egress
  • Full isolation pattern: default-deny + selective allow
  • Common policy patterns: namespace isolation, database tier, ingress-controller allow, monitoring allow
  • AdminNetworkPolicy and BaselineAdminNetworkPolicy (KEP-2091, GA 1.31)
  • CNI enforcement: which CNIs enforce NetworkPolicy; Flannel does not
  • Calico GlobalNetworkPolicy and NetworkPolicy extensions
  • CiliumNetworkPolicy: L7 HTTP rules, DNS FQDN egress, toFQDNs
  • Testing policies: netcat, kubectl exec, policy-tester tools
  • Visualizing policies: np-viewer, Cilium Hubble
  • Interaction with service mesh (Istio/Linkerd) mTLS
  • 6 metrics + 4 alerting rules + 5 troubleshooting runbooks
  • 9 best practices for production policy management

How Network Policies Work

By default, Kubernetes allows all pod-to-pod communication — any pod can reach any other pod on any port. NetworkPolicy objects change this by selecting pods and defining allowed traffic. Key behavioral rules:

  • Additive only — policies never block existing allowed traffic; they only add permissions. Multiple policies applying to the same pod are unioned.
  • Pod selection activates enforcement — a pod only becomes subject to policy when at least one NetworkPolicy selects it. A pod with no matching policy has all traffic allowed.
  • Separate ingress and egress — each is independently controlled. Selecting a pod in policyTypes: [Ingress] activates ingress enforcement only.
  • Enforced by the CNI plugin — NetworkPolicy objects have no effect without a CNI that implements them (Calico, Cilium, Weave Net, Antrea). Flannel alone does not enforce policies.
  • Stateful — reply packets for established connections are automatically allowed. You do not need a return-direction rule.
🚨
Flannel does not enforce NetworkPolicy

If your cluster uses Flannel as the CNI, NetworkPolicy objects are accepted by the API server but silently ignored. You must add Calico as a policy engine (Canal = Flannel + Calico policy) or switch to a policy-capable CNI. Always verify enforcement with a connectivity test.

NetworkPolicy Anatomy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-policy
  namespace: production              # policies are namespace-scoped
spec:
  podSelector:                       # which pods this policy applies to
    matchLabels:
      app: api                       # empty {} = all pods in namespace
  policyTypes:
    - Ingress                        # activates ingress enforcement
    - Egress                         # activates egress enforcement
  ingress:
    - from:                          # list of allowed sources (OR between list items)
        - podSelector:               # same-namespace pods with this label
            matchLabels:
              app: frontend
        - namespaceSelector:         # pods in namespaces with this label
            matchLabels:
              kubernetes.io/metadata.name: monitoring
      ports:
        - protocol: TCP
          port: 8080
          endPort: 8090              # port range (GA 1.25)
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to:                            # allow DNS (critical under default-deny egress)
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Selector Logic: AND vs OR

The most common source of NetworkPolicy bugs is misunderstanding when selectors are ANDed vs ORed. The rule is:

OR — separate list entries

from:
  - podSelector:         # entry 1
      matchLabels:
        app: frontend
  - namespaceSelector:   # entry 2
      matchLabels:
        env: prod
# Allows pods with app=frontend
# OR pods in env=prod namespaces
# (different list items = OR)

AND — combined in one entry

from:
  - podSelector:         # combined
      matchLabels:       # in one
        app: frontend    # entry
    namespaceSelector:
      matchLabels:
        env: prod
# Allows pods with app=frontend
# AND that are in env=prod namespaces
# (same list item = AND)
⚠️
The most common NetworkPolicy mistake

When you have namespaceSelector and podSelector as separate list items (OR), namespaceSelector alone allows all pods in matching namespaces regardless of their labels. This is often unintentionally permissive. Put both in the same entry (AND) to require both conditions simultaneously.

ipBlock — External CIDRs

ipBlock matches traffic from/to external IP ranges. It supports except to carve out sub-ranges:

ingress:
  - from:
      - ipBlock:
          cidr: 10.0.0.0/8          # allow entire RFC1918 /8
          except:
            - 10.42.0.0/16          # except the pod CIDR (avoid confusion)
            - 10.96.0.0/12          # except the service CIDR
      ports:
        - protocol: TCP
          port: 443

egress:
  - to:
      - ipBlock:
          cidr: 0.0.0.0/0           # allow all internet egress
          except:
            - 169.254.169.254/32    # block AWS IMDS from workloads
ℹ️
ipBlock and Pod IPs

ipBlock applies to the pod's source IP, not to Service ClusterIPs. ClusterIPs are virtual (see kube-proxy internals) — DNAT happens before the NetworkPolicy is evaluated. You should use podSelector/namespaceSelector to match pod-to-pod traffic, not ipBlock with pod CIDRs.

Named Ports and Port Ranges

# Named port — references the port name defined in the container spec
# Useful when different pods use different port numbers for the same protocol

# Pod spec:
spec:
  containers:
    - name: api
      ports:
        - name: http-api
          containerPort: 8080

# NetworkPolicy references the name:
ports:
  - port: http-api      # resolved to containerPort 8080 on matching pods
    protocol: TCP

# Port range (GA since Kubernetes 1.25):
ports:
  - protocol: TCP
    port: 10000
    endPort: 11000      # allows ports 10000-11000 inclusive

# SCTP support (GA 1.20):
ports:
  - protocol: SCTP
    port: 9999

Default-Deny and Default-Allow Patterns

Default Deny Ingress

Selects all pods in the namespace; specifies Ingress policyType with no rules — no ingress is allowed. Must be combined with specific allow policies.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}        # matches ALL pods in this namespace
  policyTypes:
    - Ingress            # ingress enforcement active; empty rules = deny all

Default Deny Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress             # egress enforcement active; empty rules = deny all

Default Deny All (Ingress + Egress)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Allow All (override deny for debugging)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-all-ingress
  namespace: production
spec:
  podSelector: {}
  ingress:
    - {}                 # one empty rule = allow all ingress
  policyTypes:
    - Ingress
⚠️
DNS must be explicitly allowed under default-deny egress

When you apply default-deny egress to a namespace, pods lose DNS resolution because UDP/TCP port 53 to CoreDNS is blocked. You must add an explicit DNS egress rule or all service names stop resolving. This is the #1 cause of mysterious application failures after adding egress policies.

DNS Egress Rule — Always Required

Under default-deny egress, add this policy to every namespace that applies egress control. It allows DNS queries to CoreDNS in kube-system:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns        # CoreDNS pods
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53                     # DNS over TCP for large responses
ℹ️
NodeLocal DNSCache consideration

If your cluster uses NodeLocal DNSCache (see 04-dns.html), DNS queries go to 169.254.20.10 on the node — which is an ipBlock address, not a pod IP. Add an additional rule allowing UDP/TCP 53 to 169.254.20.10/32 when NodeLocal DNSCache is active.

Common Policy Patterns

Namespace Isolation

Prevent cross-namespace traffic while allowing intra-namespace communication:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: namespace-isolation
  namespace: team-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}            # allow from any pod in SAME namespace
  egress:
    - to:
        - podSelector: {}            # allow to any pod in SAME namespace
    - to:                            # allow DNS
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Database Tier Isolation

Allow only the application tier to connect to the database; deny everything else including monitoring (add a separate monitoring policy if needed):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-policy
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api           # only API pods can connect
      ports:
        - protocol: TCP
          port: 5432
  egress:
    - to:                        # postgres replication to standby
        - podSelector:
            matchLabels:
              app: postgres
              role: standby
      ports:
        - protocol: TCP
          port: 5432

Allow Traffic from Ingress Controller

Applications behind an Ingress need to allow traffic from the ingress controller pods. The controller runs in a separate namespace (typically ingress-nginx):

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-controller
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: frontend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx
      ports:
        - protocol: TCP
          port: 80
        - protocol: TCP
          port: 443

Allow Prometheus Scraping

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: production
spec:
  podSelector: {}                # all pods in namespace
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9090             # adjust to actual metrics port
        - protocol: TCP
          port: 8080             # some apps expose /metrics here

Allow Specific External Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-external-api
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: payment-processor
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24   # payment gateway IP range
      ports:
        - protocol: TCP
          port: 443
    - to:                           # also allow DNS
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Production Full-Isolation Pattern

The recommended layered approach for a production namespace — apply in this order:

Layer 1: default-deny-all → blocks everything; activates enforcement Layer 2: allow-dns-egress → restores DNS (UDP/TCP 53 to kube-dns) Layer 3: allow-intra-namespace → pods can talk within the namespace Layer 4: allow-ingress-controller → inbound from nginx/traefik namespace Layer 5: allow-prometheus-scrape → monitoring can scrape /metrics Layer 6: app-specific policies → per-app: postgres, redis, external APIs
# Apply all at once with a Kustomize overlay or Helm values:
# namespace-policies/
#   01-default-deny-all.yaml
#   02-allow-dns-egress.yaml
#   03-allow-intra-namespace.yaml
#   04-allow-ingress-controller.yaml
#   05-allow-prometheus-scrape.yaml
kubectl apply -f namespace-policies/

AdminNetworkPolicy & BaselineAdminNetworkPolicy

Standard NetworkPolicy has a critical limitation: cluster administrators cannot set policies that override or pre-empt namespace-scoped policies. The AdminNetworkPolicy API (KEP-2091, GA 1.31) solves this with cluster-scoped policy objects that take precedence over namespace policies.

GA 1.31

AdminNetworkPolicy (ANP)

  • Cluster-scoped (not namespaced)
  • Priority field (0–1000, lower = higher priority)
  • Actions: Allow, Deny, Pass
  • Pass = defer to namespace NetworkPolicy
  • Overrides namespace NetworkPolicy when action is Allow or Deny
  • Can select pods across namespaces
GA 1.31

BaselineAdminNetworkPolicy (BANP)

  • Singleton (only one per cluster)
  • No priority (evaluated after all ANPs)
  • Only actions: Allow, Deny
  • Acts as cluster-wide default when no namespace policy matches
  • Lower precedence than ANP and namespace NetworkPolicy

Precedence Order

Highest priority ┌──────────────────────────────────────────────────────────┐ │ AdminNetworkPolicy (priority 0) │ Allow / Deny → final │ AdminNetworkPolicy (priority 100) │ Pass → continue │ AdminNetworkPolicy (priority 1000) │ ├──────────────────────────────────────────────────────────┤ │ Namespace NetworkPolicy │ (standard rules) ├──────────────────────────────────────────────────────────┤ │ BaselineAdminNetworkPolicy │ Allow / Deny → final └──────────────────────────────────────────────────────────┘ Lowest priority
apiVersion: policy.networking.k8s.io/v1alpha1
kind: AdminNetworkPolicy
metadata:
  name: deny-to-kube-system
spec:
  priority: 10                          # evaluated first (lowest number = highest priority)
  subject:
    namespaces:
      matchExpressions:
        - key: kubernetes.io/metadata.name
          operator: NotIn
          values: [kube-system, monitoring]   # all namespaces except these
  ingress:
    - name: "deny-non-system-to-api-server"
      action: Deny
      from:
        - namespaces:
            namespaceSelector: {}       # from any namespace
      ports:
        - portNumber:
            protocol: TCP
            port: 443
  egress:
    - name: "pass-to-namespace-policy"
      action: Pass                      # let namespace NetworkPolicy decide
      to:
        - namespaces:
            namespaceSelector: {}
---
apiVersion: policy.networking.k8s.io/v1alpha1
kind: BaselineAdminNetworkPolicy
metadata:
  name: default                         # must be named "default"
spec:
  subject:
    namespaces: {}                      # all namespaces
  ingress:
    - name: "allow-same-namespace"
      action: Allow
      from:
        - namespaces:
            sameLabels: [kubernetes.io/metadata.name]
    - name: "deny-all-else"
      action: Deny
      from:
        - namespaces:
            namespaceSelector: {}

CNI-Specific Policy Extensions

Calico GlobalNetworkPolicy

Calico extends standard NetworkPolicy with cluster-scoped GlobalNetworkPolicy and richer matching capabilities. See 02-cni-plugins.html for Calico architecture details.

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: deny-ssh-external
spec:
  selector: all()                         # all pods cluster-wide
  order: 100                              # lower = evaluated first
  types:
    - Ingress
  ingress:
    - action: Deny
      protocol: TCP
      destination:
        ports: [22]
      source:
        nets:
          - "0.0.0.0/0"
        notNets:
          - "10.0.0.0/8"                  # except internal networks
---
apiVersion: projectcalico.org/v3
kind: NetworkPolicy                       # Calico namespaced NetworkPolicy (richer than K8s)
metadata:
  name: api-policy
  namespace: production
spec:
  selector: app == 'api'
  types:
    - Ingress
    - Egress
  ingress:
    - action: Allow
      protocol: TCP
      source:
        selector: app == 'frontend'
      destination:
        ports: [8080]
  egress:
    - action: Allow
      protocol: TCP
      destination:
        selector: app == 'postgres'
        ports: [5432]
    - action: Allow
      protocol: UDP
      destination:
        selector: k8s-app == 'kube-dns'
        namespaceSelector: kubernetes.io/metadata.name == 'kube-system'
        ports: [53]

CiliumNetworkPolicy — L7 HTTP Rules

Cilium's CiliumNetworkPolicy extends standard NetworkPolicy to L7 — it can enforce HTTP method, path, and headers, and allow DNS egress by FQDN rather than by IP. See 02-cni-plugins.html for Cilium architecture.

apiVersion: "cilium.io/v2"
kind: CiliumNetworkPolicy
metadata:
  name: api-l7-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels:
      app: api
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: "GET"            # only allow GET /v1/*
                path: "^/v1/.*"
              - method: "POST"
                path: "^/v1/orders$"
                headers:
                  - "Content-Type: application/json"
  egress:
    # DNS egress by FQDN (resolves to IPs automatically)
    - toFQDNs:
        - matchName: "payments.stripe.com"
        - matchPattern: "*.stripe.com"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP
    # Allow DNS queries themselves
    - toEndpoints:
        - matchLabels:
            "k8s:k8s-app": kube-dns
            "k8s:io.kubernetes.pod.namespace": kube-system
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY
          rules:
            dns:
              - matchPattern: "*"       # allow all DNS lookups
CiliumNetworkPolicy toFQDNs advantage

Standard NetworkPolicy requires ipBlock rules with static IPs. For cloud services like Stripe or S3 that rotate IPs, this is unmanageable. Cilium's toFQDNs resolves hostnames and dynamically updates the allowed IP set — no manual CIDR maintenance needed.

Interaction with Service Mesh (mTLS)

When using Istio or Linkerd, all pod traffic is intercepted by a sidecar proxy. NetworkPolicy is evaluated against the pod IP after the sidecar receives and decrypts the mTLS connection from another sidecar.

This creates two enforcement layers:

LayerWhat It ControlsWhere Enforced
NetworkPolicy (L3/L4)IP + port access between pods; works on pod IP even with sidecarsCNI dataplane (iptables/eBPF)
Istio AuthorizationPolicy (L7)Service identity (SPIFFE), HTTP method/path, JWT claimsEnvoy sidecar
⚠️
Port 15008 (Istio HBONE) and NetworkPolicy

Istio ambient mode (no sidecar) uses port 15008 for intra-mesh traffic. If you have default-deny NetworkPolicy, you must explicitly allow TCP 15008 between workloads for ambient mesh traffic to flow. This does not apply to sidecar mode.

Testing Network Policies

Quick Connectivity Test with netshoot

# Spin up a debug pod in the source namespace
kubectl run netshoot --image=nicolaka/netshoot -n team-a --rm -it -- /bin/bash

# From inside: test TCP connectivity
nc -zv postgres.production.svc.cluster.local 5432   # should succeed
nc -zv redis.production.svc.cluster.local 6379       # should fail if blocked

# Test DNS resolution
nslookup kubernetes.default.svc.cluster.local
dig +short postgres.production.svc.cluster.local

# Test HTTP
curl -sv http://api.production.svc.cluster.local:8080/health

Targeted Connectivity Test (kubectl exec)

# Test from a specific pod
kubectl exec -n production deploy/api -- nc -zv postgres 5432
kubectl exec -n production deploy/api -- curl -s http://external-api.example.com

# Test DNS from pod
kubectl exec -n production deploy/api -- nslookup postgres

Policy Verification with np-viewer

# np-viewer: visualize which policies apply to a pod
# https://github.com/runoncloud/np-viewer
kubectl np-viewer pod -n production -l app=api

# Cilium: check policy enforcement status
cilium policy get
cilium endpoint list
cilium endpoint get 

# Calico: check which policies select a pod
calicoctl get networkpolicy -n production -o wide
kubectl exec -n kube-system deploy/calico-kube-controllers -- /bin/sh -c \
  "calico-kube-controllers status"

Hubble (Cilium) — Visualize Dropped Packets

# Watch for dropped flows in real time
hubble observe --verdict DROPPED -f

# Filter to specific namespace
hubble observe --namespace production --verdict DROPPED -f

# Check why a specific flow was dropped (shows which policy)
hubble observe --from-pod production/api --to-pod production/postgres

Metrics, Alerting & Troubleshooting

Key Metrics

MetricSourceWhat It Tells You
cilium_drop_count_totalCiliumPackets dropped by NetworkPolicy; label reason=POLICY_DENIED
cilium_policy_countCiliumTotal policies imported; drop after deployment = parse error
cilium_policy_endpoint_enforcement_statusCiliumEndpoints with/without policy enforcement enabled
calico_iptables_rules_countCalico FelixTotal iptables rules; very high values indicate scaling issue
felix_active_local_policiesCalico FelixPolicies active on the node
felix_int_dataplane_failures_totalCalico FelixDataplane programming failures; non-zero = policies not applied

Alerting Rules

# Alert: High policy drop rate (possible misconfiguration)
- alert: NetworkPolicyHighDropRate
  expr: |
    rate(cilium_drop_count_total{reason="POLICY_DENIED"}[5m]) > 100
  for: 5m
  labels:
    severity: warning
  annotations:
    summary: "High NetworkPolicy drop rate on {{ $labels.node }}"
    description: "Check if a new policy is blocking expected traffic"

# Alert: Calico dataplane programming failures
- alert: CalicoDataplaneFailure
  expr: rate(felix_int_dataplane_failures_total[5m]) > 0
  for: 5m
  labels:
    severity: critical
  annotations:
    summary: "Calico Felix dataplane programming failures"

# Alert: DNS resolution failures (often caused by missing DNS egress rule)
- alert: PodDNSResolutionFailures
  expr: |
    rate(coredns_dns_response_rcode_count_total{rcode="SERVFAIL"}[5m]) > 1
  for: 5m
  labels:
    severity: warning

# Alert: NetworkPolicy not enforced (no CNI policy support)
- alert: NetworkPoliciesNotEnforced
  expr: kube_networkpolicy_created > 0 unless cilium_policy_count > 0
  for: 30m
  labels:
    severity: critical

Troubleshooting Runbooks

Runbook 1: Pod Cannot Reach Another Pod (Connection Refused / Timeout)

# Step 1: Identify which policies apply to source and destination pods
kubectl get networkpolicy -n <source-namespace> -o yaml
kubectl get networkpolicy -n <dest-namespace> -o yaml

# Step 2: Check if destination pod is selected by any egress-blocking policy
kubectl get networkpolicy -n <dest-namespace> -o json | \
  jq '.items[] | select(.spec.policyTypes[] | contains("Ingress"))'

# Step 3: Verify selector labels match
kubectl get pod <dest-pod> -n <dest-namespace> --show-labels

# Step 4: Use Cilium Hubble to see drop reason
hubble observe --to-pod <dest-namespace>/<dest-pod> --verdict DROPPED -f

# Step 5: Temporarily remove the blocking policy (test environment only)
kubectl delete networkpolicy <policy-name> -n <namespace>

Runbook 2: DNS Resolution Failures After Adding Egress Policy

# Symptom: pods get connection refused to service names but not to pod IPs
# Cause: missing DNS egress rule under default-deny egress

# Confirm DNS is blocked:
kubectl exec -n production deploy/api -- nslookup kubernetes.default
# Should return: SERVFAIL or timeout

# Fix: apply DNS egress policy
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
EOF

Runbook 3: Policy Applied But Traffic Still Blocked

# Possible causes:
# 1. Policy in wrong namespace
kubectl get networkpolicy --all-namespaces | grep <policy-name>

# 2. podSelector labels don't match pod labels
kubectl get pod <pod> -n <ns> --show-labels
# Compare with NetworkPolicy podSelector/matchLabels

# 3. AND vs OR selector mistake (see selector logic section)
# 4. CNI not supporting NetworkPolicy (Flannel)
kubectl get daemonset -n kube-system   # check for calico-node or cilium
kubectl get pods -n kube-system -l k8s-app=calico-node

# 5. Calico Felix not programmed on the node
kubectl logs -n kube-system -l k8s-app=calico-node -c calico-node | grep -i error

Runbook 4: Ingress Controller Cannot Reach Backend Pod

# After adding default-deny-ingress, ingress controller gets 502
# Cause: need to allow ingress from ingress-nginx namespace

# Find ingress controller namespace and pod labels
kubectl get pods -n ingress-nginx --show-labels

# Apply allow rule (adjust labels to match your controller)
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-nginx
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
      ports:
        - protocol: TCP
          port: 80
        - protocol: TCP
          port: 8080
        - protocol: TCP
          port: 443
EOF

Runbook 5: NetworkPolicy Not Enforced (Flannel Cluster)

# Verify your CNI
kubectl get daemonset -n kube-system
# If you see flannel but no calico-node or cilium: policies are NOT enforced

# Option 1: Add Canal (Flannel + Calico policy engine)
# https://docs.tigera.io/calico/latest/getting-started/kubernetes/flannel/flannel

# Option 2: Replace Flannel with Calico or Cilium (requires re-provisioning)

# Option 3: Quick smoke test to confirm enforcement:
kubectl create ns netpol-test
kubectl run server -n netpol-test --image=nginx --expose --port=80
kubectl apply -f - <<'EOF'
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
  namespace: netpol-test
spec:
  podSelector: {}
  policyTypes: [Ingress]
EOF
kubectl run client -n netpol-test --image=alpine --rm -it \
  -- wget -T5 -O- http://server/  # should fail if enforced
kubectl delete ns netpol-test

Best Practices

  1. Start with default-deny-all per namespace, then add allows — never start permissive and try to harden later. It's much harder to audit.
  2. Always include a DNS egress allow rule with default-deny-egress — template it into your namespace provisioning automation so it's never missed.
  3. Use namespaceSelector with kubernetes.io/metadata.name — this built-in label (auto-set since 1.21) gives stable, unique namespace identity without requiring custom labels.
  4. Verify AND vs OR semantics in every policy — use a connectivity test immediately after applying a policy to confirm it works as intended.
  5. Use AdminNetworkPolicy for cluster-wide baseline rules — don't replicate the same "deny external SSH" rule in 50 namespace policies. Write it once in ANP.
  6. Label namespaces consistently — agree on a label taxonomy (env: prod/staging/dev, team: payments/catalog) before writing policies. Retroactively labeling is painful.
  7. Never use ipBlock for pod-to-pod traffic — pod IPs are ephemeral. Use podSelector/namespaceSelector instead. ipBlock is only for traffic from/to outside the cluster.
  8. Store policies in Git alongside workload manifests — treating policies as code enables PR review, audit trail, and automated testing.
  9. Use Cilium CiliumNetworkPolicy or Calico GlobalNetworkPolicy for L7/FQDN rules — standard NetworkPolicy cannot handle HTTP path enforcement or dynamic IP egress. Use CNI extensions for these requirements.