From 12cc248977cd5bca2a9fa0e9f65a640ac96d1e08 Mon Sep 17 00:00:00 2001 From: Wesley Dawson Date: Thu, 19 Mar 2026 22:37:18 -0700 Subject: [PATCH 1/3] feat(google_deployment_accounts): add gha_branch_assertions --- .../.terraform-docs.yml | 16 +++- google_deployment_accounts/README.md | 89 ++++++++++++++----- google_deployment_accounts/examples/gha.tf | 2 + .../examples/gha_attribute_specifiers.tf | 28 ++++++ .../examples/gha_branch.tf | 11 +++ google_deployment_accounts/locals.tf | 24 ++++- google_deployment_accounts/variables.tf | 30 +++++++ 7 files changed, 173 insertions(+), 27 deletions(-) create mode 100644 google_deployment_accounts/examples/gha_attribute_specifiers.tf create mode 100644 google_deployment_accounts/examples/gha_branch.tf diff --git a/google_deployment_accounts/.terraform-docs.yml b/google_deployment_accounts/.terraform-docs.yml index 3481b924..d4d82648 100644 --- a/google_deployment_accounts/.terraform-docs.yml +++ b/google_deployment_accounts/.terraform-docs.yml @@ -4,19 +4,27 @@ content: |- ## Examples ```hcl - {{ include "examples/circleci.tf" }} + {{ include "examples/gha.tf" }} ``` ```hcl - {{ include "examples/circleci2.tf" }} + {{ include "examples/gha_branch.tf" }} ``` ```hcl - {{ include "examples/circleci3.tf" }} + {{ include "examples/gha_attribute_specifiers.tf" }} ``` ```hcl - {{ include "examples/gha.tf" }} + {{ include "examples/circleci.tf" }} + ``` + + ```hcl + {{ include "examples/circleci2.tf" }} + ``` + + ```hcl + {{ include "examples/circleci3.tf" }} ``` {{ .Inputs }} diff --git a/google_deployment_accounts/README.md b/google_deployment_accounts/README.md index f86dddd9..0aca503f 100644 --- a/google_deployment_accounts/README.md +++ b/google_deployment_accounts/README.md @@ -4,6 +4,73 @@ Creates a Cloud IAM service account which lets CI workflows authenticate to GCP. ## Examples +```hcl +# Allow OIDC access from GitHub Actions workflows running in the stage +# environment of a specific repo +data "terraform_remote_state" "wip_project" { + backend = "gcs" + + config = { + bucket = "my-wip-project" + prefix = "wip-project/prefix" + } +} + +module "google_deployment_accounts" { + source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" + project = "my-project" + environment = "stage" + github_repository = "org/project" + wip_name = "github-actions" + wip_project_number = data.terraform_remote_state.wip_project.number +} +``` + +```hcl +# Allow OIDC access from GitHub Actions workflows triggered on the main branch +# of a specific repo +module "google_deployment_accounts_branch" { + source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" + project = "my-project" + environment = "prod" + github_repository = "mozilla/my-repo" + wip_name = "github-actions" + wip_project_number = 12345 + gha_branches = ["main"] +} +``` + +```hcl +# A more complex example using attribute specifiers directly. Allow OIDC access +# from GitHub Actions workflows running on the main branch of org/repo1 and +# org/repo2, as well as any workflow running in the "production" environment. +data "terraform_remote_state" "wip_project" { + backend = "gcs" + + config = { + bucket = "my-wip-project" + prefix = "wip-project/prefix" + } +} + +locals { + allowed_repo_refs = formatlist("attribute.repository_ref/org/%s:refs/heads/main", ["repo1", "repo2"]) + allowed_environments = formatlist("attribute.environment/%s", ["production"]) +} + +module "google_deployment_accounts" { + source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" + project = "my-project" + environment = "prod" + wip_name = "github-actions" + wip_project_number = data.terraform_remote_state.wip_project.number + gha_attribute_specifiers = setunion( + local.allowed_repo_refs, + local.allowed_environments, + ) +} +``` + ```hcl # Allow OIDC access from CircleCI jobs triggered in a specific repo data "terraform_remote_state" "wip_project" { @@ -81,26 +148,6 @@ module "google_deployment_accounts" { } ``` -```hcl -data "terraform_remote_state" "wip_project" { - backend = "gcs" - - config = { - bucket = "my-wip-project" - prefix = "wip-project/prefix" - } -} - -module "google_deployment_accounts" { - source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" - project = "my-project" - environment = "stage" - github_repository = "org/project" - wip_name = "github-actions" - wip_project_number = data.terraform_remote_state.wip_project.number -} -``` - ## Inputs | Name | Description | Type | Default | Required | @@ -111,6 +158,8 @@ module "google_deployment_accounts" { | [circleci\_context\_ids](#input\_circleci\_context\_ids) | (CircleCI only) Contexts to allow deployments from. Not recommended when using merge queues since CircleCI Contexts are only accessible to members of your organization. | `set(string)` | `[]` | no | | [display\_name](#input\_display\_name) | Display name for the service account. Defaults to "Deployment to the ENV environment". | `string` | `null` | no | | [environment](#input\_environment) | Environment e.g., stage. Not used for OIDC configuration in CircleCI. | `string` | n/a | yes | +| [gha\_attribute\_specifiers](#input\_gha\_attribute\_specifiers) | Set of attribute specifiers to allow deploys from, in the form ATTR/ATTR\_VALUE. If specified, this overrides the github\_repository variable and any other GHA-specific variables. | `set(string)` | `[]` | no | +| [gha\_branches](#input\_gha\_branches) | Branches to allow deployments from. If unspecified, allow deployment from any branch via environment-based principals. | `set(string)` | `[]` | no | | [gha\_environments](#input\_gha\_environments) | Github environments from which to deploy. If specified, this overrides the environment variable. | `list(string)` | `[]` | no | | [github\_repositories](#input\_github\_repositories) | The Github repositories running the deployment workflows in the format org/repository, will be used if github\_repository is not defined. | `list(string)` | `[]` | no | | [github\_repository](#input\_github\_repository) | The Github repository running the deployment workflows in the format org/repository. Optional for CircleCI or when github\_repositories is specified. | `string` | `null` | no | diff --git a/google_deployment_accounts/examples/gha.tf b/google_deployment_accounts/examples/gha.tf index e0fd66a0..775a0fba 100644 --- a/google_deployment_accounts/examples/gha.tf +++ b/google_deployment_accounts/examples/gha.tf @@ -1,3 +1,5 @@ +# Allow OIDC access from GitHub Actions workflows running in the stage +# environment of a specific repo data "terraform_remote_state" "wip_project" { backend = "gcs" diff --git a/google_deployment_accounts/examples/gha_attribute_specifiers.tf b/google_deployment_accounts/examples/gha_attribute_specifiers.tf new file mode 100644 index 00000000..7d33970f --- /dev/null +++ b/google_deployment_accounts/examples/gha_attribute_specifiers.tf @@ -0,0 +1,28 @@ +# A more complex example using attribute specifiers directly. Allow OIDC access +# from GitHub Actions workflows running on the main branch of org/repo1 and +# org/repo2, as well as any workflow running in the "production" environment. +data "terraform_remote_state" "wip_project" { + backend = "gcs" + + config = { + bucket = "my-wip-project" + prefix = "wip-project/prefix" + } +} + +locals { + allowed_repo_refs = formatlist("attribute.repository_ref/org/%s:refs/heads/main", ["repo1", "repo2"]) + allowed_environments = formatlist("attribute.environment/%s", ["production"]) +} + +module "google_deployment_accounts" { + source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" + project = "my-project" + environment = "prod" + wip_name = "github-actions" + wip_project_number = data.terraform_remote_state.wip_project.number + gha_attribute_specifiers = setunion( + local.allowed_repo_refs, + local.allowed_environments, + ) +} diff --git a/google_deployment_accounts/examples/gha_branch.tf b/google_deployment_accounts/examples/gha_branch.tf new file mode 100644 index 00000000..48be0759 --- /dev/null +++ b/google_deployment_accounts/examples/gha_branch.tf @@ -0,0 +1,11 @@ +# Allow OIDC access from GitHub Actions workflows triggered on the main branch +# of a specific repo +module "google_deployment_accounts_branch" { + source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" + project = "my-project" + environment = "prod" + github_repository = "mozilla/my-repo" + wip_name = "github-actions" + wip_project_number = 12345 + gha_branches = ["main"] +} diff --git a/google_deployment_accounts/locals.tf b/google_deployment_accounts/locals.tf index e009a728..e44ae07f 100644 --- a/google_deployment_accounts/locals.tf +++ b/google_deployment_accounts/locals.tf @@ -3,7 +3,25 @@ locals { environments = length(var.gha_environments) > 0 ? var.gha_environments : [var.environment] - github_deploy_members = flatten([for repo in local.github_deploy_repositories : [for env in local.environments : [ + # GHA principal resolution precedence: + # 1. gha_attribute_specifiers: full override + # 2. gha_branches: repository_ref-based principals per repo and branch + # 3. Default: environment-based principals + gha_attribute_assertions = [for attribute_specifier in var.gha_attribute_specifiers : + "principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/${attribute_specifier}" + ] + + gha_branch_assertions = flatten([for repo in local.github_deploy_repositories : [for branch in var.gha_branches : + "principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.repository_ref/${repo}:refs/heads/${branch}" + ]]) + + gha_environment_assertions = flatten([for repo in local.github_deploy_repositories : [for env in local.environments : "principal://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/subject/repo:${repo}:environment:${env}" - ]]]) -} \ No newline at end of file + ]]) + + github_deploy_members = ( + length(local.gha_attribute_assertions) > 0 ? local.gha_attribute_assertions : + length(local.gha_branch_assertions) > 0 ? local.gha_branch_assertions : + local.gha_environment_assertions + ) +} diff --git a/google_deployment_accounts/variables.tf b/google_deployment_accounts/variables.tf index 94875675..2c66d917 100644 --- a/google_deployment_accounts/variables.tf +++ b/google_deployment_accounts/variables.tf @@ -26,6 +26,36 @@ variable "gha_environments" { default = [] } +variable "gha_branches" { + description = "Branches to allow deployments from. If unspecified, allow deployment from any branch via environment-based principals." + type = set(string) + default = [] +} + +variable "gha_attribute_specifiers" { + description = "Set of attribute specifiers to allow deploys from, in the form ATTR/ATTR_VALUE. If specified, this overrides the github_repository variable and any other GHA-specific variables." + type = set(string) + default = [] + validation { + condition = alltrue( + [for attribute_specifier in var.gha_attribute_specifiers : + contains( + [ + "subject", + "attribute.actor", + "attribute.environment", + "attribute.ref", + "attribute.repository", + "attribute.repository_owner", + "attribute.repository_ref", + "attribute.workflow" + ], split("/", attribute_specifier)[0]) + ] + ) + error_message = "Attribute specifiers must contain a valid attribute prefix." + } +} + # For CircleCI, the default options are to deploy from certain repositories # (any branch) or allow deploys via a CircleCI Context. You can also limit # CircleCI to deploy from specific branches. For more complex use From 5a051d606123613313019048569a4c47920e5457 Mon Sep 17 00:00:00 2001 From: Wesley Dawson Date: Tue, 9 Jun 2026 15:16:21 -0700 Subject: [PATCH 2/3] chore: update CODEOWNERS --- CODEOWNERS | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1dd880c7..48670026 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,8 +1,8 @@ -google_deployment_accounts/ @jasonthomas @jbuck @bkochendorfer @smarnach @whd @mozilla/gcp-wg -google_fastly_waf/ @jasonthomas @jbuck @bkochendorfer @whd @tibap @mozilla/gcp-wg -google_gke/ @jasonthomas @ahoneiser @mikaeld @jbuck @bkochendorfer @mozilla/gcp-wg -google_gke_tenant/ @jasonthomas @jbuck @bkochendorfer @mozilla/gcp-wg -google_workgroup/ @jasonthomas @jbuck @bkochendorfer @mozilla/gcp-wg -google_workload_identity/ @jasonthomas @jbuck @bkochendorfer @mozilla/gcp-wg -pagerduty_service/ @jasonthomas @jbuck @bkochendorfer @whd @bwells-moz @cass-moz @mozilla/gcp-wg -pagerduty_team/ @jasonthomas @jbuck @bkochendorfer @whd @bwells-moz @cass-moz @mozilla/gcp-wg +google_deployment_accounts/ @mozilla/gcp-wg +google_fastly_waf/ @tibap @mozilla/gcp-wg +google_gke/ @ahoneiser @mikaeld @mozilla/gcp-wg +google_gke_tenant/ @mozilla/gcp-wg +google_workgroup/ @mozilla/gcp-wg +google_workload_identity/ @mozilla/gcp-wg +pagerduty_service/ @bwells-moz @cass-moz @mozilla/gcp-wg +pagerduty_team/ @bwells-moz @cass-moz @mozilla/gcp-wg From ec5b6df9b6d2cf43a629768646c90aa605c67760 Mon Sep 17 00:00:00 2001 From: Wesley Dawson Date: Tue, 9 Jun 2026 16:02:52 -0700 Subject: [PATCH 3/3] chore(google_deployment_accounts): clarify docs --- google_deployment_accounts/README.md | 7 ++++--- google_deployment_accounts/examples/gha_branch.tf | 3 ++- google_deployment_accounts/locals.tf | 4 +++- google_deployment_accounts/main.tf | 6 ++++-- google_deployment_accounts/variables.tf | 8 ++++++-- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/google_deployment_accounts/README.md b/google_deployment_accounts/README.md index 0aca503f..cf86d60b 100644 --- a/google_deployment_accounts/README.md +++ b/google_deployment_accounts/README.md @@ -28,7 +28,8 @@ module "google_deployment_accounts" { ```hcl # Allow OIDC access from GitHub Actions workflows triggered on the main branch -# of a specific repo +# of a specific repo. Branch-based access is typically secured by strong +# primary branch protection settings, not Github environment approval rules. module "google_deployment_accounts_branch" { source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" project = "my-project" @@ -159,8 +160,8 @@ module "google_deployment_accounts" { | [display\_name](#input\_display\_name) | Display name for the service account. Defaults to "Deployment to the ENV environment". | `string` | `null` | no | | [environment](#input\_environment) | Environment e.g., stage. Not used for OIDC configuration in CircleCI. | `string` | n/a | yes | | [gha\_attribute\_specifiers](#input\_gha\_attribute\_specifiers) | Set of attribute specifiers to allow deploys from, in the form ATTR/ATTR\_VALUE. If specified, this overrides the github\_repository variable and any other GHA-specific variables. | `set(string)` | `[]` | no | -| [gha\_branches](#input\_gha\_branches) | Branches to allow deployments from. If unspecified, allow deployment from any branch via environment-based principals. | `set(string)` | `[]` | no | -| [gha\_environments](#input\_gha\_environments) | Github environments from which to deploy. If specified, this overrides the environment variable. | `list(string)` | `[]` | no | +| [gha\_branches](#input\_gha\_branches) | Branches to allow deployments from. If specified, takes precedence over gha\_environments and the environment variable, and is overridden by gha\_attribute\_specifiers. If unspecified, allow deployment from any branch via environment-based principals. | `set(string)` | `[]` | no | +| [gha\_environments](#input\_gha\_environments) | Github environments from which to deploy. If specified, this overrides the environment variable. Overridden by gha\_branches and gha\_attribute\_specifiers when either is set. | `list(string)` | `[]` | no | | [github\_repositories](#input\_github\_repositories) | The Github repositories running the deployment workflows in the format org/repository, will be used if github\_repository is not defined. | `list(string)` | `[]` | no | | [github\_repository](#input\_github\_repository) | The Github repository running the deployment workflows in the format org/repository. Optional for CircleCI or when github\_repositories is specified. | `string` | `null` | no | | [project](#input\_project) | n/a | `string` | `null` | no | diff --git a/google_deployment_accounts/examples/gha_branch.tf b/google_deployment_accounts/examples/gha_branch.tf index 48be0759..6c4accf0 100644 --- a/google_deployment_accounts/examples/gha_branch.tf +++ b/google_deployment_accounts/examples/gha_branch.tf @@ -1,5 +1,6 @@ # Allow OIDC access from GitHub Actions workflows triggered on the main branch -# of a specific repo +# of a specific repo. Branch-based access is typically secured by strong +# primary branch protection settings, not Github environment approval rules. module "google_deployment_accounts_branch" { source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" project = "my-project" diff --git a/google_deployment_accounts/locals.tf b/google_deployment_accounts/locals.tf index e44ae07f..66235900 100644 --- a/google_deployment_accounts/locals.tf +++ b/google_deployment_accounts/locals.tf @@ -7,8 +7,10 @@ locals { # 1. gha_attribute_specifiers: full override # 2. gha_branches: repository_ref-based principals per repo and branch # 3. Default: environment-based principals + # A subject specifier maps to a single identity (principal://); all other + # attributes map to identity sets (principalSet://). gha_attribute_assertions = [for attribute_specifier in var.gha_attribute_specifiers : - "principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/${attribute_specifier}" + "${split("/", attribute_specifier)[0] == "subject" ? "principal" : "principalSet"}://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/${attribute_specifier}" ] gha_branch_assertions = flatten([for repo in local.github_deploy_repositories : [for branch in var.gha_branches : diff --git a/google_deployment_accounts/main.tf b/google_deployment_accounts/main.tf index 96dc5f74..7528e604 100644 --- a/google_deployment_accounts/main.tf +++ b/google_deployment_accounts/main.tf @@ -23,9 +23,11 @@ resource "google_service_account_iam_binding" "github-actions-access" { locals { circleci = var.wip_name == "circleci" - # explicit attributes replace all other kinds of assertions + # explicit attributes replace all other kinds of assertions. A subject + # specifier maps to a single identity (principal://); all other attributes + # map to identity sets (principalSet://). circleci_attribute_assertions = local.circleci ? [for attribute_specifier in var.circleci_attribute_specifiers : - "principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/${attribute_specifier}" + "${split("/", attribute_specifier)[0] == "subject" ? "principal" : "principalSet"}://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/${attribute_specifier}" ] : [] # single repo, all branches circleci_vcs_origin_assertions = local.circleci && var.github_repository != null && length(var.circleci_branches) == 0 ? ["principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.vcs_origin/github.com/${var.github_repository}", diff --git a/google_deployment_accounts/variables.tf b/google_deployment_accounts/variables.tf index 2c66d917..c5162d77 100644 --- a/google_deployment_accounts/variables.tf +++ b/google_deployment_accounts/variables.tf @@ -21,13 +21,17 @@ variable "environment" { # requires manual approval for deploying to production, and that particular # Github environment should be included in the gha_environments list. variable "gha_environments" { - description = "Github environments from which to deploy. If specified, this overrides the environment variable." + description = "Github environments from which to deploy. If specified, this overrides the environment variable. Overridden by gha_branches and gha_attribute_specifiers when either is set." type = list(string) default = [] } +# Note that branch-based principals authorize any workflow running on the given +# branch and rely entirely on branch protection rules for security. This is +# acceptable because branch protection rules on the primary branch for which +# this setting is likely to be used are typically strong. variable "gha_branches" { - description = "Branches to allow deployments from. If unspecified, allow deployment from any branch via environment-based principals." + description = "Branches to allow deployments from. If specified, takes precedence over gha_environments and the environment variable, and is overridden by gha_attribute_specifiers. If unspecified, allow deployment from any branch via environment-based principals." type = set(string) default = [] }