Help us shape the future of Go! Take the 2026 Go Developer Survey

kmsrole

command
v0.5.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Overview

Package main implements kmsrole, the guard that holds each GCP identity's key-level KMS role to the Encrypt/Decrypt its binary's packages name (#750).

#748 was a Terraform role string kept correct only by the prose around it. Seven comments asserted the executor never decrypts; the code had decrypted for three tagged releases before anyone noticed; nothing failed until a fresh GCP project ran a session — the cipher's startup probe only encrypts, so both processes went Ready and the deploy was green while every clone of a private repository and every vault-credentialed MCP dial lost its credential. The fix corrected the role and the seven comments, which is the same mechanism that failed, applied again. This guard re-derives the fact instead: it reads the role from the Terraform and every Encrypt/Decrypt identifier from the Go source, and fails when an identity is granted less than its binary names.

Where it runs

`make gcp-kms-role-check`, in the gcp-* group and NOT in `make verify`. It reads deploy/gcp's Terraform, and that tree is developer tooling for GCP deployment (plan 20, Decision 9) — never a dependency of the platform, its build, or the Go gate, which is why `gcp-split-check` guards a scarier invariant from outside the gate too. CI runs it beside the other gcp-* checks. This package's own test runs in `make verify` like any Go test, but it reads only fixtures and the Go tree, never deploy/gcp: the split mirrors check_split.py, whose unit tests and whose run against the real configuration are separate targets for the same reason.

The rule, and the direction it is allowed to be wrong in

One-directional: an identity must be granted at least what its code names. An identity granted MORE is not reported, because the failure being prevented is a narrowed grant. It is a floor, not a split — it can say the executor needs Decrypt, never that the brain should stop being able to encrypt.

The Go-source half counts every IDENTIFIER named Encrypt or Decrypt — a call, a method value, a bare same-package call and a declaration alike — in every in-module package a binary transitively imports, read with go/parser so that neither a comment nor a string can contribute one. "Call site" would be the wrong word for what it finds, and the wrong thing to narrow it to. That over-counts on purpose, in four known ways: a member with those names on something other than secrets.Cipher counts, a struct field named Encrypt counts, the Cipher interface's own method declarations count — so any binary importing internal/secrets inherits both permissions whatever it calls — and so does a cipher backend's own call to its cloud client (internal/secrets/gcpkms). Over-counting demands a broader role and names the exact file and line that demanded it; under-counting prints ok over #748 happening again. When a binary appears that genuinely encrypts and never decrypts, the honest refinement is to ignore a call that sits inside a method which itself implements the seam — a backend's inner call, never a consumer's — and it is deliberately not built ahead of a caller that needs it.

What a syntactic scan cannot see, it cannot count: a cipher reached by reflection (`MethodByName("Decrypt")`) is invisible here, and no amount of go/parser will change that. Imports are followed from every non-test .go file regardless of build constraints, which over-includes in the same safe direction.

It assumes cmd/<name> runs as the service account labelled <name>. That is true of the three workloads and not of the other two: cmd/gate is a sidecar in the executor's pod and shares the executor's identity, and cmd/worker is customer-hosted with no GCP identity at all. Neither reaches the cipher today, so the assumption costs nothing; the day one does, this guard reports it ungranted and the mapping has to be taught here.

What it refuses

A refusal is an error, not a finding: the guard could not do its job, and saying so is the whole point of the tool. It refuses a role outside the closed set below, a member it cannot attribute to a service account, a cipher grant whose identity is not a cmd/ binary, a grant carrying `count` or `for_each` (which can make it apply zero times), a grant carrying `ignore_changes` (which decides whether the attributes read here are ever applied on an update), a grant with a nested block other than `lifecycle` or `timeouts`, an override file (whose semantics REPLACE rather than add, so unioning it is wrong), a `module` block (whose configuration lives where this guard does not look, and whose _iam_policy could revoke the members read here), a KMS grant of a kind it does not understand on this key, an IAM deny or principal-access-boundary policy and the bindings that attach one (which SUBTRACT, and are evaluated before the allows read here), a cloudkms role it can read LITERALLY granted above the key to one of these identities (one it cannot — `each.value`, a local — is ignored instead, for the reason wideIAMOK argues), a whole project/folder/organization IAM policy (whose opaque policy_data may grant or revoke cloudkms above the key), a `package main` directly under cmd/ or below cmd/<name> (walked from nowhere, and mapping to no identity), a `.tf.json` file (this reader parses HCL only, and a grant in JSON would read as absent), a `.tf` file outside the two roots an apply loads, a cipher-key block that assigns no `crypto_key_id`, `member` or `role` at all, one that assigns the same attribute twice (the two would disagree and this guard would report on whichever it kept), and any .tf construct its reader cannot read (see hcl.go). It also refuses when grants appear in more than one Terraform root, when it found no grant at all, and when no binary reaches the cipher — the last two would let it print ok over nothing, which is how a checker's bug becomes the input it never reads.

A grant on a crypto key OTHER than the cipher is not refused: it is simply not this key's grant, and ignoring it is the correct reading.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL