Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions google_agent_platform/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,29 @@
| <a name="output_secret_id"></a> [secret\_id](#output\_secret\_id) | n/a |

<!-- BEGIN_TF_DOCS -->
# 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:<owner>@<owner_id>/<repo>@<repo_id>` instead of `repo:<owner>/<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/<owner>/<repo>/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

Expand All @@ -41,6 +63,7 @@
| <a name="input_deploy_repositories"></a> [deploy\_repositories](#input\_deploy\_repositories) | GitHub repositories, in org/repository format, that can deploy agents to this environment. | `list(string)` | `[]` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Environment to create (like, 'dev', 'stage', or 'prod'). | `string` | n/a | yes |
| <a name="input_gcp_organization_id"></a> [gcp\_organization\_id](#input\_gcp\_organization\_id) | GCP organization ID, used in the Agent Identity principal. | `number` | `442341870013` | no |
| <a name="input_github_subject_prefixes"></a> [github\_subject\_prefixes](#input\_github\_subject\_prefixes) | OIDC subject prefix per deploy repository, for repositories with GitHub's immutable subject<br/>claims enabled. Copy sub\_claim\_prefix from the repository's actions/oidc/customization/sub API.<br/>Repositories not listed use repo:<org>/<repository>. | `map(string)` | `{}` | no |
| <a name="input_image_repository"></a> [image\_repository](#input\_image\_repository) | Artifact Registry repository the Reasoning Engine service agent pulls BYOC images from. Null skips the grant. | <pre>object({<br/> project = string<br/> location = string<br/> name = string<br/> })</pre> | `null` | no |
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | The project the agents run in. | `string` | n/a | yes |
| <a name="input_secret_readers"></a> [secret\_readers](#input\_secret\_readers) | Extra members that can read the secrets bag, for example a workgroup in nonprod. | `list(string)` | `[]` | no |
Expand Down
2 changes: 1 addition & 1 deletion google_agent_platform/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
]

Expand Down
26 changes: 26 additions & 0 deletions google_agent_platform/main.tf
Original file line number Diff line number Diff line change
@@ -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:<owner>@<owner_id>/<repo>@<repo_id>` instead of `repo:<owner>/<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/<owner>/<repo>/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
}
Expand Down
15 changes: 15 additions & 0 deletions google_agent_platform/tests/agent_platform.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions google_agent_platform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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:<org>/<repository>.
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)
Expand Down
23 changes: 23 additions & 0 deletions google_tenant_bootstrap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,29 @@ module "lightweight_tenant" {
| <a name="output_public_ips"></a> [public\_ips](#output\_public\_ips) | n/a |

<!-- BEGIN_TF_DOCS -->
# 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:<owner>@<owner_id>/<repo>@<repo_id>` instead of `repo:<owner>/<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/<owner>/<repo>/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" {
Expand Down Expand Up @@ -99,6 +121,7 @@ module "lightweight_tenant" {
| <a name="input_github_deploy_repositories"></a> [github\_deploy\_repositories](#input\_github\_deploy\_repositories) | GitHub repositories (org/repository) that deploy. Overrides `github_repository` for the<br/>deploy service account. | `list(string)` | `[]` | no |
| <a name="input_github_repository"></a> [github\_repository](#input\_github\_repository) | GitHub repository (org/repository) that builds and deploys. `github_build_repositories`<br/>and `github_deploy_repositories` override it. | `string` | `null` | no |
| <a name="input_github_require_build_env"></a> [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 |
| <a name="input_github_subject_prefixes"></a> [github\_subject\_prefixes](#input\_github\_subject\_prefixes) | OIDC subject prefix per build repository, for repositories with GitHub's immutable subject<br/>claims enabled. Copy sub\_claim\_prefix from the repository's actions/oidc/customization/sub API.<br/>Repositories not listed use repo:<org>/<repository>. | `map(string)` | `{}` | no |
| <a name="input_log_retention_days"></a> [log\_retention\_days](#input\_log\_retention\_days) | Log retention in days | `number` | `90` | no |
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | Tenant project ID | `string` | `null` | no |
| <a name="input_realm"></a> [realm](#input\_realm) | Realm: nonprod, prod, mgmt, or global | `string` | n/a | yes |
Expand Down
2 changes: 1 addition & 1 deletion google_tenant_bootstrap/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
Expand Down
26 changes: 26 additions & 0 deletions google_tenant_bootstrap/main.tf
Original file line number Diff line number Diff line change
@@ -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:<owner>@<owner_id>/<repo>@<repo_id>` instead of `repo:<owner>/<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/<owner>/<repo>/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"
Expand Down
22 changes: 22 additions & 0 deletions google_tenant_bootstrap/tests/tenant_bootstrap.tftest.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
17 changes: 17 additions & 0 deletions google_tenant_bootstrap/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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:<org>/<repository>.
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"
Expand Down
Loading