diff --git a/google_agent_platform/README.md b/google_agent_platform/README.md index c5ac29e0..1ecbe2f7 100644 --- a/google_agent_platform/README.md +++ b/google_agent_platform/README.md @@ -28,7 +28,29 @@ | [secret\_id](#output\_secret\_id) | n/a | +# google\_agent\_platform +## GitHub immutable OIDC subjects + +GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims. +Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches. +This affects the deployer service account trust when a GitHub environment is required. + +To check a repository and get its prefix, run: + +```sh +gh api repos///actions/oidc/customization/sub +``` + +When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`: + +```hcl +github_subject_prefixes = { + "example-org/example-repo" = "repo:example-org@123456/example-repo@456789" +} +``` + +See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs. ## Inputs @@ -41,6 +63,7 @@ | [deploy\_repositories](#input\_deploy\_repositories) | GitHub repositories, in org/repository format, that can deploy agents to this environment. | `list(string)` | `[]` | no | | [environment](#input\_environment) | Environment to create (like, 'dev', 'stage', or 'prod'). | `string` | n/a | yes | | [gcp\_organization\_id](#input\_gcp\_organization\_id) | GCP organization ID, used in the Agent Identity principal. | `number` | `442341870013` | no | +| [github\_subject\_prefixes](#input\_github\_subject\_prefixes) | OIDC subject prefix per deploy repository, for repositories with GitHub's immutable subject
claims enabled. Copy sub\_claim\_prefix from the repository's actions/oidc/customization/sub API.
Repositories not listed use repo:/. | `map(string)` | `{}` | no | | [image\_repository](#input\_image\_repository) | Artifact Registry repository the Reasoning Engine service agent pulls BYOC images from. Null skips the grant. |
object({
project = string
location = string
name = string
})
| `null` | no | | [project\_id](#input\_project\_id) | The project the agents run in. | `string` | n/a | yes | | [secret\_readers](#input\_secret\_readers) | Extra members that can read the secrets bag, for example a workgroup in nonprod. | `list(string)` | `[]` | no | diff --git a/google_agent_platform/locals.tf b/google_agent_platform/locals.tf index 8773c070..efb60a31 100644 --- a/google_agent_platform/locals.tf +++ b/google_agent_platform/locals.tf @@ -3,7 +3,7 @@ locals { deploy_principals = [ for repo in var.deploy_repositories : var.deploy_github_environment != null ? - "principal://iam.googleapis.com/${local.wip_id}/subject/repo:${repo}:environment:${var.deploy_github_environment}" : + "principal://iam.googleapis.com/${local.wip_id}/subject/${lookup(var.github_subject_prefixes, repo, "repo:${repo}")}:environment:${var.deploy_github_environment}" : "principalSet://iam.googleapis.com/${local.wip_id}/attribute.repository/${repo}" ] diff --git a/google_agent_platform/main.tf b/google_agent_platform/main.tf index 9ed91c6e..8fa91e1c 100644 --- a/google_agent_platform/main.tf +++ b/google_agent_platform/main.tf @@ -1,3 +1,29 @@ +/** + * # google_agent_platform + * + * ## GitHub immutable OIDC subjects + * + * GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims. + * Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches. + * This affects the deployer service account trust when a GitHub environment is required. + * + * To check a repository and get its prefix, run: + * + * ```sh + * gh api repos///actions/oidc/customization/sub + * ``` + * + * When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`: + * + * ```hcl + * github_subject_prefixes = { + * "example-org/example-repo" = "repo:example-org@123456/example-repo@456789" + * } + * ``` + * + * See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs. + */ + data "google_project" "this" { project_id = var.project_id } diff --git a/google_agent_platform/tests/agent_platform.tftest.hcl b/google_agent_platform/tests/agent_platform.tftest.hcl index 949777d0..cf0b9071 100644 --- a/google_agent_platform/tests/agent_platform.tftest.hcl +++ b/google_agent_platform/tests/agent_platform.tftest.hcl @@ -75,6 +75,21 @@ run "prod_requires_github_environment" { } } +run "prod_immutable_subject_prefix" { + command = plan + + variables { + environment = "prod" + deploy_github_environment = "prod" + github_subject_prefixes = { "example-org/agents" = "repo:example-org@444/agents@555" } + } + + assert { + condition = one(google_service_account_iam_binding.deployer_wif[0].members) == "principal://iam.googleapis.com/projects/333333333333/locations/global/workloadIdentityPools/github-actions/subject/repo:example-org@444/agents@555:environment:prod" + error_message = "Prod deploys must use the repository's immutable subject prefix when one is set." + } +} + run "optional_resources" { command = plan diff --git a/google_agent_platform/variables.tf b/google_agent_platform/variables.tf index 6bbccf2a..3d18e582 100644 --- a/google_agent_platform/variables.tf +++ b/google_agent_platform/variables.tf @@ -33,6 +33,21 @@ variable "deploy_github_environment" { default = null } +variable "github_subject_prefixes" { + description = <<-EOT + OIDC subject prefix per deploy repository, for repositories with GitHub's immutable subject + claims enabled. Copy sub_claim_prefix from the repository's actions/oidc/customization/sub API. + Repositories not listed use repo:/. + EOT + type = map(string) + default = {} + + validation { + condition = alltrue([for prefix in values(var.github_subject_prefixes) : can(regex("^repo:\\S+@\\d+/\\S+@\\d+$", prefix))]) + error_message = "Valid values are in the following format: repo:org@org_id/repository@repository_id." + } +} + variable "caller_repositories" { description = "GitHub repositories, in org/repository format, that invoke agents from CI without deploying them." type = list(string) diff --git a/google_tenant_bootstrap/README.md b/google_tenant_bootstrap/README.md index 876cbe0f..5b2231b8 100644 --- a/google_tenant_bootstrap/README.md +++ b/google_tenant_bootstrap/README.md @@ -60,7 +60,29 @@ module "lightweight_tenant" { | [public\_ips](#output\_public\_ips) | n/a | +# google\_tenant\_bootstrap +## GitHub immutable OIDC subjects + +GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims. +Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches. +This affects the artifact-writer service account trust when a GitHub environment is required. + +To check a repository and get its prefix, run: + +```sh +gh api repos///actions/oidc/customization/sub +``` + +When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`: + +```hcl +github_subject_prefixes = { + "example-org/example-repo" = "repo:example-org@123456/example-repo@456789" +} +``` + +See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs. ## Example ```hcl module "tenant" { @@ -99,6 +121,7 @@ module "lightweight_tenant" { | [github\_deploy\_repositories](#input\_github\_deploy\_repositories) | GitHub repositories (org/repository) that deploy. Overrides `github_repository` for the
deploy service account. | `list(string)` | `[]` | no | | [github\_repository](#input\_github\_repository) | GitHub repository (org/repository) that builds and deploys. `github_build_repositories`
and `github_deploy_repositories` override it. | `string` | `null` | no | | [github\_require\_build\_env](#input\_github\_require\_build\_env) | Require the GitHub Actions environment "build" to use the artifact-writer service account. | `bool` | `true` | no | +| [github\_subject\_prefixes](#input\_github\_subject\_prefixes) | OIDC subject prefix per build repository, for repositories with GitHub's immutable subject
claims enabled. Copy sub\_claim\_prefix from the repository's actions/oidc/customization/sub API.
Repositories not listed use repo:/. | `map(string)` | `{}` | no | | [log\_retention\_days](#input\_log\_retention\_days) | Log retention in days | `number` | `90` | no | | [project\_id](#input\_project\_id) | Tenant project ID | `string` | `null` | no | | [realm](#input\_realm) | Realm: nonprod, prod, mgmt, or global | `string` | n/a | yes | diff --git a/google_tenant_bootstrap/locals.tf b/google_tenant_bootstrap/locals.tf index d1ebd516..6fd11eac 100644 --- a/google_tenant_bootstrap/locals.tf +++ b/google_tenant_bootstrap/locals.tf @@ -24,7 +24,7 @@ locals { github_build_repositories = length(var.github_build_repositories) > 0 ? var.github_build_repositories : compact([var.github_repository]) github_build_principals = [ for repo in local.github_build_repositories : var.github_require_build_env ? - "principal://iam.googleapis.com/${local.wip_id}/subject/repo:${repo}:environment:build" : + "principal://iam.googleapis.com/${local.wip_id}/subject/${lookup(var.github_subject_prefixes, repo, "repo:${repo}")}:environment:build" : "principalSet://iam.googleapis.com/${local.wip_id}/attribute.repository/${repo}" if local.writer_service_account_name != null ] diff --git a/google_tenant_bootstrap/main.tf b/google_tenant_bootstrap/main.tf index 3f058ca7..2565414e 100644 --- a/google_tenant_bootstrap/main.tf +++ b/google_tenant_bootstrap/main.tf @@ -1,3 +1,29 @@ +/** + * # google_tenant_bootstrap + * + * ## GitHub immutable OIDC subjects + * + * GitHub repositories created after July 15, 2026, and repositories renamed or transferred after that date, use immutable OIDC subject claims. + * Their `sub` starts with `repo:@/@` instead of `repo:/`, so an environment-scoped binding in the old form never matches. + * This affects the artifact-writer service account trust when a GitHub environment is required. + * + * To check a repository and get its prefix, run: + * + * ```sh + * gh api repos///actions/oidc/customization/sub + * ``` + * + * When `use_immutable_subject` is `true`, add the `sub_claim_prefix` value to `github_subject_prefixes`: + * + * ```hcl + * github_subject_prefixes = { + * "example-org/example-repo" = "repo:example-org@123456/example-repo@456789" + * } + * ``` + * + * See [Immutable subject claims](https://docs.github.com/en/actions/reference/security/oidc#immutable-subject-claims) in the GitHub docs. + */ + module "google_gke_tenant" { count = var.disable_shared_integrations ? 0 : 1 source = "../google_gke_tenant" diff --git a/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl b/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl index 54fa38dd..2431b7aa 100644 --- a/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl +++ b/google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl @@ -83,6 +83,28 @@ run "lightweight_prod" { } } +run "immutable_subject_prefix" { + command = plan + + variables { + function = "aiservices" + realm = "prod" + environment = "prod" + project_id = "app-prod" + disable_shared_integrations = true + github_build_repositories = ["example-org/app", "example-org/legacy"] + github_subject_prefixes = { "example-org/app" = "repo:example-org@111/app@222" } + } + + assert { + condition = google_service_account_iam_binding.artifact_writer_access[0].members == toset([ + "principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/github-actions/subject/repo:example-org@111/app@222:environment:build", + "principal://iam.googleapis.com/projects/123456789012/locations/global/workloadIdentityPools/github-actions/subject/repo:example-org/legacy:environment:build", + ]) + error_message = "Listed repositories must use their immutable subject prefix, others the legacy form." + } +} + run "lightweight_nonprod_without_writer" { command = plan diff --git a/google_tenant_bootstrap/variables.tf b/google_tenant_bootstrap/variables.tf index 541cb4d1..14e36a25 100644 --- a/google_tenant_bootstrap/variables.tf +++ b/google_tenant_bootstrap/variables.tf @@ -134,6 +134,23 @@ variable "github_require_build_env" { EOT } +variable "github_subject_prefixes" { + type = map(string) + default = {} + description = <<-EOT + OIDC subject prefix per build repository, for repositories with GitHub's immutable subject + claims enabled. Copy sub_claim_prefix from the repository's actions/oidc/customization/sub API. + Repositories not listed use repo:/. + EOT + validation { + condition = alltrue([ + for prefix in values(var.github_subject_prefixes) : + can(regex("^repo:\\S+@\\d+/\\S+@\\d+$", prefix)) + ]) + error_message = "Valid values are in the following format: repo:org@org_id/repository@repository_id." + } +} + variable "log_retention_days" { type = number description = "Log retention in days"