I built a security-impact diff for GitOps

Before and after rendered manifests flow into a focused security-impact review

Original diagram for this build note. It shows the tool’s review model, not a production cluster topology.

A Kubernetes pull request can look harmless when the visible change is short: a new RoleBinding, one securityContext field, a route rule, or a service type. The difficult part is that these changes rarely read like the security event they create.

A Role can gain access to Secrets. A workload can begin sharing a host network namespace. A route can make a new hostname reachable. None of that has to look dramatic in a line-by-line diff.

I wanted a small tool that starts from the artifact GitOps teams already review: rendered manifests before and after a change. That became kube-blast-radius, an offline CLI that explains the security-relevant capability or surface added by that change.

The input is the delivery artifact, not another policy file

The command compares two already-rendered states. It can also render Kustomize directories or Helm charts first.

kube-blast-radius diff \
  --before rendered/main.yaml \
  --after rendered/pr.yaml

For a simple fixture, a Role gained Secret access while a Deployment enabled hostNetwork and added a privileged container. The CLI reported:

HIGH  Role/payments/checkout: RBAC rule newly grants access to secrets
      (RBAC_SECRET_ACCESS_ADDED)

HIGH  Deployment/payments/checkout: workload newly enables hostNetwork
      (WORKLOAD_HOST_NETWORK_ADDED)

HIGH  Deployment/payments/checkout: workload newly adds a privileged container
      (WORKLOAD_PRIVILEGED_ADDED)

The intent is not to decide whether every finding is wrong. A privileged DaemonSet or a public route can be intentional. The point is to turn a subtle manifest change into a question a reviewer can answer deliberately.

I treated false assurance as the main bug

The first version found the obvious cases quickly. The more important work was finding ways it could say too little.

An independent review caught a few examples. A ClusterRole referenced by a namespace-scoped RoleBinding was initially easy to miss because ClusterRoles do not have a namespace. A kind: List wrapper could hide objects from a naive document parser. And an omitted ServiceAccount and an explicit default ServiceAccount should mean the same thing, not produce a scary but meaningless finding.

Those became regression tests, not release notes:

ClusterRole + RoleBinding -> effective Secret access
kind: List              -> expanded and analyzed
missing identity        -> analysis error, not silent skip
implicit default        -> normalized before comparison

The tool also reports unsupported resource kinds as information instead of quietly declaring the change clean. Its text output always carries the same caveat: enabled checks are not a complete security assessment.

Render first, then compare

Raw YAML is useful, but many GitOps changes live in overlays and values files. The tool supports three modes:

raw        YAML manifest stream
kustomize  kubectl kustomize <directory>
helm       helm template <chart> with before/after values

The render commands use argument vectors rather than a shell, have a fixed timeout, and do not load kubeconfig or talk to a Kubernetes cluster. That is a deliberate boundary. Rendering still means trusting the chart or overlay source, so untrusted inputs belong in an isolated runner.

I tested the renderers inside a Docker container with a read-only filesystem, a non-root user, all Linux capabilities dropped, no-new-privileges, and read-only mounted fixtures. Kustomize caught a newly enabled hostNetwork; Helm caught a values change that made a container privileged.

A real GitOps artifact changed the scope

The useful test was a read-only render of a detached Traefik route-review artifact from a GitOps repository. I exported only that tracked review directory at the commit that introduced it, rendered it with Kustomize, and compared it with an empty prior state. No cluster, DNS provider, Secret values, or live route was accessed.

The first run did not understand Traefik IngressRoute resources. That was the right result to take seriously: an “unsupported kind” message is better than pretending a route addition has no security meaning.

I added focused support for Traefik route matches. The next run marked the added desktop and mobile route rules as high-severity exposure changes. It did not call them malicious. It made their external-surface effect visible in the review output.

What the first release covers

v0.1.0 looks for changes including:

  • bound RBAC access to Secrets, wildcards, escalation verbs, and broader resource-name scope;
  • new bindings to roles that already grant Secret access;
  • privileged containers, host namespaces, hostPath volumes, added Linux capabilities, UID 0, and weakened container hardening;
  • Service external exposure, Kubernetes Ingress hosts and paths, removed NetworkPolicies, and Traefik IngressRoute rules.

It returns text for a reviewer and JSON for CI. High findings exit with code 1; malformed manifests and renderer failures exit with 2.

What it does not claim

This is not a cluster security platform. It does not calculate full NetworkPolicy reachability, inspect cloud IAM, query a live cluster, read Secret values, or certify compliance.

That limitation is part of the product. A GitOps diff tool should be trusted for the specific questions it can answer, not for imaginary coverage.

The source, release binary, and checksum are public:


Test notes: I built and tested the CLI locally and in disposable Docker containers. The hardened renderer test used a read-only filesystem, non-root UID 65532, dropped Linux capabilities, no-new-privileges, and read-only mounted synthetic fixtures. The GitOps validation was a read-only render of a detached review artifact; no cluster, Secret value, DNS provider, or live route was accessed. The examples are sanitized.

Related work: Security and regulated operations case study · A rollout needs a return address