DEV Community

Cover image for Testing Kubernetes NetworkPolicies Before They Hit Production
Prasad MK
Prasad MK

Posted on

Testing Kubernetes NetworkPolicies Before They Hit Production

You write a NetworkPolicy, apply it to a namespace, and move on. Nobody tests it. Nobody validates it against the actual traffic the service depends on. It sits there until a deploy breaks something, and the first sign of trouble is a pod that can't reach its database.

Most teams treat NetworkPolicies like documentation instead of code. Someone writes one, a reviewer skims the YAML, it gets merged, and it goes straight onto the cluster. There's no equivalent of a unit test for "can pod A talk to pod B on port 5432." That gap is where the 2am pages come from.

Here are three tools that close it, plus a CI setup that catches a bad rule before merge instead of during an incident.

Why NetworkPolicy bugs are hard to catch by reading YAML

A NetworkPolicy looks simple. Selectors, ports, ingress and egress rules. But three things make them easy to get wrong:

  1. Default-deny is implicit. The moment you add any NetworkPolicy that selects a pod, all traffic not explicitly allowed is blocked. Add a policy meant to restrict one path, and you silently cut off three others that had no policy governing them before.
  2. Selectors don't validate against real labels. A typo in app: payment-svc versus the pod's actual app: payment-service produces a policy that matches nothing. kubectl apply succeeds. The policy does nothing. You find out when traffic that should be blocked isn't, or worse, traffic that should be allowed is.
  3. Multiple policies combine additively, and nobody traces the union. If three teams each own a policy touching the same namespace, the effective ruleset is the union of all three. Reading one YAML file tells you nothing about the combined effect.

None of this shows up in a code review unless the reviewer mentally simulates traffic flow across every policy in the namespace. That doesn't scale past a handful of services.

Tool 1: kubectl-np-viewer

kubectl-np-viewer is a kubectl plugin that renders the effective policy for a pod or namespace, not just the raw YAML. Instead of asking you to trace intersections across five NetworkPolicy objects by hand, it computes the resolved ingress and egress rules and prints them as a readable table.

Install:

kubectl krew install np-viewer
Enter fullscreen mode Exit fullscreen mode

Run it against a namespace:

kubectl np-viewer -n checkout
Enter fullscreen mode Exit fullscreen mode

Output shows, per pod selector, which ingress sources and egress destinations are actually permitted after all policies in the namespace are combined. This is the tool you reach for when someone asks "can the checkout service reach Redis" and nobody wants to trace four YAML files to find out.

Use it locally before you open a PR. If the effective policy doesn't match what you intended, fix the policy before it's even committed.

Tool 2: netpol-analyzer

netpol-analyzer (from the np-guard project) goes further. It's built for static analysis and CI, not just interactive inspection. It can:

  • Diff two policy sets and show exactly what connectivity changed
  • Detect policies that have no effect (selector matches zero pods)
  • Detect redundant rules already covered by a broader rule
  • Generate a full connectivity map for a namespace or cluster snapshot

The diff mode is the one that matters most for CI. Given the policies currently in the cluster (or a manifest directory) and the policies in your PR branch, it tells you the delta in allowed connections:

netpol-analyzer diff \
  --dir1 manifests/main \
  --dir2 manifests/pr-branch \
  --output text
Enter fullscreen mode Exit fullscreen mode

A sample result looks like this:

Changed connections between main and pr-branch:
Denied connection: checkout-service -> inventory-db:5432 (was allowed, now denied)
Added connection: checkout-service -> new-cache:6379 (newly allowed)
Enter fullscreen mode Exit fullscreen mode

That first line is the bug that takes down a service on merge, every time. Someone tightens egress for one workload and doesn't notice it also strips access to the database three lines down. A reviewer scanning YAML won't catch it. netpol-analyzer diff puts it right at the top of the output, which is the whole point of running it.

Run this as a required check on any PR that touches networkpolicy manifests. Treat a "Denied connection" line touching a live dependency as a blocking failure, not a warning.

Tool 3: Cilium connectivity test

If your cluster runs Cilium as the CNI, cilium connectivity test gives you something the first two tools don't: live traffic verification against a real cluster, not just static analysis of YAML. It deploys a set of test pods, generates traffic between them under different policy conditions, and reports pass/fail based on actual packet delivery.

cilium connectivity test --test-namespace cilium-test
Enter fullscreen mode Exit fullscreen mode

