Provisions the project's Binary Authorization policy (
google_binary_authorization_policy) β a project-wide singleton governing default and per-cluster container-image admission rules β together with one or more attestors (google_binary_authorization_attestor,for_each-managed). Targetshashicorp/google ~> 7.0, Terraform>= 1.12.0.
- π‘οΈ Provisions the project's single Binary Authorization policy
(
google_binary_authorization_policy.this) β a project-wide singleton, not a freely creatable resource (see π§ Architecture Notes). - π Manages any number of attestors (
google_binary_authorization_attestor.attestors) viafor_eachovervar.attestors, keyed by attestor name. - π«
default_admission_ruleis required with no default value β a deliberate deviation from the house "empty call produces the safe resource" rule (see π§± Design Principles): an opinionatedALWAYS_DENYdefault could silently block all pod creation, including system pods, with no plan-time warning. - π
require_attestations_byfields accept only keys into this module's ownvar.attestorsmap β never a hand-builtprojects/*/attestors/*string β resolved togoogle_binary_authorization_attestor.attestors[key].idso the reference is a real Terraform dependency edge, not a typo-prone literal. - π Attestors support both PGP (
ascii_armored_pgp_public_key) and PKIX (pkix_public_key) public-key encodings; exactly one is required perpublic_keysentry. - π§±
deletion_policydefaults to"PREVENT"at both the policy and attestor level β a Binary-Authorization-specific secure-default extension to this module suite's conventions, guarding against an accidental reset of a project-wide singleton or afor_eachkey-removal mistake.
π‘ Why it matters: Binary Authorization is the last gate before an image runs on GKE. This module is the house-standard way to express that gate as code, including the constraint the underlying GCP API itself imposes: there is exactly one policy object per project, full stop.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
flowchart LR
ps["tf-mod-gcp-project-services"]:::sibling
kms["tf-mod-gcp-kms-keyring"]:::sibling
note(("Container Analysis Note\n(external, out-of-catalog)")):::external
binauthz["tf-mod-gcp-binary-authorization"]:::self
gke["tf-mod-gcp-gke-cluster"]:::keystone
ps -->|"binaryauthorization.googleapis.com + containeranalysis.googleapis.com enabled"| binauthz
kms -->|"optional pkix_public_key source"| binauthz
note -->|"note_reference (caller-supplied)"| binauthz
binauthz -->|"attestor_ids (require_attestations_by)"| gke
gke -->|"cluster identifier location.clusterId (naming convention only)"| binauthz
classDef self fill:#4285F4,color:#FFFFFF,stroke:#174EA6,stroke-width:2px;
classDef keystone fill:#174EA6,color:#FFFFFF,stroke:#0D47A1,stroke-width:2px;
classDef sibling fill:#ECEFF1,color:#263238,stroke:#90A4AE,stroke-width:1px;
classDef external fill:#ECEFF1,color:#263238,stroke:#90A4AE,stroke-width:1px,stroke-dasharray: 5 5;
Validated via the Mermaid Chart MCP (validate_and_render_mermaid_diagram) before embedding.
The Container Analysis Note is drawn as an external, out-of-catalog node (dashed border), not a
sibling module β no google_container_analysis_note-equivalent resource exists in the catalog
today (see SCOPE.md "Out of scope"). The tf-mod-gcp-gke-cluster edge is bidirectional: this
module's attestor_ids output feeds a GKE composition's own require_attestations_by-style
configuration, while GKE cluster identifiers flow the other way into cluster_admission_rules β but
only as a plain string naming convention ({{location}}.{{clusterId}}), never a Terraform resource
reference.
flowchart TB
subgraph Module["tf-mod-gcp-binary-authorization"]
direction TB
policy["google_binary_authorization_policy.this\n(project-wide singleton keystone)"]:::keystone
dar["default_admission_rule (required)"]:::nested
car["cluster_admission_rules (dynamic, for_each map)"]:::nested
awp["admission_whitelist_patterns (dynamic, list)"]:::nested
polto["timeouts (dynamic)\ncreate/update/delete"]:::nested
attestors["google_binary_authorization_attestor.attestors\nfor_each var.attestors"]:::child
aan["attestation_authority_note (required)"]:::nested
pk["public_keys (dynamic, list)"]:::nested
pkix["pkix_public_key (dynamic, max 1)"]:::nested
attto["timeouts (dynamic)\ncreate/update/delete"]:::nested
end
policy --> dar
policy --> car
policy --> awp
policy --> polto
dar -->|"require_attestations_by[*].id"| attestors
car -->|"require_attestations_by[*].id"| attestors
attestors --> aan
aan --> pk
pk --> pkix
attestors --> attto
classDef keystone fill:#174EA6,color:#FFFFFF,stroke:#0D47A1,stroke-width:2px;
classDef child fill:#4285F4,color:#FFFFFF,stroke:#174EA6,stroke-width:2px;
classDef nested fill:#ECEFF1,color:#263238,stroke:#90A4AE,stroke-width:1px;
Validated via the Mermaid Chart MCP before embedding.
Resource inventory (2 resources):
| Resource | Count | Role |
|---|---|---|
google_binary_authorization_policy.this |
1 (project-wide singleton β see Architecture Notes) | Default + per-cluster admission rules, global policy evaluation mode, whitelist patterns |
google_binary_authorization_attestor.attestors |
for_each over var.attestors |
Attestors verifying container image provenance via PGP or PKIX public keys |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/google |
~> 7.0 |
| Provider block | None β the caller's root module configures google (ADC, WIF, or a service-account key per our authentication model) |
Schema notes that bite:
- The policy is a project-wide singleton, not a freely-creatable resource. Two Terraform
configurations (or two instances of this module) targeting the same project's policy conflict
destructively at
applyβ last-apply-wins, not a merge. See π Troubleshooting. attestation_authority_note.note_referenceis not updatable once set. Changing it forces recreation of the attestor β verified against the live schema ("This field may not be updated").global_policy_evaluation_modeinteracts withdefault_admission_rule. Google's own system-image admission policy can still admit certain images even when the project'sdefault_admission_rule.evaluation_mode = "ALWAYS_DENY"β a fully closed posture requires understanding this interaction, not just setting one field.- Neither resource in this family exposes
labelsorself_link. Confirmed directly against the live, resolved schema (terraform providers schema -json) β this is a total omission across both resources, not a per-resource split. See π§ Architecture Notes. - The policy resource has no
nameattribute either β onlyid. This corrects an assumption in this module's own pre-authoring SCOPE.md draft (see SCOPE.md's Emits section for the full correction note) β the live schema was checked directly rather than trusting the draft. public_keysentries are a one-of, not two independent optional fields. Exactly one ofascii_armored_pgp_public_keyorpkix_public_keymust be set per entry β this module's ownvalidation {}block catches both-set and neither-set atplantime.require_attestations_byrequires the attestor to already exist. The live schema documents: "Each attestor must exist before a policy can reference it." This module resolves everyrequire_attestations_byentry through the attestor resource's own.idattribute specifically to create that ordering guarantee via Terraform's implicit dependency graph.
roles/binaryauthorization.policyEditorβ create/modify the project policy and attestors. (If a narrower split between policy-only and attestor-only administration is desired, GCP may expose a more granular predefined role for attestor management specifically β role name to be confirmed viagcloudbefore relying on it; this module only asserts the role given above.)
(Sourced directly from SCOPE.md β not re-derived.)
binaryauthorization.googleapis.comenabled on the target project.containeranalysis.googleapis.comenabled on the target project β Container Analysis is the backing store for the Notes/Occurrences attestors reference, required even though this module does not create Notes directly.- GKE clusters referenced in
cluster_admission_rulesmust exist for the identifier to resolve at enforcement time. Binary Authorization enforcement happens at pod-admission time, not at Terraform apply time β a dangling cluster identifier will not failplan/apply, only real pod creation against that cluster later.
(Sourced directly from SCOPE.md β not re-derived.)
tf-mod-gcp-binary-authorization/
βββ providers.tf # required_providers (hashicorp/google ~> 7.0) + required_version β no provider {} block
βββ variables.tf # description, global_policy_evaluation_mode, deletion_policy, default_admission_rule,
β # cluster_admission_rules, admission_whitelist_patterns, timeouts, attestors
βββ main.tf # google_binary_authorization_policy.this + google_binary_authorization_attestor.attestors (for_each)
βββ outputs.tf # id, attestor_ids β no self_link, no policy name (see Architecture Notes)
βββ README.md # this file
βββ SCOPE.md # cross-module contract
βββ examples/ # runnable example(s) matching the Quick Start below
# Caller's root module configures the google provider (ADC, WIF, or a service-account key) β
# this module never declares project/region/zone/credentials variables.
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
}
β οΈ default_admission_rulehas no default value β every caller must supply it explicitly. The example above is the least-restrictive valid value (ALWAYS_ALLOW); see the Example Library for aREQUIRE_ATTESTATIONposture with attestors wired in.
Consumes
| Input | Type | Source module |
|---|---|---|
| Container Analysis Note resource name, per attestor | string |
Caller-supplied (out-of-catalog prerequisite) |
| Public key material (ASCII-armored PGP or PKIX PEM), per attestor | string |
Caller-supplied; optionally sourced from tf-mod-gcp-kms-keyring for a PKIX key |
| GKE cluster identifiers for per-cluster admission rules | string ({{location}}.{{clusterId}} convention, not a Terraform id) |
tf-mod-gcp-gke-cluster (by naming convention only) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Policy resource id (project-scoped path) | Reporting/compliance tooling |
attestor_ids |
Map of attestor name β google_binary_authorization_attestor.id |
tf-mod-gcp-gke-cluster or CI/CD pipeline configuration referencing require_attestations_by |
βΉοΈ No
self_linkrow, and nonamerow for the policy β see π§ Architecture Notes for why both are genuinely absent from the live schema, not merely omitted from this table.
1 Β· Minimal ALWAYS_ALLOW baseline (no attestors)
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
}βΉοΈ
attestorsdefaults to{}β a policy that allows everything needs no attestors at all. Use this only as a deliberate, temporary starting point; see example 2 for a default-deny baseline.
2 Β· ALWAYS_DENY default-deny baseline
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "ALWAYS_DENY"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
}π The most restrictive valid baseline β every pod creation is denied unless a
cluster_admission_rulesoverride applies. Confirm this is intentional before applying: it blocks all workloads, including ones GKE itself may need, subject to theglobal_policy_evaluation_modeinteraction noted in Architecture Notes.
3 Β· REQUIRE_ATTESTATION default rule with one attestor (PGP key)
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
attestors = {
"build-provenance" = {
description = "Attests that the image was produced by the approved CI pipeline."
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{
ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n"
}
]
}
}
}
}π‘
require_attestations_by = ["build-provenance"]is a key into this module's ownattestorsmap, never a hand-builtprojects/*/attestors/*string βmain.tfresolves it togoogle_binary_authorization_attestor.attestors["build-provenance"].id.
4 Β· Per-cluster ALWAYS_ALLOW override for a non-production cluster
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
cluster_admission_rules = {
"us-central1.non-prod-sandbox" = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
}
attestors = {
"build-provenance" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{ ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" }
]
}
}
}
}βΉοΈ
cluster_admission_rulesmap keys use Binary Authorization's own{{location}}.{{clusterId}}identifier convention β sourced fromtf-mod-gcp-gke-clusterby naming convention only, not a Terraform resource reference. The referenced cluster does not need to exist atplan/applytime.
5 Β· admission_whitelist_patterns excluding a base-images registry
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
admission_whitelist_patterns = [
{ name_pattern = "gcr.io/casey-prod-networking/base-images/*" },
]
attestors = {
"build-provenance" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{ ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" }
]
}
}
}
}
β οΈ Everyadmission_whitelist_patternsentry is a deliberate carve-out of this project's entire Binary Authorization posture β images matching the pattern are always permitted, regardless of any other rule. Use sparingly and only for known-trusted registries.
6 Β· Attestor using pkix_public_key
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["vuln-scan"]
}
attestors = {
"vuln-scan" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/vuln-scan"
public_keys = [
{
pkix_public_key = {
public_key_pem = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
signature_algorithm = "ECDSA_P256_SHA256"
}
}
]
}
}
}
}βΉοΈ
signature_algorithmis deliberately not validated against a hardcoded enum β Cloud KMS/ Binary Authorization's signature-algorithm list is large and evolving; an invalid value is rejected by the API atapply, not caught by this library's plan-onlyvalidate/fmtgate.
7 Β· Multiple attestors, one REQUIRE_ATTESTATION rule requiring both
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance", "vuln-scan"]
}
attestors = {
"build-provenance" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{ ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" }
]
}
}
"vuln-scan" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/vuln-scan"
public_keys = [
{
pkix_public_key = {
public_key_pem = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
signature_algorithm = "ECDSA_P256_SHA256"
}
}
]
}
}
}
}π‘ Every attestor listed in
require_attestations_bymust independently attest to the image β a logical AND, not an OR. All attestors referenced must already be entries in this sameattestorsmap (see Design limitation in Architecture Notes).
8 Β· DRYRUN_AUDIT_LOG_ONLY staging rollout
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "DRYRUN_AUDIT_LOG_ONLY"
require_attestations_by = ["build-provenance"]
}
attestors = {
"build-provenance" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{ ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" }
]
}
}
}
}π‘
enforcement_mode = "DRYRUN_AUDIT_LOG_ONLY"logs what would have been denied without actually blocking pod creation β the recommended way to stage a newREQUIRE_ATTESTATIONrule before flipping toENFORCED_BLOCK_AND_AUDIT_LOG.
9 Β· Multiple public keys on one attestor (key rotation overlap)
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
attestors = {
"build-provenance" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{ comment = "2026 key", ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" },
{ comment = "2025 key (rotation overlap)", ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" },
]
}
}
}
}βΉοΈ Any one of the listed public keys can satisfy this attestor β useful during a signing-key rotation window where both old and new signatures must be accepted temporarily.
10 Β· Custom policy-level timeouts
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
timeouts = {
create = "20m"
update = "20m"
delete = "20m"
}
}βΉοΈ The policy resource supports all three timeout operations β verified independently against the live schema, each defaulting to 20 minutes when unset.
11 Β· Custom per-attestor timeouts
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
attestors = {
"build-provenance" = {
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/build-provenance"
public_keys = [
{ ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" }
]
}
timeouts = {
create = "15m"
update = "15m"
delete = "15m"
}
}
}
}12 Β· Opting out of the deletion guard for a disposable dev attestor
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
attestors = {
"scratch-attestor" = {
deletion_policy = "DELETE"
attestation_authority_note = {
note_reference = "projects/casey-security-tooling/notes/scratch"
public_keys = [
{ ascii_armored_pgp_public_key = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...\n-----END PGP PUBLIC KEY BLOCK-----\n" }
]
}
}
}
}
β οΈ deletion_policydefaults to"PREVENT"on every attestor (secure-by-default extension). Setting it to"DELETE"restores the provider's own destroy semantics β only appropriate for an attestor genuinely never referenced by a liverequire_attestations_byrule.
13 Β· Global policy evaluation mode disabled
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
global_policy_evaluation_mode = "DISABLE"
default_admission_rule = {
evaluation_mode = "ALWAYS_DENY"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
}π Disabling the Google-maintained global policy means system-level images are evaluated purely against this project's own
default_admission_rule/cluster_admission_rulesβ confirm this is intentional, since Google's global policy exists specifically to avoid breaking known-good system components.
14 Β· Explicit policy-level deletion_policy opt-out (temporary/testing project)
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
deletion_policy = "ABANDON"
default_admission_rule = {
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
}
}
β οΈ deletion_policydefaults to"PREVENT"on the policy resource (secure-by-default extension β see Design Principles)."ABANDON"removes it from Terraform state without touching the live policy β appropriate for a genuinely temporary/testing project being decommissioned, never for a production project.
15 Β· ποΈ End-to-end composition
Wires a caller-supplied Container Analysis Note reference (out-of-catalog prerequisite) and an
optional tf-mod-gcp-kms-keyring PKIX key into an attestor entry, then feeds this module's
attestor_ids output into a require_attestations_by-style reference for a
tf-mod-gcp-gke-cluster composition β reflecting the documented Consumes/Emits relationships in
both modules' SCOPE.md files.
# Out-of-catalog prerequisite: the caller creates/owns the Container Analysis Note this module
# does not manage (see SCOPE.md "Out of scope / consumed by id").
resource "google_container_analysis_note" "build_provenance" {
name = "build-provenance"
attestation_authority {
hint {
human_readable_name = "build provenance attestor"
}
}
}
# Optional PKIX key source β tf-mod-gcp-kms-keyring never exposes raw key material for signing
# directly, but a caller-managed asymmetric key's public PEM can back a pkix_public_key entry.
module "kms_keyring" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-kms-keyring.git?ref=v1.0.0"
key_ring_name = "casey-binauthz-signing"
location = "us-east1"
crypto_keys = {
"attestor-signing-key" = {
purpose = "ASYMMETRIC_SIGN"
rotation_period = null
version_template = {
algorithm = "EC_SIGN_P256_SHA256"
}
}
}
}
module "binary_authorization" {
source = "git::https://github.com/microsoftexpert/tf-mod-gcp-binary-authorization.git?ref=v1.0.0"
default_admission_rule = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
cluster_admission_rules = {
"us-central1.casey-prod-workloads" = {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = ["build-provenance"]
}
}
attestors = {
"build-provenance" = {
description = "Attests that the image was produced by the approved CI pipeline."
attestation_authority_note = {
# Caller-supplied, out-of-catalog prerequisite β the Note resource above, not a Terraform
# reference this module manages.
note_reference = google_container_analysis_note.build_provenance.name
public_keys = [
{
pkix_public_key = {
# Public key PEM exported out-of-band from the KMS asymmetric key above β Cloud KMS
# does not expose a Terraform-native "get public key" data source in this provider
# version, so this value is retrieved via `gcloud kms keys versions get-public-key`
# and supplied here as a literal (or via a secure out-of-band pipeline step).
public_key_pem = "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
signature_algorithm = "EC_SIGN_P256_SHA256"
}
}
]
}
}
}
}
# tf-mod-gcp-gke-cluster (illustrative β not part of this module) consumes attestor_ids via its
# own require_attestations_by-style configuration, and is itself the source of the
# "us-central1.casey-prod-workloads" cluster identifier referenced in cluster_admission_rules above.
# module "gke_cluster" {
# source = "git::https://github.com/microsoftexpert/tf-mod-gcp-gke-cluster.git?ref=v1.0.0"
#...
# binary_authorization_evaluation_mode = "PROJECT_SINGLETON_POLICY_ENFORCE"
# }
output "attestor_ids" {
value = module.binary_authorization.attestor_ids
}π Before this composition can succeed,
roles/binaryauthorization.policyEditormust already be granted to the applying principal, and bothbinaryauthorization.googleapis.comandcontaineranalysis.googleapis.commust already be enabled (viatf-mod-gcp-project-services, applied first). Allow for IAM propagation lag (up to ~60 seconds) between any IAM grant and this module's apply.
| Variable | Type | Default | Notes |
|---|---|---|---|
description |
string |
null |
Policy-level descriptive comment |
global_policy_evaluation_mode |
string |
"ENABLE" |
ENABLE | DISABLE β mirrors the provider's own documented default |
deletion_policy |
string |
"PREVENT" |
DELETE | ABANDON | PREVENT β secure-by-default extension |
default_admission_rule |
object({...}) |
β (required) | No default β see Design Principles |
cluster_admission_rules |
map(object({...})) |
{} |
Keyed by {{location}}.{{clusterId}} |
admission_whitelist_patterns |
list(object({...})) |
[] |
Carve-outs, use sparingly |
timeouts |
object({create, update, delete}) |
null |
Policy-level only |
attestors |
map(object({...})) |
{} |
Keyed by attestor name β see full schema below |
Full default_admission_rule / cluster_admission_rules object schema
variable "default_admission_rule" {
type = object({
evaluation_mode = string
enforcement_mode = string
require_attestations_by = optional(set(string), [])
})
}
variable "cluster_admission_rules" {
type = map(object({
evaluation_mode = string
enforcement_mode = string
require_attestations_by = optional(set(string), [])
}))
default = {}
}Full attestors object schema
variable "attestors" {
type = map(object({
description = optional(string)
attestation_authority_note = object({
note_reference = string
public_keys = optional(list(object({
comment = optional(string)
id = optional(string)
ascii_armored_pgp_public_key = optional(string)
pkix_public_key = optional(object({
public_key_pem = optional(string)
signature_algorithm = optional(string)
}))
})), [])
})
deletion_policy = optional(string, "PREVENT")
timeouts = optional(object({
create = optional(string)
update = optional(string)
delete = optional(string)
}))
}))
default = {}
}| Output | Description | Sensitive |
|---|---|---|
id |
Policy resource id | No |
attestor_ids |
Map of attestor name β attestor resource id | No |
βΉοΈ No
self_linkoutput (neither resource exposes one), and nonameoutput for the policy (the resource has nonameattribute at all β see π§ Architecture Notes). Attestornameis always identical to theattestor_idsmap key, so it is not duplicated as a separate output.
Unlike a typical composite keystone (a VPC network, a GKE cluster β resources the API happily lets
you create many of), google_binary_authorization_policy is a project-scoped singleton: the
API auto-provisions a default-deny policy the moment Binary Authorization is enabled on a project,
and Terraform's resource "google_binary_authorization_policy" "this" block effectively takes
ownership of that one existing object rather than creating a genuinely new one. This is similar in
spirit to google_project_iam_policy being authoritative over a project's entire IAM policy β a
pattern this module suite explicitly excludes from this library.
The difference that keeps this module in scope as designed: google_project_iam_policy is excluded
because IAM bindings are an additive-by-design domain with a non-authoritative alternative already
preferred house-wide (_iam_member/_iam_binding). Binary Authorization's policy resource has no
such non-authoritative alternative β there is no "binary_authorization_policy_rule" style
additive resource to fall back on; the API itself models exactly one policy object per project,
full stop. Given that, the recommended call is to keep this module composite, keep
google_binary_authorization_policy as its singleton keystone this, and document the resulting
operational hazard prominently (below, and in π Troubleshooting) rather than attempt to route
around a constraint the API itself imposes.
- Project-wide singleton conflict hazard. Two Terraform configurations (or two instances of
this module) targeting the same project's policy conflict destructively at
apply: last-apply-wins, not a merge. This library's convention is that exactly one composition owns one project's Binary Authorization policy β an organizational discipline, not something this module's code can enforce. require_attestations_byonly resolves keys from this module's ownvar.attestors. Every entry indefault_admission_rule.require_attestations_byand everycluster_admission_rules[*].require_attestations_bymust be a key present invar.attestorsβ enforced by a cross-variablevalidation {}block (Terraform 1.9+ cross-variable validation, available given this module'srequired_version >= 1.12.0).main.tfthen resolves each key togoogle_binary_authorization_attestor.attestors[key].id, producing a genuine implicit dependency edge rather than a hand-built string. Design limitation, accepted deliberately: an attestor owned by a different Terraform configuration or a different GCP project cannot be referenced in v1 β only attestors this same module instance creates.attestation_authority_note.note_referenceis immutable. Changing it forces recreation of the attestor β verified against the live schema.global_policy_evaluation_mode/default_admission_ruleinteraction. Google's own system-image admission policy can still admit certain images even whendefault_admission_rule.evaluation_mode = "ALWAYS_DENY"β a fully closed posture requires settingglobal_policy_evaluation_mode = "DISABLE"as well, and even then, confirm current GKE system component behavior before relying on a fully closed posture in production.- No
labelson either resource β a total omission, not a per-resource split. Verified directly against the live, resolvedhashicorp/google ~> 7.0schema (terraform providers schema -json): neithergoogle_binary_authorization_policynorgoogle_binary_authorization_attestorexposes alabelsattribute. This module declares nolabelsvariable anywhere, per this module suite's escape clause for resources that genuinely do not support one. timeoutsdiffer in scope, not in shape. Both resources supportcreate/update/deleteidentically β unliketf-mod-gcp-kms-keyring's key ring (create/delete only), there is no asymmetry here to document beyond where eachtimeoutsblock lives (module-level for the policy, per-entry for each attestor).- Implicit dependency ordering.
google_binary_authorization_policy.this'srequire_attestations_bylists referencegoogle_binary_authorization_attestor.attestors[*].iddirectly β there is no explicitdepends_onbetween the two resources; Terraform's graph ordering follows the reference, and the API's own "attestor must exist before the policy references it" requirement is satisfied as a side effect. for_eachkey stability. Attestor map keys become the GCP resourcename(and the key used inattestor_ids, and the only valid form ofrequire_attestations_byentry). Renaming a key is a destroy/recreate, not an in-place rename β keep keys stable once referenced by a liverequire_attestations_byentry or a downstreamtf-mod-gcp-gke-clustercomposition.- IAM propagation delay. This module grants no IAM itself, but a composition that grants
roles/binaryauthorization.policyEditorimmediately before this module's apply may see a transient permission-denied error for up to ~60 seconds after the grant. - Deviation from SCOPE.md's pre-authoring draft, corrected during authoring. The draft assumed
a
nameoutput could substitute for the missingself_linkon the policy resource, matching the pattern used elsewhere in this library. The live, resolved schema was checked directly and shows nonameattribute exists on the policy at all β SCOPE.md's Emits table has been finalized to match the actual code (see SCOPE.md for the full correction note).
| Concern | Secure default | Opt-out (explicit) |
|---|---|---|
default_admission_rule.evaluation_mode / enforcement_mode |
No default β required, forcing an explicit caller choice, since a silently-applied ALWAYS_DENY guess could block all pod creation cluster-wide with no plan-time warning |
Caller supplies the full object explicitly (always required) |
| Accidental destroy/reset of the project-wide singleton policy (Binary-Authorization-specific extension) | deletion_policy = "PREVENT" on the policy |
Caller sets "DELETE" or "ABANDON" explicitly |
| Accidental deletion of an in-use attestor (Binary-Authorization-specific extension) | deletion_policy = "PREVENT" on every attestors entry |
Caller sets "DELETE" or "ABANDON" per entry |
require_attestations_by referencing a nonexistent/typo'd attestor |
Rejected at plan time β every entry must be a key in var.attestors, resolved via the attestor's own .id |
N/A β this module does not support raw provider-format strings here by design (see Architecture Notes limitation) |
public_keys entries with zero or two key encodings set |
Rejected at plan time β exactly one of ascii_armored_pgp_public_key/pkix_public_key required per entry |
N/A β this is a closed either/or the type system alone cannot express |
| Global system-image admission policy | global_policy_evaluation_mode = "ENABLE" (mirrors the provider's own default) |
Caller sets "DISABLE" explicitly |
| Empty call | attestors, cluster_admission_rules, and admission_whitelist_patterns all default to empty β the only required input is default_admission_rule itself |
Caller supplies entries explicitly |
cd C:\GitHubCode\newgooglecloudmodules\tf-mod-gcp-binary-authorization
terraform init -backend=false
terraform validate
terraform fmt -checkPin ?ref=v1.0.0 when consuming this module β never a branch. This library is plan-only; a human
applies from CI with valid Workload Identity Federation or ADC credentials.
terraform init -backend=false,terraform validate, andterraform fmt -checkare the entire offline proof gate for this module β all three pass cleanly as of this authoring session, against the actually-resolvedhashicorp/googleprovider version 7.39.0.validate/fmtconfirm internal type/reference consistency and canonical formatting only. Neither can catch GCP API-level rejections (quota, org policy, an invalidsignature_algorithmstring, the project-singleton conflict hazard, IAM propagation) β those surface only atapplytime, against a real project, from a human-run CI pipeline with valid credentials.- The
examples/directory exists so a consuming GitHub Actions workflow can run a realterraform planagainst a real project as part of that pipeline's own review gate; this library only guarantees the example is syntactically and structurally sound in isolation.
$ terraform output
id = "projects/casey-prod-workloads/policy"
attestor_ids = {
"build-provenance" = "projects/casey-prod-workloads/attestors/build-provenance"
}
| Symptom | Cause | Fix |
|---|---|---|
Policy changes made by one Terraform configuration are silently overwritten (or vice versa) after two separate apply runs against the same project |
Project-wide singleton conflict β google_binary_authorization_policy is a project-wide singleton (see π§ Architecture Notes); two Terraform configurations (or two instances of this module) targeting the same project's policy conflict destructively β last-apply-wins, not a merge |
Ensure exactly one composition/state file owns a given project's Binary Authorization policy; if two teams/pipelines need to manage different admission rules, coordinate through a single shared composition rather than two independent ones |
Error: require_attestations_by: every entry must be a key present in var.attestors at plan time |
A default_admission_rule/cluster_admission_rules entry references an attestor name that has no matching entry in var.attestors, or the attestor entry has a typo |
Add the missing attestors entry, or fix the typo β this module deliberately rejects a hand-built projects/*/attestors/* string here (see Architecture Notes design limitation) |
Error: every public_keys entry must set exactly one of ascii_armored_pgp_public_key or pkix_public_key |
A public_keys entry sets both, or neither, of the two key-encoding fields |
Set exactly one per entry β they are alternative encodings of the same concept, not independent fields |
terraform apply fails trying to update attestation_authority_note.note_reference on an existing attestor |
This field is documented as not updatable once set | Remove and recreate the attestor entry (a destroy/recreate is unavoidable for this field) rather than attempting an in-place change |
terraform destroy fails with a deletion_policy error on the policy or an attestor |
deletion_policy = "PREVENT" (the module default) |
Expected behavior β the guard is doing its job. Change deletion_policy to "ABANDON" or "DELETE" explicitly, re-apply, then destroy |
Images are still admitted even with default_admission_rule.evaluation_mode = "ALWAYS_DENY" |
Google's Google-maintained global admission policy for system images (global_policy_evaluation_mode = "ENABLE", the default) can still admit certain system-level images regardless of the project's own default rule |
Set global_policy_evaluation_mode = "DISABLE" if a fully closed posture is required, and independently confirm current GKE system component behavior before relying on it in production |
terraform plan shows no error but a real cluster's pods are never actually gated by a cluster_admission_rules entry |
The {{location}}.{{clusterId}} identifier string does not match a real, existing cluster β this module cannot validate that at plan/apply time (see βοΈ GCP Prerequisites) |
Confirm the identifier matches the target cluster's actual location and name exactly; enforcement only ever happens at pod-admission time against a real cluster |
A downstream tf-mod-gcp-gke-cluster composition fails to read an attestor referenced by attestor_ids right after this module's apply |
IAM propagation lag (up to ~60 seconds) after a recent roles/binaryauthorization.policyEditor grant, or the attestor genuinely was not yet created in the same apply |
Confirm the grant has propagated and that the attestor exists in this module's state before the dependent apply runs |
google_binary_authorization_policyprovider referencegoogle_binary_authorization_attestorprovider reference- Sibling modules:
tf-mod-gcp-gke-cluster,tf-mod-gcp-kms-keyring,tf-mod-gcp-project-services - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."