Hedronite · Ops Lesson · 01-Earth-DevOps · Sat 2026-10-03

GKE Binary Authorization require attestation and a Rust image census

The cluster flag selects the project policy. The Pod spec only stores an image string.

Lesson Class: Ops (T2 · GKE + kube-rs census)
Cloud Referent: GKE Binary Authorization · REQUIRE_ATTESTATION · project singleton policy
Automation: cargo script + kube 2.0.1 + Nix writeBashBin
Checked: cargo check exit 0 · classifier cargo run real · cluster list illustrative
Paired Dev: match guards on image references
Paired Cert: CKS enforced block versus dry-run
Cluster switch
PROJECT_SINGLETON_POLICY_ENFORCE or DISABLED. The bool enabled is deprecated.
Rule vs action
evaluation_mode asks. enforcement_mode blocks or only logs.
Shape is not a signature
DigestPinned means the spec names a digest. It does not mean attested.
Flip the cluster only after the default rule requires an attestor you can read.

<!-- hal:authoritative:yaml -->

The cluster flag selects the project policy. The policy names the attestor. The Pod spec only stores an image string.

§I. Frame

K8s day 73 still ships Ops, Dev, and Cert together. The Ops-plus-Cert-only clock starts Sunday.

09-12 already put Pod Security Admission labels on an AKS namespace. 09-27 was GKE surge and PodDisruptionBudgets. 09-30 was EKS Fargate profile selectors. The 09-18 Gateway lesson named Binary Authorization as a later GKE fire. That later fire is this one.

The problem for today: point one Standard cluster at the project Binary Authorization policy, require an existing attestor on the default rule, and census image strings on Pods. A digest in the spec is not an attestation.

§II. Three owners

  1. Cluster switch. google_container_cluster.binary_authorization.evaluation_mode. PROJECT_SINGLETON_POLICY_ENFORCE makes that cluster use the project singleton policy. DISABLED does not. The bool enabled is deprecated. The Terraform resource documents only those two strings. gcloud beta also accepts policy-binding modes. Do not write those strings into this block.
  1. Project policy. google_binary_authorization_policy is one resource for the project. default_admission_rule.evaluation_mode is ALWAYS_ALLOW, ALWAYS_DENY, or REQUIRE_ATTESTATION. REQUIRE_ATTESTATION requires a non-empty require_attestations_by. The attestor must already exist, and the caller changing the policy must be able to read it. enforcement_mode is a second field. ENFORCED_BLOCK_AND_AUDIT_LOG rejects the Pod and writes Cloud Audit Logs. DRYRUN_AUDIT_LOG_ONLY admits the Pod and still writes the violation.
  1. Exemptions. global_policy_evaluation_mode = "ENABLE" turns on Google's system-image evaluation, so GKE-managed images are not held to your attestor. admission_whitelist_patterns.name_pattern is your own name list. A namespace label does not exempt an image. That label is the 09-12 PSA mechanism, and this policy does not read it.

cluster_admission_rules overrides the default for one key shaped like us-central1-a.payments. No matching key means the default rule applies.

There is one Binary Authorization policy per project. A second google_binary_authorization_policy resource does not create a second policy. Import the existing one if Terraform does not already own it.

Flip the cluster only after you have read the live default rule. PROJECT_SINGLETON_POLICY_ENFORCE plus ALWAYS_DENY rejects every image that is not exempt, including ones you meant to attest later. PROJECT_SINGLETON_POLICY_ENFORCE plus ALWAYS_ALLOW admits everything and teaches you nothing. The useful pair is REQUIRE_ATTESTATION with the attestor list filled in, then the cluster flag. Dry-run (DRYRUN_AUDIT_LOG_ONLY) is the safe first enforcement value while you watch Cloud Audit Logs for misses.

A rejected create comes back from the API server as a forbidden Pod. The audit log is the record of which rule and which attestor missed. This lesson does not paste a kubectl error string, because no cluster was queried.