This catches things static analysis can't, like a CNI-level misconfiguration, an eBPF program that didn't reload correctly after a policy update, or a policy that's syntactically fine but doesn't behave as expected once the dataplane applies it. Static tools tell you what the policy should do. This tool tells you what actually happens on the wire.

You don't want to run the full Cilium test suite on every PR. It needs a live cluster and takes minutes, not seconds. Reserve it for a staging environment, run on merge to main or on a schedule, not on every commit.

Building this into a CI pipeline

The goal is a pipeline where a NetworkPolicy change gets validated automatically, and a rule that breaks an existing dependency fails the build before a team member has to notice. Here's a structure that works with GitHub Actions, adjust for GitLab CI or Jenkins as needed.

Stage 1: Static validation on every PR touching NetworkPolicy manifests

name: netpol-validation

on:
  pull_request:
    paths:
      - 'manifests/**/networkpolicy*.yaml'
      - 'manifests/**/networkpolicy*.yml'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Install netpol-analyzer
        run: |
          curl -sLo netpol-analyzer https://github.com/np-guard/netpol-analyzer/releases/latest/download/netpol-analyzer-linux-amd64
          chmod +x netpol-analyzer
          sudo mv netpol-analyzer /usr/local/bin/

      - name: Checkout base branch manifests
        run: |
          git worktree add /tmp/base origin/main -- manifests

      - name: Diff connectivity
        run: |
          netpol-analyzer diff \
            --dir1 /tmp/base/manifests \
            --dir2 manifests \
            --output text | tee netpol-diff.txt

      - name: Fail on denied connections to known dependencies
        run: |
          if grep -q "Denied connection" netpol-diff.txt; then
            echo "::error::NetworkPolicy change removes existing connectivity. Review netpol-diff.txt."
            exit 1
          fi

      - name: Check for ineffective policies
        run: netpol-analyzer eval --dir manifests --output text --fail-on-empty-selectors
Enter fullscreen mode Exit fullscreen mode

This job runs on every PR that touches a policy file. It fails the build if the diff shows a connection being removed. That's the check that would have caught the checkout-to-inventory-db example above, before merge instead of during an incident.

Stage 2: Live connectivity test on merge to main

name: netpol-live-validation

on:
  push:
    branches: [main]
    paths:
      - 'manifests/**/networkpolicy*.yaml'

jobs:
  live-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up cluster access
        run: |
          echo "${{ secrets.STAGING_KUBECONFIG }}" > kubeconfig
          echo "KUBECONFIG=$(pwd)/kubeconfig" >> $GITHUB_ENV

      - name: Apply policies to staging
        run: kubectl apply -f manifests/ -n staging

      - name: Install cilium CLI
        run: |
          curl -L --remote-name-all https://github.com/cilium/cilium-cli/releases/latest/download/cilium-linux-amd64.tar.gz
          tar xzf cilium-linux-amd64.tar.gz
          sudo mv cilium /usr/local/bin/

      - name: Run connectivity test
        run: cilium connectivity test --test-namespace cilium-test --timeout 10m
Enter fullscreen mode Exit fullscreen mode

This second stage runs only on merge, against a real staging cluster, and catches dataplane-level problems that static diffing can't see. It's slower and it needs a live cluster, so it doesn't belong on every PR, but it's the last line of defense before the same policies get promoted to production.

Stage 3 (optional): scheduled drift check

Policies drift from what's in git if anyone applies changes directly to the cluster. A nightly job that diffs the live cluster state against the manifests in main catches that:

on:
  schedule:
    - cron: '0 6 * * *'
Enter fullscreen mode Exit fullscreen mode

Run netpol-analyzer diff between a live export of the cluster's policies (kubectl get networkpolicy -A -o yaml) and the manifests in git. Any difference means someone bypassed the pipeline, and you want to know before it causes a support ticket six weeks later when nobody remembers making the change.

What this catches in practice

A rule of thumb for interpreting output from netpol-analyzer diff: a removed connection touching anything with active traffic is a merge blocker. A newly allowed connection is worth a second look but rarely urgent. An empty-selector warning means someone wrote a policy that does nothing, which is a silent no-op rather than a security control, and it should get fixed even though it won't break anything today.

None of this replaces code review. It replaces the part of code review that asks us to mentally trace traffic across a namespace, which people are genuinely bad at and a diff tool is genuinely good at. Static check on every PR, live test on merge, drift check on a schedule. Do that and the first time you hear about a bad NetworkPolicy rule stops being an incident channel at 2am.

Top comments (0)