From 30b7e2de643058f0f72fcb054b6f5d2d02c3fd55 Mon Sep 17 00:00:00 2001 From: whd Date: Tue, 27 Feb 2024 17:52:19 +0000 Subject: [PATCH] Circleci OIDC (#148) * feat(google_deployment_accounts): support circleci oidc * chore(google_deployment_accounts): add a fixme for 2024 * feat(google_deployment_accounts): add opinionated circleci interface * feat(google_deployment_accounts): allow overriding display name * chore(google_deployment_accounts): update docs * chore(google_deployment_accounts): add examples * chore(google_deployment_accounts): fix up docs and interface * chore(google_deployment_accounts): fixups post rebase --- google_deployment_accounts/README.md | 21 +++--- .../examples/circleci.tf | 18 +++++ .../examples/circleci2.tf | 20 ++++++ .../examples/circleci3.tf | 30 +++++++++ google_deployment_accounts/examples/gha.tf | 17 +++++ google_deployment_accounts/main.tf | 47 +++++++++++++- google_deployment_accounts/variables.tf | 65 +++++++++++++++++-- 7 files changed, 200 insertions(+), 18 deletions(-) create mode 100644 google_deployment_accounts/examples/circleci.tf create mode 100644 google_deployment_accounts/examples/circleci2.tf create mode 100644 google_deployment_accounts/examples/circleci3.tf create mode 100644 google_deployment_accounts/examples/gha.tf diff --git a/google_deployment_accounts/README.md b/google_deployment_accounts/README.md index 1e6044ca..0c8452f7 100644 --- a/google_deployment_accounts/README.md +++ b/google_deployment_accounts/README.md @@ -1,5 +1,5 @@ -# Terraform Module: Service Accounts for deployment from GitHub Actions -Creates a Cloud IAM service accounts which let GitHub Actions workflows authenticate to GKE. +# Terraform Module: Service Accounts for deployment from GitHub Actions and CircleCI +Creates a Cloud IAM service account which lets CI workflows authenticate to GCP. ## Requirements @@ -23,20 +23,25 @@ No modules. | Name | Type | |------|------| | [google_service_account.account](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | +| [google_service_account_iam_binding.circleci-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource | | [google_service_account_iam_binding.github-actions-access](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_iam_binding) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [account\_id](#input\_account\_id) | Name of the service account. Defaults to deploy- | `string` | `null` | no | -| [environment](#input\_environment) | Environment e.g., stage. | `string` | n/a | yes | +| [account\_id](#input\_account\_id) | Name of the service account. Defaults to deploy-ENV. | `string` | `null` | no | +| [circleci\_attribute\_specifiers](#input\_circleci\_attribute\_specifiers) | (CircleCI only) 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 CircleCI-specific variables. | `set(string)` | `[]` | no | +| [circleci\_branches](#input\_circleci\_branches) | (CircleCI only) Branches to allow deployments from. If unspecified, allow deployment from all branches. | `set(string)` | `[]` | no | +| [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\_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, overwriting the github\_deployment variable | `list(string)` | `[]` | no | -| [github\_repository](#input\_github\_repository) | The Github repository running the deployment workflows in the format org/repository | `string` | n/a | yes | +| [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 | -| [wip\_name](#input\_wip\_name) | The name of the workload identity provider | `string` | `"github-actions"` | no | -| [wip\_project\_number](#input\_wip\_project\_number) | The project number of the project the workload identity provider lives in | `number` | n/a | yes | +| [wip\_name](#input\_wip\_name) | The name of the workload identity provider. This value implicitly controls whether to provision access to github-actions or circleci. | `string` | `"github-actions"` | no | +| [wip\_project\_number](#input\_wip\_project\_number) | The project number of the project the workload identity provider lives in. | `number` | n/a | yes | ## Outputs diff --git a/google_deployment_accounts/examples/circleci.tf b/google_deployment_accounts/examples/circleci.tf new file mode 100644 index 00000000..476d82ff --- /dev/null +++ b/google_deployment_accounts/examples/circleci.tf @@ -0,0 +1,18 @@ +# Allow OIDC access from CircleCI jobs triggered in 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 = "circleci" + wip_project_number = data.terraform_remote_state.wip_project.number +} diff --git a/google_deployment_accounts/examples/circleci2.tf b/google_deployment_accounts/examples/circleci2.tf new file mode 100644 index 00000000..e32f5da5 --- /dev/null +++ b/google_deployment_accounts/examples/circleci2.tf @@ -0,0 +1,20 @@ +# Allow OIDC access from CircleCI jobs triggered on the main branch only 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 = "circleci" + wip_project_number = data.terraform_remote_state.wip_project.number + circleci_branches = ["main"] +} diff --git a/google_deployment_accounts/examples/circleci3.tf b/google_deployment_accounts/examples/circleci3.tf new file mode 100644 index 00000000..35b5ec93 --- /dev/null +++ b/google_deployment_accounts/examples/circleci3.tf @@ -0,0 +1,30 @@ +# A more complex example using attribute specifiers directly. Allow OIDC access +# from CircleCI jobs triggered on the main branch of org/repo1 and org/repo2, +# as well as any job using the fake-context context +data "terraform_remote_state" "wip_project" { + backend = "gcs" + + config = { + bucket = "my-wip-project" + prefix = "wip-project/prefix" + } +} + +locals { + allowed_repos = formatlist("attribute.vcs/github.com/org/%s:/refs/heads/main", ["repo1", "repo2"]) + allowed_contexts = formatlist("attribute.context_id/%s", + one(values({ "fake-context" = "6e1515f7-40f0-4063-a74a-d77d22ee9f7e" } + ))) +} + +module "google_deployment_accounts" { + source = "github.com/mozilla/terraform-modules//google_deployment_accounts?ref=main" + project = "my-project" + environment = "prod" + wip_name = "circleci" + wip_project_number = data.terraform_remote_state.wip_project.number + circleci_attribute_specifiers = setunion( + local.allowed_repos, + local.allowed_contexts, + ) +} diff --git a/google_deployment_accounts/examples/gha.tf b/google_deployment_accounts/examples/gha.tf new file mode 100644 index 00000000..e0fd66a0 --- /dev/null +++ b/google_deployment_accounts/examples/gha.tf @@ -0,0 +1,17 @@ +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 +} diff --git a/google_deployment_accounts/main.tf b/google_deployment_accounts/main.tf index 369ea37e..96dc5f74 100644 --- a/google_deployment_accounts/main.tf +++ b/google_deployment_accounts/main.tf @@ -1,16 +1,57 @@ /** - * # Terraform Module: Service Accounts for deployment from GitHub Actions - * Creates a Cloud IAM service accounts which let GitHub Actions workflows authenticate to GKE. + * # Terraform Module: Service Accounts for deployment from GitHub Actions and CircleCI + * Creates a Cloud IAM service account which lets CI workflows authenticate to GCP. */ +locals { + gha_count = var.wip_name == "github-actions" ? 1 : 0 + circleci_count = var.wip_name == "circleci" ? 1 : 0 +} + resource "google_service_account" "account" { account_id = coalesce(var.account_id, "deploy-${var.environment}") - display_name = "Deployment to the ${var.environment} environment" + display_name = coalesce(var.display_name, "Deployment to the ${var.environment} environment") project = var.project } resource "google_service_account_iam_binding" "github-actions-access" { + count = local.gha_count service_account_id = google_service_account.account.name role = "roles/iam.workloadIdentityUser" members = local.github_deploy_members } + +locals { + circleci = var.wip_name == "circleci" + # explicit attributes replace all other kinds of assertions + 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}" + ] : [] + # 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}", + ] : [] + # single repo, specific branches + circleci_vcs_assertions = var.wip_name == "circleci" && var.github_repository != null && length(var.circleci_branches) > 0 ? [ + for branch in var.circleci_branches : + "principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.vcs/github.com/${var.github_repository}:refs/heads/${branch}" + ] : [] + # specific CircleCI Context + circleci_context_id_assertions = local.circleci && length(var.circleci_context_ids) > 0 ? [ + for context in var.circleci_context_ids : + "principalSet://iam.googleapis.com/projects/${var.wip_project_number}/locations/global/workloadIdentityPools/${var.wip_name}/attribute.context_id/${context}" + ] : [] +} + +resource "google_service_account_iam_binding" "circleci-access" { + count = local.circleci_count + service_account_id = google_service_account.account.name + role = "roles/iam.workloadIdentityUser" + # test value generated via GUI, assertions should look something like: + # "principalSet://iam.googleapis.com/projects/12141114016/locations/global/workloadIdentityPools/circleci-2/attribute.aud/c3874144-7d38-44e8-8b38-f6b8778a4eb0" + members = length(local.circleci_attribute_assertions) > 0 ? local.circleci_attribute_assertions : setunion( + local.circleci_attribute_assertions, + local.circleci_vcs_origin_assertions, + local.circleci_vcs_assertions, + local.circleci_context_id_assertions, + ) +} diff --git a/google_deployment_accounts/variables.tf b/google_deployment_accounts/variables.tf index c9eede27..94875675 100644 --- a/google_deployment_accounts/variables.tf +++ b/google_deployment_accounts/variables.tf @@ -1,11 +1,17 @@ variable "account_id" { type = string - description = "Name of the service account. Defaults to deploy-" + description = "Name of the service account. Defaults to deploy-ENV." + default = null +} + +variable "display_name" { + type = string + description = "Display name for the service account. Defaults to \"Deployment to the ENV environment\"." default = null } variable "environment" { - description = "Environment e.g., stage." + description = "Environment e.g., stage. Not used for OIDC configuration in CircleCI." type = string } @@ -20,6 +26,47 @@ variable "gha_environments" { default = [] } +# 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 +# cases (such as CI access to a service account across multiple repositories) +# you can specify those attribute specifiers explicitly instead of the +# convenience variables. +variable "circleci_branches" { + description = "(CircleCI only) Branches to allow deployments from. If unspecified, allow deployment from all branches." + type = set(string) + default = [] +} + +variable "circleci_context_ids" { + description = "(CircleCI only) Contexts to allow deployments from. Not recommended when using merge queues since CircleCI Contexts are only accessible to members of your organization." + type = set(string) + default = [] +} + +variable "circleci_attribute_specifiers" { + description = "(CircleCI only) 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 CircleCI-specific variables." + type = set(string) + default = [] + validation { + condition = alltrue( + [for attribute_specifier in var.circleci_attribute_specifiers : + contains( + [ + "subject", + "attribute.aud", + "attribute.vcs", + "attribute.project", + "attribute.vcs_origin", + "attribute.vcs_ref", + "attribute.context_id" + ], split("/", attribute_specifier)[0]) + ] + ) + error_message = "Attribute specifiers must contain a valid attribute prefix." + } +} + variable "project" { type = string default = null @@ -27,23 +74,27 @@ variable "project" { variable "wip_project_number" { type = number - description = "The project number of the project the workload identity provider lives in" + description = "The project number of the project the workload identity provider lives in." } variable "wip_name" { type = string - description = "The name of the workload identity provider" + description = "The name of the workload identity provider. This value implicitly controls whether to provision access to github-actions or circleci." default = "github-actions" + validation { + condition = contains(["github-actions", "circleci"], var.wip_name) + error_message = "The value of wip_name must be either github-actions or circleci." + } } variable "github_repository" { type = string - description = "The Github repository running the deployment workflows in the format org/repository" + description = "The Github repository running the deployment workflows in the format org/repository. Optional for CircleCI or when github_repositories is specified." default = null } variable "github_repositories" { type = list(string) - description = "The Github repositories running the deployment workflows in the format org/repository, will be used if github_repository is not defined" + description = "The Github repositories running the deployment workflows in the format org/repository, will be used if github_repository is not defined." default = [] -} \ No newline at end of file +}