§III. The Terraform

Schema-faithful to current hashicorp/google docs. Not applied, and not terraform validated this fire.

resource "google_container_cluster" "payments" {
  name               = "payments"
  location           = "us-central1-a"
  initial_node_count = 1

  binary_authorization {
    evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE"
  }
}

resource "google_binary_authorization_policy" "project" {
  global_policy_evaluation_mode = "ENABLE"

  admission_whitelist_patterns {
    name_pattern = "us-central1-docker.pkg.dev/payments-prod/break-glass/*"
  }

  default_admission_rule {
    evaluation_mode         = "REQUIRE_ATTESTATION"
    enforcement_mode        = "ENFORCED_BLOCK_AND_AUDIT_LOG"
    require_attestations_by = [var.build_attestor_name]
  }
}

var.build_attestor_name is the attestor resource name. Use projects/PROJECT/attestors/NAME when the attestor lives in another project. This lesson does not create the Container Analysis note or the Cloud KMS key. Those feed the attestor. They are not the admission rule.

A dry-run rollout keeps REQUIRE_ATTESTATION and sets enforcement_mode to DRYRUN_AUDIT_LOG_ONLY. That change does not add or remove attestors. It changes whether a miss blocks.

§IV. The Rust census

Admission evaluates the digest resolved at that moment. The Pod spec is a weaker record: a tag can move after an attestor signed the old digest. The census classifies the string. It does not call Container Analysis, so it never prints "attested".

The match guard runs first. A reference containing @sha256: is DigestPinned, including repo/app:1.2@sha256:dead. Otherwise take the name before @, then the tag after the last slash. A colon in front of that slash is a registry port (localhost:5000/app), not a tag. No tag, or the tag latest, is ImplicitLatest. Any other tag is MovingTag. Empty or absent is Missing.

Print ATTENTION for MovingTag, ImplicitLatest, and Missing. DigestPinned means the spec names a digest. It does not mean an attestor signed it.

Full script: gke-binauthz-image-census.rs (nightly -Zscript). Nix wrapper: gke-binauthz-image-census.nix (writeBashBin). The wrapper was not built this fire.

Checked on the lab Mac. cargo check of a scratch bin crate (script body, same deps, stable rustc 1.99.0) finished with exit 0. Resolved kube 2.0.1, k8s-openapi 0.26.1 feature v1_34, tokio 1.53.2. Not run against a cluster.

Illustrative cluster output (no kubeconfig used):

payments/api image=us-central1-docker.pkg.dev/payments-prod/app/api@sha256:abc shape=DigestPinned
payments/api image=us-central1-docker.pkg.dev/payments-prod/app/api:1.4.2 shape=MovingTag ATTENTION
summary containers=2 attention=1

Real cargo run --offline --quiet of the same classifier on the lab Mac (no cluster):

None -> Missing
Some("gcr.io/payments/api@sha256:abc") -> DigestPinned
Some("gcr.io/payments/api:1.4.2") -> MovingTag
Some("gcr.io/payments/api:latest") -> ImplicitLatest
Some("gcr.io/payments/api") -> ImplicitLatest
Some("localhost:5000/payments/api") -> ImplicitLatest
Some("localhost:5000/payments/api:1") -> MovingTag
Some("gcr.io/payments/api:1.2@sha256:dead") -> DigestPinned

§V. How to run

cargo +nightly -Zscript ./gke-binauthz-image-census.rs

Point kubeconfig at the cluster you mean to read. The script lists every namespace with Api::all and prints one line per container. It does not delete or patch Pods.

§VI. Close

Three owners: the cluster evaluation mode, the project rule (what to check, then whether a miss blocks), and the exemption lists. The Rust census reads image-reference shape from Pod specs. It does not prove an attestation.

Paired Dev: the same classifier written as match and match guards, inside the Patterns chapter already shipped. Paired Cert: enforced block versus dry-run, and why a registry-prefix webhook is a different control.

Related