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:
- 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.
-
Selectors don't validate against real labels. A typo in
app: payment-svcversus the pod's actualapp: payment-serviceproduces a policy that matches nothing.kubectl applysucceeds. The policy does nothing. You find out when traffic that should be blocked isn't, or worse, traffic that should be allowed is. - 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
Run it against a namespace:
kubectl np-viewer -n checkout
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
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)
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
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
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
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 * * *'
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)