Inspiration

An issue asks an AI agent to let a payment provider's webhooks reach a payments service. The agent's merge request does three things that each look minor. It opens the webhook load balancer to the whole internet. It lets any pod reach checkout on any port. It widens checkout's database role from cloudsql.client to cloudsql.admin. Every scanner passes, because nothing here is bad in general. It is only wrong compared with what was asked for. Together, the three changes form one chain from the internet to the production database, and no tool in the pipeline connects them or knows what was agreed.

AI didn't create this problem, but it made it urgent. Infrastructure changes now arrive faster than people can read them, and enterprises are holding back on agents because they can't govern what the agents change. We wanted a gate that answers one question for every change, whoever wrote it: does it allow exactly what was agreed, and nothing more?

What it does

ProofGate checks that every Terraform + Kubernetes change on Google Cloud allows exactly what was agreed (the intent) and nothing more. It proves this live in a sandbox, signs the evidence, and lets agents earn autonomy. Agents do the work; code is the referee.

When a change arrives as a merge request, a trusted pipeline that the change cannot edit:

  1. Guards against forbidden code before anything is planned (an HCL allowlist, plus an integrity guard so an infra MR can't touch intent, CI or the verifier).
  2. Computes facts from the plan against the live estate: who can reach what across pod networking, the network edge and cloud IAM, and which permissions are granted.
  3. Compares every fact with approved intent and builds attack paths to the crown jewels (a database, a bucket). This includes paths through old debt that was already accepted.
  4. Assigns a tier: Blocked, Human review, Standard or Auto.
  5. Proves it live. It applies the change in a sandbox, replays each link of every path, cross-checks with Google (Connectivity Tests, testIamPermissions) and runs smoke journeys.
  6. Signs a verification attestation. Only the signed head can merge.
  7. Releases under supervision. Staging is applied after merge, and production waits for one human approval of the outcome.

Agents built on the GitLab Duo Agent Platform do the work:

  • An Intent Planner drafts default-deny intent from the issue.
  • An Author agent builds against that intent and self-checks through ProofGate's MCP server.
  • A Fixer team (a coordinator, edge/pod/IAM specialists and an Explainer) repairs blocked changes from a repair plan that code computes.

No model output ever enters a fact, a verdict, a tier, an approval, the merge decision or an attestation. Findings appear in GitLab's own MR security widget, next to GitLab IaC scanning.

How we built it

  • Engine: deterministic Python 3.12, also served as a CLI (check) and an MCP server. Network policy is evaluated with the pinned netpol-analyzer binary, not a reimplementation. IAM is compared permission by permission through a locked role catalog. The attack graph is node-level and searched with a deterministic, depth-bounded BFS. Every output is canonical JSON with sha256 digests and stable IDs, and a test checks that each producer gives byte-identical output when run twice and with shuffled input.
  • GitLab: custom Duo flows, a Pipeline Execution Policy that injects the gate jobs into the application project, a merge request approval policy, a separate verifier project, the security report and MR widget, the generic package registry, scheduled pipelines, resource_group, CODEOWNERS, environments and fast-forward merges.
  • Google Cloud: one GKE cluster each for production, staging and sandbox. Keyless access everywhere through GitLab ID tokens and Workload Identity Federation, conditioned on the verifier's CI file on its protected branch. Terraform state lives in GCS, signing is keyless with Sigstore/cosign, and the MCP server runs on Cloud Run.
  • Benchmark as acceptance suite: synthetic merge requests (patch, plan overlay, estate overlay) against a synthetic estate, using only RFC 5737 addresses. A case passes only if every expected fact, path and tier matches.
  • Workflow: we built in strict plan order, one complete feature per commit. Each commit carries its code, tests and benchmark cases together and passed lint, mypy --strict, tests, schema drift and benchmark checks before it was committed.

Challenges we ran into

  • Building on a Developer+ role. We couldn't configure protected environments or a second approver, so production approval became a manual job. It proceeds only if the user who starts it is listed in intent/release-approvers.yaml on main. We document each stand-in like this instead of hiding it.
  • Custom flows can't use MCP servers on GitLab.com. Our flows reach the checker through run_command. External agents such as Claude Code and Cursor connect to the MCP server directly.
  • Duo agents are members of the policy project. They can't forge a pass, because the gate checks a signature they can't produce. They could still weaken the policy, so a settings-watch job reports any change to the policy or injected CI files.
  • Planning needs production read access. The hardened plan job is a stated exception, and it has several controls: the allowlist runs first, providers are pinned, a custom read-only role excludes data access, and raw plans go to a bucket the job can't read back. We wrote down the risk that remains.
  • A NetworkPolicy ipBlock can't select a pod IP. Modelling Cloud SQL faithfully in the sandbox needed a stand-in VM.
  • Determinism everywhere. Making facts, paths and tiers byte-identical across runs and input orderings took discipline in IDs, sorting and digests.

Accomplishments that we're proud of

  • The demo story works. P1, the three-change chain above, is Blocked, with one new attack path to prod-db shown in plain language on the MR (live-proven 25 Oct). GitLab's IaC scanning raised a single generic finding for the same change. The repaired change, G7, reaches the Auto tier in the static benchmark.
  • Live sandbox proof. Apply and destroy leave nothing behind. Every path probe matched its expectation, and Google Connectivity Tests agreed. After the fix, the broader links were closed and both journeys passed (26–27 Oct).
  • Static benchmark (run 45, engine commit c6fab5e):
    • Detection: 36 of 36 known-bad and chain cases caught.
    • False alarms: 0 of 8 good cases Blocked.
    • Auto-tier reach: 7 of 7.
    • Self-check agreement: the parse-only check agrees with the full check in 38 of 38 cases.
  • No keys anywhere. Cloud access and signing are both keyless through OIDC.
  • Offline audit. An evidence bundle can be verified with cosign and the repository alone, with no GitLab login and no cloud access.

What we learned

  • Most real exposure comes from the combination of changes, not from any single one. You only see it by joining network, edge and identity into one graph and comparing against what was agreed, not against generic rules.
  • "Agents do the work; code is the referee" is a design rule you have to enforce at every boundary. The test is whether any model output can reach a decision. If it can, the trust model is broken.
  • Supervision works best on the outcome, not the steps. A single, informed production approval can replace many step approvals, while risky tiers still get more review.
  • A benchmark that the README is tested against keeps you honest. If a number isn't in a committed results file, we don't publish it.

What's next for ProofGate

  • Prove G7 end to end through staging and production, and run the Fixer and Intent Planner live, with committed agent benchmark results.
  • Run the pinned scanners against the benchmark to publish the scanner gap: how many dangerous changes pass every scanner.
  • Finish the learning loop: drift issues with Fixer revert MRs, unused-permission narrowing and exception expiry.
  • Add the signed OCI bundle of the verified plan and manifests (the Package stage).
  • Move to a real corporate layout: a protected environment for production, a second approver for guardrail changes, and a dedicated merge-bot identity.

Built With

Share this project:

Updates

Submission history