Add support for Workload Identity to project module and project factory (#3531)

* module-level support

* fast stage 0

* fix inventory, add outputs/tfvars

* wip

* project factory

* pf outputs

* iam templates will be added where ci/cd configs are managed

* fix merge conflicts
This commit is contained in:
Ludovico Magnocavallo
2025-11-17 08:31:21 +01:00
committed by GitHub
parent 87ed19bc47
commit 897c6ef8c3
29 changed files with 1487 additions and 143 deletions

View File

@@ -40,6 +40,7 @@ This module implements the creation and management of one GCP project including
- [Managing project related configuration without creating it](#managing-project-related-configuration-without-creating-it)
- [Observability](#observability)
- [Observability factory](#observability-factory)
- [Workload Identity Federation](#workload-identity-federation)
- [Files](#files)
- [Variables](#variables)
- [Outputs](#outputs)
@@ -1975,6 +1976,90 @@ alerts:
foo: bar
```
## Workload Identity Federation
Workload Identity federation pools and providers can be created via the `workload_identity_pools` variable.
Auto-population of provider attributes and issuer are supported for OIDC providers via the `provider_template` attribute. Currently `github`, `gitlab`, `okta` and `terraform` provider types are supported.
```hcl
module "project" {
source = "./fabric/modules/project"
name = "project"
billing_account = var.billing_account_id
parent = var.folder_id
prefix = var.prefix
workload_identity_pools = {
test-oidc = {
display_name = "Test pool (OIDC)."
providers = {
github-test = {
attribute_condition = "attribute.repository_owner=='my_org'"
display_name = "GitHub provider (from template)."
identity_provider = {
oidc = {
template = "github"
}
}
}
gitlab-test = {
display_name = "GitLab provider (explicit attributes)."
attribute_condition = "attribute.namespace_path=='my_org'"
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.sub" = "assertion.sub"
"attribute.environment" = "assertion.environment"
"attribute.namespace_id" = "assertion.namespace_id"
"attribute.namespace_path" = "assertion.namespace_path"
"attribute.project_id" = "assertion.project_id"
"attribute.project_path" = "assertion.project_path"
"attribute.repository" = "assertion.project_path"
"attribute.ref" = "assertion.ref"
"attribute.ref_type" = "assertion.ref_type"
}
identity_provider = {
oidc = {
issuer_uri = "https://gitlab.com"
}
}
}
}
}
test-non-oidc = {
display_name = "Test pool (non-OIDC)."
providers = {
aws-test = {
attribute_condition = "attribute.aws_role==\"arn:aws:sts::999999999999:assumed-role/stack-eu-central-1-lambdaRole\""
attribute_mapping = {
"google.subject" = "assertion.arn"
"attribute.aws_account" = "assertion.account"
"attribute.environment" = "assertion.arn.contains(\":instance-profile/Production\") ? \"prod\" : \"test\""
}
identity_provider = {
aws = {
account_id = "999999999999"
}
}
}
saml-test = {
attribute_mapping = {
"google.subject" = "assertion.arn"
"attribute.aws_account" = "assertion.account"
"attribute.environment" = "assertion.arn.contains(\":instance-profile/Production\") ? \"prod\" : \"test\""
}
identity_provider = {
saml = {
idp_metadata_xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>..."
}
}
}
}
}
}
}
# tftest modules=1 resources=7 inventory=wif.yaml
```
<!-- TFDOC OPTS files:1 -->
<!-- BEGIN TFDOC -->
## Files
@@ -1985,6 +2070,8 @@ alerts:
| [bigquery-reservation.tf](./bigquery-reservation.tf) | None | <code>google_bigquery_reservation</code> · <code>google_bigquery_reservation_assignment</code> |
| [cmek.tf](./cmek.tf) | Service Agent IAM Bindings for CMEK | <code>google_kms_crypto_key_iam_member</code> |
| [iam.tf](./iam.tf) | IAM bindings. | <code>google_project_iam_binding</code> · <code>google_project_iam_custom_role</code> · <code>google_project_iam_member</code> |
| [identity-providers-defs.tf](./identity-providers-defs.tf) | Workload Identity provider definitions. | |
| [identity-providers.tf](./identity-providers.tf) | None | <code>google_iam_workload_identity_pool</code> · <code>google_iam_workload_identity_pool_provider</code> |
| [logging-metrics.tf](./logging-metrics.tf) | None | <code>google_logging_metric</code> |
| [logging.tf](./logging.tf) | Log sinks and supporting resources. | <code>google_bigquery_dataset_iam_member</code> · <code>google_logging_log_scope</code> · <code>google_logging_project_exclusion</code> · <code>google_logging_project_sink</code> · <code>google_project_iam_audit_config</code> · <code>google_project_iam_member</code> · <code>google_pubsub_topic_iam_member</code> · <code>google_storage_bucket_iam_member</code> |
| [main.tf](./main.tf) | Module-level locals and resources. | <code>google_compute_project_default_network_tier</code> · <code>google_compute_project_metadata_item</code> · <code>google_essential_contacts_contact</code> · <code>google_kms_key_handle</code> · <code>google_monitoring_monitored_project</code> · <code>google_project</code> · <code>google_project_service</code> · <code>google_resource_manager_lien</code> |
@@ -1998,6 +2085,7 @@ alerts:
| [shared-vpc.tf](./shared-vpc.tf) | Shared VPC project-level configuration. | <code>google_compute_shared_vpc_host_project</code> · <code>google_compute_shared_vpc_service_project</code> · <code>google_compute_subnetwork_iam_member</code> · <code>google_project_iam_member</code> |
| [tags.tf](./tags.tf) | Manages GCP Secure Tags, keys, values, and IAM. | <code>google_tags_tag_binding</code> · <code>google_tags_tag_key</code> · <code>google_tags_tag_key_iam_binding</code> · <code>google_tags_tag_key_iam_member</code> · <code>google_tags_tag_value</code> · <code>google_tags_tag_value_iam_binding</code> · <code>google_tags_tag_value_iam_member</code> |
| [variables-iam.tf](./variables-iam.tf) | None | |
| [variables-identity-providers.tf](./variables-identity-providers.tf) | None | |
| [variables-observability.tf](./variables-observability.tf) | None | |
| [variables-pam.tf](./variables-pam.tf) | None | |
| [variables-quotas.tf](./variables-quotas.tf) | None | |
@@ -2060,6 +2148,7 @@ alerts:
| [tags_config](variables-tags.tf#L154) | Fine-grained control on tag resource and IAM creation. | <code title="object&#40;&#123;&#10; force_context_ids &#61; optional&#40;bool, false&#41;&#10; ignore_iam &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>&#123;&#125;</code> |
| [universe](variables.tf#L371) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | <code title="object&#40;&#123;&#10; prefix &#61; string&#10; forced_jit_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_services &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; unavailable_service_identities &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [vpc_sc](variables.tf#L382) | VPC-SC configuration for the project, use when `ignore_changes` for resources is set in the VPC-SC module. | <code title="object&#40;&#123;&#10; perimeter_name &#61; string&#10; is_dry_run &#61; optional&#40;bool, false&#41;&#10;&#125;&#41;">object&#40;&#123;&#8230;&#125;&#41;</code> | | <code>null</code> |
| [workload_identity_pools](variables-identity-providers.tf#L17) | Workload Identity Federation pools and providers. | <code title="map&#40;object&#40;&#123;&#10; display_name &#61; optional&#40;string&#41;&#10; description &#61; optional&#40;string&#41;&#10; disabled &#61; optional&#40;bool&#41;&#10; providers &#61; optional&#40;map&#40;object&#40;&#123;&#10; description &#61; optional&#40;string&#41;&#10; display_name &#61; optional&#40;string&#41;&#10; attribute_condition &#61; optional&#40;string&#41;&#10; attribute_mapping &#61; optional&#40;map&#40;string&#41;, &#123;&#125;&#41;&#10; disabled &#61; optional&#40;bool, false&#41;&#10; identity_provider &#61; object&#40;&#123;&#10; aws &#61; optional&#40;object&#40;&#123;&#10; account_id &#61; string&#10; &#125;&#41;&#41;&#10; oidc &#61; optional&#40;object&#40;&#123;&#10; allowed_audiences &#61; optional&#40;list&#40;string&#41;, &#91;&#93;&#41;&#10; issuer_uri &#61; optional&#40;string&#41;&#10; jwks_json &#61; optional&#40;string&#41;&#10; template &#61; optional&#40;string&#41;&#10; &#125;&#41;&#41;&#10; saml &#61; optional&#40;object&#40;&#123;&#10; idp_metadata_xml &#61; string&#10; &#125;&#41;&#41;&#10; &#125;&#41;&#10; &#125;&#41;&#41;, &#123;&#125;&#41;&#10;&#125;&#41;&#41;">map&#40;object&#40;&#123;&#8230;&#125;&#41;&#41;</code> | | <code>&#123;&#125;</code> |
## Outputs
@@ -2088,6 +2177,8 @@ alerts:
| [sink_writer_identities](outputs.tf#L197) | Writer identities created for each sink. | |
| [tag_keys](outputs.tf#L204) | Tag key resources. | |
| [tag_values](outputs.tf#L213) | Tag value resources. | |
| [workload_identity_provider_ids](outputs.tf#L221) | Workload identity provider attributes. | |
| [workload_identity_providers](outputs.tf#L229) | Workload identity provider attributes. | |
## Fixtures

View File

@@ -0,0 +1,81 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# tfdoc:file:description Workload Identity provider definitions.
locals {
wif_defs = {
# https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect
github = {
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.sub" = "assertion.sub"
"attribute.actor" = "assertion.actor"
"attribute.repository" = "assertion.repository"
"attribute.repository_owner" = "assertion.repository_owner"
"attribute.ref" = "assertion.ref"
"attribute.fast_sub" = "\"repo:\" + assertion.repository + \":ref:\" + assertion.ref"
}
issuer_uri = "https://token.actions.githubusercontent.com"
}
# https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html#token-payload
gitlab = {
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.sub" = "assertion.sub"
"attribute.environment" = "assertion.environment"
"attribute.environment_protected" = "assertion.environment_protected"
"attribute.namespace_id" = "assertion.namespace_id"
"attribute.namespace_path" = "assertion.namespace_path"
"attribute.pipeline_id" = "assertion.pipeline_id"
"attribute.pipeline_source" = "assertion.pipeline_source"
"attribute.project_id" = "assertion.project_id"
"attribute.project_path" = "assertion.project_path"
"attribute.repository" = "assertion.project_path"
"attribute.ref" = "assertion.ref"
"attribute.ref_protected" = "assertion.ref_protected"
"attribute.ref_type" = "assertion.ref_type"
}
issuer_uri = "https://gitlab.com"
}
# https://developer.hashicorp.com/terraform/cloud-docs/workspaces/dynamic-provider-credentials/workload-identity-tokens#token-structure
terraform = {
attribute_mapping = {
"google.subject" = "assertion.terraform_workspace_id"
"attribute.aud" = "assertion.aud"
"attribute.terraform_run_phase" = "assertion.terraform_run_phase"
"attribute.terraform_project_id" = "assertion.terraform_project_id"
"attribute.terraform_project_name" = "assertion.terraform_project_name"
"attribute.terraform_workspace_id" = "assertion.terraform_workspace_id"
"attribute.terraform_workspace_name" = "assertion.terraform_workspace_name"
"attribute.terraform_organization_id" = "assertion.terraform_organization_id"
"attribute.terraform_organization_name" = "assertion.terraform_organization_name"
"attribute.terraform_run_id" = "assertion.terraform_run_id"
"attribute.terraform_full_workspace" = "assertion.terraform_full_workspace"
}
issuer_uri = "https://app.terraform.io"
}
# https://developer.okta.com/docs/api/openapi/okta-oauth/guides/overview/
okta = {
attribute_mapping = {
"google.subject" = "assertion.sub"
"attribute.sub" = "assertion.sub"
}
# okta issuer
# "https://${each.value.custom_settings.okta.organization_name}/oauth2/${each.value.custom_settings.okta.auth_server_name}", null)
}
}
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
wif_providers = merge([
for k, v in var.workload_identity_pools : {
for pk, pv in v.providers : "${k}/${pk}" => merge(pv, {
provider_id = pk
pool = k
attribute_mapping = merge(
try(
local.wif_defs[pv.identity_provider.oidc.template].attribute_mapping, {}
),
pv.attribute_mapping
)
})
}
]...)
}
resource "google_iam_workload_identity_pool" "default" {
for_each = var.workload_identity_pools
project = local.project.project_id
workload_identity_pool_id = each.key
display_name = each.value.display_name
description = each.value.description
disabled = each.value.disabled
}
resource "google_iam_workload_identity_pool_provider" "default" {
for_each = local.wif_providers
project = local.project.project_id
workload_identity_pool_id = (
google_iam_workload_identity_pool.default[each.value.pool].workload_identity_pool_id
)
workload_identity_pool_provider_id = each.value.provider_id
attribute_condition = each.value.attribute_condition
attribute_mapping = each.value.attribute_mapping
description = each.value.description
display_name = each.value.display_name
disabled = each.value.disabled
dynamic "aws" {
for_each = each.value.identity_provider.aws == null ? [] : [""]
content {
account_id = each.value.identity_provider.aws.account_id
}
}
dynamic "oidc" {
for_each = each.value.identity_provider.oidc == null ? [] : [""]
content {
# don't fail in the coalesce for a null issuer, but let the API do it
issuer_uri = coalesce(
# if a specific issuer is set, it overrides the template
each.value.identity_provider.oidc.issuer_uri,
try(
local.wif_defs[each.value.identity_provider.oidc.template].issuer_uri,
null
)
)
allowed_audiences = each.value.identity_provider.oidc.allowed_audiences
jwks_json = each.value.identity_provider.oidc.jwks_json
}
}
dynamic "saml" {
for_each = each.value.identity_provider.saml == null ? [] : [""]
content {
idp_metadata_xml = each.value.identity_provider.saml.idp_metadata_xml
}
}
}

View File

@@ -217,3 +217,21 @@ output "tag_values" {
k => v if try(local.tag_values[k].tag_network, null) == null
}
}
output "workload_identity_provider_ids" {
description = "Workload identity provider attributes."
value = {
for k, v in google_iam_workload_identity_pool_provider.default :
k => v.name
}
}
output "workload_identity_providers" {
description = "Workload identity provider attributes."
value = {
for k, v in local.wif_providers : k => {
name = google_iam_workload_identity_pool_provider.default[k].name
type = try(v.identity_provider.oidc.template, null)
}
}
}

View File

@@ -0,0 +1,84 @@
/**
* Copyright 2025 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "workload_identity_pools" {
description = "Workload Identity Federation pools and providers."
type = map(object({
display_name = optional(string)
description = optional(string)
disabled = optional(bool)
providers = optional(map(object({
description = optional(string)
display_name = optional(string)
attribute_condition = optional(string)
attribute_mapping = optional(map(string), {})
disabled = optional(bool, false)
identity_provider = object({
aws = optional(object({
account_id = string
}))
oidc = optional(object({
allowed_audiences = optional(list(string), [])
issuer_uri = optional(string)
jwks_json = optional(string)
template = optional(string)
}))
saml = optional(object({
idp_metadata_xml = string
}))
# x509 = optional(object({}))
})
})), {})
}))
nullable = false
default = {}
validation {
condition = alltrue(flatten([
for k, v in var.workload_identity_pools : [
for pk, pv in v.providers : (
(pv.identity_provider.aws == null ? 0 : 1) +
(pv.identity_provider.oidc == null ? 0 : 1) +
(pv.identity_provider.saml == null ? 0 : 1)
) == 1
]
]))
error_message = "Exactly one of identity_provider.aws, identity_provider.oidc, identity_provider.saml can be defined."
}
validation {
condition = alltrue(flatten([
for k, v in var.workload_identity_pools : [
for pk, pv in v.providers : contains(
["github", "gitlab", "okta", "terraform"],
coalesce(try(pv.identity_provider.oidc.template, null), "github")
)
]
]))
error_message = "Supported provider templates are: github, gitlab, okta, terraform."
}
validation {
condition = alltrue(flatten([
for k, v in var.workload_identity_pools : [
for pk, pv in v.providers : pv.identity_provider.oidc == null || (
pv.identity_provider.oidc.issuer_uri != null || (
pv.identity_provider.oidc.template != null &&
pv.identity_provider.oidc.template != "okta"
)
)
]
]))
error_message = "OIDC providers need explicit issuer_uri unless template is one of github, gitlab, terraform."
}
}