Skip to content

Add policy testing, NetworkPolicy fence, and drift audit - #5

Draft
kylecrawshaw wants to merge 2 commits into
mainfrom
claude/networkpolicy-kyverno-anteroom-zk8zbs
Draft

kylecrawshaw wants to merge 2 commits into
mainfrom
claude/networkpolicy-kyverno-anteroom-zk8zbs

Conversation

@kylecrawshaw

Copy link
Copy Markdown
Member

Summary

This PR adds comprehensive offline testing for Kyverno policies, introduces two new policies (NetworkPolicy fence and drift audit), and enhances the Service rewrite policy with automatic restoration when labels are removed.

Key Changes

Testing Infrastructure

  • Added charts/kyverno-policies/tests/run.sh: A comprehensive test harness that uses kyverno apply to evaluate policies offline against fixture resources, comparing output against golden files
  • Added test fixtures: resource files and expected outputs for all policy scenarios
  • Integrated policy testing into CI via new charts job in .github/workflows/ci.yaml
  • Added helm-test target to Makefile

New Policies

  • clusterpolicy-generate-networkpolicy.yaml: Opt-in policy that generates a NetworkPolicy per namespace, admitting only TCP 8080 (the gate's port) to gated pods, closing the side door where the application's own port remains accessible on the pod IP
  • clusterpolicy-audit-drift.yaml: Audit-only policy that reports pods carrying the inject label but no gate container—the invisible failure state when a pod was admitted while injection was disabled

Enhanced Service Rewrite Policy

  • clusterpolicy-route-service.yaml: Now includes three rules instead of one:
    1. Records original spec.ports in annotation before rewriting (precondition: incoming object is un-rewritten)
    2. Rewrites targetPorts to the named port "anteroom"
    3. Restores original ports when the proxied label is removed
  • This solves the removal problem: without recording, removing the label leaves the Service targeting a non-existent port name, blackholing it
  • Controlled by new policies.restoreService value (default: true)

Configuration & Documentation

  • Added policies.restoreService, policies.networkPolicy, and policies.auditDrift chart values
  • Updated Chart.yaml version to 0.2.0
  • Enhanced README and README.gotmpl with documentation of new policies, the restoration mechanism, and NetworkPolicy caveats
  • Updated examples/kyverno/README.md to document the new policies and removal workflow
  • Added helper template kyverno-policies.gatePort to keep the gate port (8080) in one place
  • Updated RBAC to grant NetworkPolicy permissions when the fence is enabled

Notable Implementation Details

  • The test harness normalizes output by stripping Kyverno-generated labels and chart version labels, making expectations stable across CLI and chart upgrades
  • Service port recording uses the full array rather than a port-to-value map to preserve the absence of explicit targetPorts (which default to port at resolution time)
  • The record-only-when-un-rewritten precondition prevents overwriting the record on subsequent updates of already-rewritten Services
  • NetworkPolicy is off by default due to CNI-dependent failure modes (probe traffic exemption varies by plugin)
  • Audit policy uses background: true to scan existing pods, not just newly admitted ones

https://claude.ai/code/session_018788NnHJYEyyhhuuV2BuST

claude added 2 commits August 28, 2026 19:14
Two gaps in the Kyverno path, both about the difference between "the policy
ran" and "the deployment is in the state you meant".

The side door. Rewriting the Service moved Service traffic behind the gate and
left the pod IP answering on the app's own port to anything in the cluster.
The example README described the fence and told you to write it yourself,
because its failure mode is CNI-dependent. That reasoning stands, so the new
generate-anteroom-networkpolicy policy is off by default — but it is a value
now rather than a snippet, with the two ways it fails silently spelled out:
kindnetd does not implement NetworkPolicy at all (it applies cleanly and
fences nothing), and where a CNI does enforce it, the app container's probes
target precisely the port being fenced. networkPolicy.extraIngress is the
node-CIDR escape hatch. Ingress-only on purpose: naming Egress would take DNS
out from under every gated pod.

Removal. A mutation is stored, not overlaid, so removing a Service's proxied
label left it targeting the port name "anteroom" with nothing able to
reconstruct the original — the trap the README warned about under "remove the
Service label first". Kyverno can undo it, but only if the original was
written down first, so the rewrite now records spec.ports verbatim into an
annotation and a third rule restores them on the update that removes the
label. The array is stored whole because a port with no explicit targetPort
has none for a reason, and a port-to-value map cannot say "absent".

The rest of the cleanup story turned out to be Kyverno's already, and is now
documented rather than left to be discovered: a synchronized generate rule
deletes its downstream when the trigger stops matching, so dropping a
namespace label removes the ConfigMap, the NetworkPolicy and the cloned
Secret. Uninstall is asymmetric — generated-from-data downstreams are deleted
in every namespace, clones are retained — hence
orphanDownstreamOnPolicyDelete. A running pod's sidecar is the one thing no
policy can remove, since container lists are immutable after admission; that
needs a rollout, and audit-anteroom-drift reports the inverse state (labeled,
no gate) which is otherwise indistinguishable from the app being down.

Fixed while here: the rewrite rule errored on a Service with no spec.ports
(Kyverno raises on the missing field rather than yielding an empty list), and
a mutate error is an admission denial under the default failurePolicy — so
labeling an ExternalName Service by mistake got it refused instead of skipped.

Verified with charts/kyverno-policies/tests/run.sh, which evaluates each
policy against fixtures using `kyverno apply` and diffs the result against
committed expectations — including the cases that motivated the design (an
implicit targetPort surviving the round trip, the port-less Service, the drift
rule applied alone so it sees an unmutated pod the way a background scan
does). Wired into a new CI job, since nothing checked the charts before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018788NnHJYEyyhhuuV2BuST
…ence

main landed `admin.networkPolicy.enabled` while this branch was open, and it
generates very nearly the policy this branch added: same trigger, same
podSelector, same `policyTypes: [Ingress]`, 8080 from anywhere, an
extraIngress escape hatch, and the same CNI and kubelet-probe caveats. Its
switch is named for the admin listener because fencing an exposed /metrics is
what it was built for, but it carries its own guard, so it already closes the
side door with `admin.expose` left off.

Two policies selecting the same pods would have been additive in Kubernetes
and confusing everywhere else — two objects, two names, two docs sections, and
union semantics for a reader to work out. So this drops the duplicate
(`policies.networkPolicy`, the top-level `networkPolicy:` block, and
clusterpolicy-generate-networkpolicy.yaml) and keeps main's, which is the
author's shipped design.

Nothing is lost by that. What this branch had and main's rule did not, it now
has:

  - The fence goldens in tests/ were regenerated against main's rule, so the
    tests now cover `generate-admin-networkpolicy` — including the collector
    ingress and extraIngress appending — rather than a policy that no longer
    exists.
  - `orphanDownstreamOnPolicyDelete` now applies to the NetworkPolicy too,
    not just the ConfigMap. Both are generated from data, so both are deleted
    when the policy is, and neither should be surprising at `helm uninstall`.
  - main's rule hardcoded `port: 8080` a second time; it now uses the
    gatePort constant this branch added, so the fence cannot drift from the
    containerPort the injection policy sets.

Kept from main verbatim: rbac.yaml (its `admin.networkPolicy.enabled` guard is
the correct condition now), the admin container port on the injected sidecar,
and the "Scraping the fleet" documentation. Chart version goes to 0.3.0 since
main already shipped 0.2.0.

Conflicts resolved in values.yaml, README.md.gotmpl, rbac.yaml,
clusterpolicy-inject-sidecar.yaml, and the generated README.md (regenerated
with helm-docs rather than hand-merged). No Go file diverges from main.
Verified with helm lint, the 12-case policy suite, go vet, and go test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018788NnHJYEyyhhuuV2BuST
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants