Skip to content

microsoftexpert/tf-mod-gcp-binary-authorization

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

3 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Google Cloud Binary Authorization Policy Terraform Module

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). Targets hashicorp/google ~> 7.0, Terraform >= 1.12.0.

Terraform Provider Version Type Resources Posture


🧩 Overview

  • πŸ›‘οΈ 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) via for_each over var.attestors, keyed by attestor name.
  • 🚫 default_admission_rule is required with no default value β€” a deliberate deviation from the house "empty call produces the safe resource" rule (see 🧱 Design Principles): an opinionated ALWAYS_DENY default could silently block all pod creation, including system pods, with no plan-time warning.
  • πŸ”— require_attestations_by fields accept only keys into this module's own var.attestors map β€” never a hand-built projects/*/attestors/* string β€” resolved to google_binary_authorization_attestor.attestors[key].id so 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 per public_keys entry.
  • 🧱 deletion_policy defaults 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 a for_each key-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.


❀️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


πŸ—ΊοΈ Where this fits

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;
Loading

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.


🧬 What this builds

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;
Loading

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

βœ… Provider / Versions

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_reference is 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_mode interacts with default_admission_rule. Google's own system-image admission policy can still admit certain images even when the project's default_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 labels or self_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 name attribute either β€” only id. 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_keys entries are a one-of, not two independent optional fields. Exactly one of ascii_armored_pgp_public_key or pkix_public_key must be set per entry β€” this module's own validation {} block catches both-set and neither-set at plan time.
  • require_attestations_by requires the attestor to already exist. The live schema documents: "Each attestor must exist before a policy can reference it." This module resolves every require_attestations_by entry through the attestor resource's own .id attribute specifically to create that ordering guarantee via Terraform's implicit dependency graph.

πŸ”‘ Required IAM Roles

  • 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 via gcloud before relying on it; this module only asserts the role given above.)

(Sourced directly from SCOPE.md β€” not re-derived.)


☁️ GCP Prerequisites

  • binaryauthorization.googleapis.com enabled on the target project.
  • containeranalysis.googleapis.com enabled 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_rules must 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 fail plan/apply, only real pod creation against that cluster later.

(Sourced directly from SCOPE.md β€” not re-derived.)


πŸ“ Module Structure

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

βš™οΈ Quick Start

# 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_rule has 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 a REQUIRE_ATTESTATION posture with attestors wired in.


πŸ”Œ Cross-Module Contract

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_link row, and no name row for the policy β€” see 🧠 Architecture Notes for why both are genuinely absent from the live schema, not merely omitted from this table.


πŸ“š Example Library

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"
  }
}

ℹ️ attestors defaults 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_rules override applies. Confirm this is intentional before applying: it blocks all workloads, including ones GKE itself may need, subject to the global_policy_evaluation_mode interaction 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 own attestors map, never a hand-built projects/*/attestors/* string β€” main.tf resolves it to google_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_rules map keys use Binary Authorization's own {{location}}.{{clusterId}} identifier convention β€” sourced from tf-mod-gcp-gke-cluster by naming convention only, not a Terraform resource reference. The referenced cluster does not need to exist at plan/apply time.

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" }
        ]
      }
    }
  }
}

⚠️ Every admission_whitelist_patterns entry 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_algorithm is 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 at apply, not caught by this library's plan-only validate/fmt gate.

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_by must independently attest to the image β€” a logical AND, not an OR. All attestors referenced must already be entries in this same attestors map (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 new REQUIRE_ATTESTATION rule before flipping to ENFORCED_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_policy defaults 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 live require_attestations_by rule.

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_policy defaults 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.policyEditor must already be granted to the applying principal, and both binaryauthorization.googleapis.com and containeranalysis.googleapis.com must already be enabled (via tf-mod-gcp-project-services, applied first). Allow for IAM propagation lag (up to ~60 seconds) between any IAM grant and this module's apply.


πŸ“₯ Inputs

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 = {}
}

🧾 Outputs

Output Description Sensitive
id Policy resource id No
attestor_ids Map of attestor name β†’ attestor resource id No

ℹ️ No self_link output (neither resource exposes one), and no name output for the policy (the resource has no name attribute at all β€” see 🧠 Architecture Notes). Attestor name is always identical to the attestor_ids map key, so it is not duplicated as a separate output.


🧠 Architecture Notes

Classification nuance (reproduced in full from SCOPE.md)

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.

Other notes

  • 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_by only resolves keys from this module's own var.attestors. Every entry in default_admission_rule.require_attestations_by and every cluster_admission_rules[*].require_attestations_by must be a key present in var.attestors β€” enforced by a cross-variable validation {} block (Terraform 1.9+ cross-variable validation, available given this module's required_version >= 1.12.0). main.tf then resolves each key to google_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_reference is immutable. Changing it forces recreation of the attestor β€” verified against the live schema.
  • global_policy_evaluation_mode / default_admission_rule interaction. Google's own system-image admission policy can still admit certain images even when default_admission_rule.evaluation_mode = "ALWAYS_DENY" β€” a fully closed posture requires setting global_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 labels on either resource β€” a total omission, not a per-resource split. Verified directly against the live, resolved hashicorp/google ~> 7.0 schema (terraform providers schema -json): neither google_binary_authorization_policy nor google_binary_authorization_attestor exposes a labels attribute. This module declares no labels variable anywhere, per this module suite's escape clause for resources that genuinely do not support one.
  • timeouts differ in scope, not in shape. Both resources support create/update/delete identically β€” unlike tf-mod-gcp-kms-keyring's key ring (create/delete only), there is no asymmetry here to document beyond where each timeouts block lives (module-level for the policy, per-entry for each attestor).
  • Implicit dependency ordering. google_binary_authorization_policy.this's require_attestations_by lists reference google_binary_authorization_attestor.attestors[*].id directly β€” there is no explicit depends_on between 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_each key stability. Attestor map keys become the GCP resource name (and the key used in attestor_ids, and the only valid form of require_attestations_by entry). Renaming a key is a destroy/recreate, not an in-place rename β€” keep keys stable once referenced by a live require_attestations_by entry or a downstream tf-mod-gcp-gke-cluster composition.
  • IAM propagation delay. This module grants no IAM itself, but a composition that grants roles/binaryauthorization.policyEditor immediately 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 name output could substitute for the missing self_link on the policy resource, matching the pattern used elsewhere in this library. The live, resolved schema was checked directly and shows no name attribute 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).

🧱 Design Principles

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

πŸš€ Runbook

cd C:\GitHubCode\newgooglecloudmodules\tf-mod-gcp-binary-authorization
terraform init -backend=false
terraform validate
terraform fmt -check

Pin ?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.


πŸ§ͺ Testing

  • terraform init -backend=false, terraform validate, and terraform fmt -check are the entire offline proof gate for this module β€” all three pass cleanly as of this authoring session, against the actually-resolved hashicorp/google provider version 7.39.0.
  • validate/fmt confirm internal type/reference consistency and canonical formatting only. Neither can catch GCP API-level rejections (quota, org policy, an invalid signature_algorithm string, the project-singleton conflict hazard, IAM propagation) β€” those surface only at apply time, 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 real terraform plan against 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.

πŸ’¬ Example Output

$ terraform output

id = "projects/casey-prod-workloads/policy"
attestor_ids = {
 "build-provenance" = "projects/casey-prod-workloads/attestors/build-provenance"
}

πŸ” Troubleshooting

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

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."

Packages

 
 
 

Contributors

Languages