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:
committed by
GitHub
parent
87ed19bc47
commit
897c6ef8c3
@@ -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({ force_context_ids = optional(bool, false) ignore_iam = optional(bool, false) })">object({…})</code> | | <code>{}</code> |
|
||||
| [universe](variables.tf#L371) | GCP universe where to deploy the project. The prefix will be prepended to the project id. | <code title="object({ prefix = string forced_jit_service_identities = optional(list(string), []) unavailable_services = optional(list(string), []) unavailable_service_identities = optional(list(string), []) })">object({…})</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({ perimeter_name = string is_dry_run = optional(bool, false) })">object({…})</code> | | <code>null</code> |
|
||||
| [workload_identity_pools](variables-identity-providers.tf#L17) | Workload Identity Federation pools and providers. | <code title="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 })) }) })), {}) }))">map(object({…}))</code> | | <code>{}</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
|
||||
|
||||
|
||||
81
modules/project/identity-providers-defs.tf
Normal file
81
modules/project/identity-providers-defs.tf
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
84
modules/project/identity-providers.tf
Normal file
84
modules/project/identity-providers.tf
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
84
modules/project/variables-identity-providers.tf
Normal file
84
modules/project/variables-identity-providers.tf
Normal 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."
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user