---
title: "IAM and Workload Identity"
slug: gcp-iam-and-workload-identity
section: GCP Infrastructure
status: stable
description: "This page explains how identity is set up across the Alliance GCP projects: which service accounts exist and why, how GitHub Actions and in-cluster workloads authenticate without long-lived keys, and which identities Terraform itself run…"
last-updated: 2026-10-07
---

# IAM and Workload Identity

This page explains how identity is set up across the Alliance GCP projects: which service accounts exist and why, how GitHub Actions and in-cluster workloads authenticate without long-lived keys, and which identities Terraform itself runs as. Per-project detail is on [gcp-overview](/docs/gcp-overview) and the environment pages; module mechanics are on [gcp-terraform-modules](/docs/gcp-terraform-modules) and the workflows on [gcp-terraform-workflows](/docs/gcp-terraform-workflows).

## Service accounts: one per workload

The standing rule is one dedicated service account per workload, with the narrowest roles that workload needs, and no shared or borrowed identities (comment above the IAM section in `environments/production/main.tf`). The `service-account` module (`modules/service-account`) creates the account and its project roles together.

- Name is derived as `<system>-<component>-<env>` with `env` one of `dev`, `qa`, `prod`, `sandbox`, `sys`. A precondition fails the plan if the id exceeds GCP's 30-character limit, so long components must be abbreviated. See [gcp-naming-conventions](/docs/gcp-naming-conventions).
- Account ids are immutable in GCP, so accounts that predate the convention are imported with `account_id_override` set to the exact live id rather than renamed. Many accounts in QA, development and production are in this category.
- Project-level roles are granted with `google_project_iam_member` (one resource per role and member); the repository declares no authoritative `google_project_iam_binding` or `_iam_policy` resources. A comment in `environments/development/host-portal-deployer-globus-npm-access.tf` gives the reasoning: an authoritative binding would diff as removing every untracked member. The consequence is that Terraform does not remove members it does not declare, so grants made outside Terraform persist.
- Secret access is increasingly granted per secret (the `secret-manager-secret-bulk` module takes an `iam_bindings` map) rather than with a project-wide `roles/secretmanager.secretAccessor`. Development removed the project-wide grant from several runtime accounts for this reason (comments in `environments/development/main.tf`), but the role is still declared project-wide for other accounts in development and QA. Secret values themselves are not managed by Terraform; see [gcp-secrets-and-data-stores](/docs/gcp-secrets-and-data-stores).

## Cross-project grants

Several identities live in one project and hold roles in another. These grants are declared in the project that is being accessed, in small single-purpose files.

- `sys-n8n-sa@alliance-platform-services` is the runtime account of the isolated system n8n (`environments/platform-services/sys-n8n.tf`). It holds `roles/run.invoker` on agent Cloud Run services, for example `gke-workload-health-agent` in development (`environments/development/gke-workload-health-agent-sys-n8n-invoker.tf`) and the `security-agent` and `vertex-sync-agent` services in the agents project (`environments/vertex/security-agent-vertex-sync-agent-sys-n8n-invoker.tf`). The file in the vertex environment records grants made directly with gcloud on 2026-09-23 and is documentation only: that environment is an un-applied inventory draft with no CI apply path.
- Agent-fleet accounts hold read-only roles in the Globus projects. `security-agent-sa` (in `alliance-platform-agents`) is granted `roles/cloudsql.viewer` and `roles/cloudkms.viewer` in development, QA, production and mcp, plus `roles/run.viewer`, `roles/iam.serviceAccountViewer` and `roles/secretmanager.viewer` in mcp. `env-audit-agent-sa` (in `alliance-platform-agents`) is granted `roles/secretmanager.viewer`, `roles/cloudsql.viewer` and `roles/compute.viewer` in development, QA and production. `user-addition-agent-sa` lives in the development project and is granted `roles/iam.securityReviewer` in QA, production and mcp. Comments on the security-agent files record that without the grant the scan gets 403 on every check, so the finding category is blind rather than clean.
- Accounts in `alliance-mcp-services` hold roles in the development project: the per-backend `*-mcp-sa` accounts have `roles/artifactregistry.reader` (several MCP services pull images from the development Artifact Registry), `globus-mcp-dev-sa` and `nexus-hub-sa` also have `roles/cloudsql.client`, and `platform-hub-sa` has viewer roles, `roles/secretmanager.secretAccessor` and `roles/iam.serviceAccountTokenCreator` on the development `manus-agent-sa` (the code comment says the Platform Hub dashboards use it to impersonate that account for Cloud Logging reads). In the `mcp` project, it also has access to the `platform-hub-gateway-pat` secret, a robot PAT mounted as the `GATEWAY_PAT` environment variable in the `platform-hub` Cloud Run service to allow the "Agent Activity" page to read audit events.
- `environment-index-sa` in `alliance-mcp-services` was the runtime account of the first `environment-index` Cloud Run service (its Terraform under `environments/mcp/` has since been removed from the repo). It held `roles/run.viewer` in development, QA and production, and `roles/secretmanager.secretAccessor` on the `github-agent-workflow-app-id` and `github-agent-workflow-app-private-key` secrets in development. The board has moved to `alliance-platform-services` (`environments/platform-services/environment-index.tf`). `environment-index-sa@alliance-platform-services` is granted `roles/run.viewer` in development, QA and production (`environment_index_platform_sa_run_viewer` in each `main.tf`) and `roles/secretmanager.secretAccessor` on the same two secrets in development, and the `alliance-mcp-services` pair is removed with that service. The mcp `roles/run.viewer` grants go away with the mcp service once DNS has cut over. The new account is the service's runtime and CI deploy identity, and its `roles/iam.workloadIdentityUser` binding is scoped to the `Alliance-Strategies/environment-index` repository. The platform-services file is documentation of live intent: that root module has never been initialised or applied.
- `globus-prod-runtime` (production) holds `roles/storage.objectViewer` and `roles/storage.objectCreator` on the development-project bucket `globus-profile-photos-prod`, declared in `environments/development/globus-prod-runtime-photo-bucket-cross-project-access.tf`.

## Workload Identity Federation for GitHub Actions

GitHub Actions authenticates to GCP through Workload Identity Federation, with OIDC from `token.actions.githubusercontent.com`. Each pool has a provider whose `attribute_condition` limits which repositories can exchange a token. The `workload-identity-pool` module sets `prevent_destroy` on both pool and provider. Pools are bound to service accounts with `roles/iam.workloadIdentityUser`, normally on a `principalSet` for `attribute.repository`.

The pool conditions below decide which repositories can get a federated token at all; which service account a token can then impersonate is decided by the separate `workloadIdentityUser` binding on that service account. The mcp environment keeps one narrowly scoped pool per repository, and a comment in `environments/mcp/main.tf` records the intent that a platform-hub change should not widen anything gateway-related. Elsewhere, QA and production share one multi-repository application pool. Pools seen in code and live state:

| Pool | Where | Condition (summary) | Service account it is bound to in code | Purpose |
|---|---|---|---|---|
| `github-actions-iac` | dev, qa, prod, mcp | `alliance-gcp-infra` only; in QA the condition also admits PPPToo, WildPacksApplicationForm and the two OCR/translation infra repos (live and code agree), but the `terraform-ci-viewer-qa` binding is still restricted to `alliance-gcp-infra` | `terraform-ci-viewer-<env>` | read-only plan and drift detection |
| `github-actions-apply-dev`, `-qa`, `-prod`, `-mcp` | each env | `alliance-gcp-infra` and `refs/heads/main` | `terraform-tf` | Terraform apply |
| `github-actions-sandbox` | dev, qa, prod | `branching-sandbox` only; `feature/*` in dev, `release/*` in qa, `main` in prod | `sandbox-deployer-<env>` | sandbox deployer |
| `github-actions-pool` | qa, prod | application repos (BackOfficePrototype, HostPortalPrototype, ReplitMCP; prod adds PPPToo and WildPacksApplicationForm in code) | prod: `github-actions-mcp-prod` for the whole pool and `host-portal-deployer` for HostPortalPrototype; qa: `host-portal-qa-runtime` for HostPortalPrototype (the `github-actions-mcp-qa` binding is not in code) | application deploys |
| `github-actions` | development | any repo in the `Alliance-Strategies` organisation | `ppp2-e2e` for PPPToo only (other bindings not in code) | CI/CD |
| `manus-pool` | development | any repo in the `Alliance-Strategies` organisation; provider has an audience restriction that the module cannot model | no binding in code | agent use |
| `github-actions-n8n-wf` | development | `n8n-workflows` | `n8n-deploy-ci-dev` and `n8n-sandbox-test-ci-dev` | n8n workflow deploys and tests |
| per-repo pools | mcp | one repo each: gcp-connector, ReplitMCP (two pools), wordpress-mcp, platform-hub | the matching deployer account | MCP service deploys |

Points worth knowing:

- Apply pools are separate from the viewer pool on purpose: apply authority and read-only authority must never share a trust boundary (comments in each `ci-terraform-apply.tf`). The `main` ref lock means only workflows running from `main` can obtain apply credentials.
- Production and mcp apply are `workflow_dispatch` only, each requiring a typed phrase (`apply-to-production`, `apply-to-mcp`) and with a destroy threshold of 1. Development and qa apply on merge to `main` (path-filtered, or by manual dispatch) with a threshold of 3. A GitHub Environment required-reviewers rule could not be used because the organisation's GitHub plan rejected it (comment in `.github/workflows/terraform-apply-prod.yml`).
- The two development pools scoped to the whole organisation (`github-actions`, `manus-pool`) are the broadest trust in the estate: any workflow in any organisation repository can request a token from them. The code binds only `ppp2-e2e` (PPPToo) to `github-actions` and nothing to `manus-pool`; bindings made outside Terraform were not checked.
- In production, `github-actions-mcp-prod` is bound to the whole `github-actions-pool` (`principalSet` ending `/*`, with a code comment saying the provider condition is the repository allowlist), so any repository admitted by that condition can impersonate it. That account holds `roles/storage.admin`, `roles/container.admin`, `roles/artifactregistry.writer` and `roles/secretmanager.secretAccessor` in production.
- Live production `github-actions-pool` lists `globus-ocr-infra` in its condition; the code (`environments/production/main.tf`) lists five repositories without it. The difference may come from an edit made outside Terraform (a code comment records that the PPPToo and WildPacksApplicationForm entries were first applied with gcloud and formalised afterwards). Because of the pool-wide binding above, the extra repository falls inside that trust.

## Terraform runner identities

| Identity | Per env | Roles | Used by |
|---|---|---|---|
| `terraform-tf` | dev, qa, prod, mcp | broad write roles (Cloud SQL, Cloud Run, IAM, Secret Manager, networking, project IAM admin) | apply workflows only |
| `terraform-ci-viewer-<env>` | dev, qa, prod, mcp | `roles/viewer`, `roles/secretmanager.viewer` (metadata only), plus `roles/storage.objectUser` on its own state bucket | plan-on-PR, nightly drift detection, prod plan-notify |

The viewer accounts cannot be truly read-only against the GCS backend: `terraform plan` takes a state lock by writing a `.tflock` object, so each viewer needs object write on its own state bucket (scoped to that bucket, not project-wide). Separately, the plan step in `terraform-plan-pr.yml` always exits 0 and only reports the outcome in the pull request comment heading (`No changes`, `Changes present` or `Plan failed`), so a passing check does not mean the plan succeeded; read the comment. `terraform-tf` in development additionally holds `roles/datastore.owner` and `roles/iam.roleAdmin`, added in dedicated files for resources it manages.

## GKE Workload Identity: two parts

A pod in GKE gets a Google identity only when both halves are in place:

1. Kubernetes side: the cluster has a workload pool (`<project>.svc.id.goog`, present live in dev, qa and prod) and the Kubernetes ServiceAccount carries the annotation `iam.gke.io/gcp-service-account` naming the Google service account.
2. Google side: that service account grants `roles/iam.workloadIdentityUser` to `serviceAccount:<project>.svc.id.goog[<namespace>/<ksa>]`. Terraform declares these members, for example `n8n/n8n-ksa`, `external-secrets/external-secrets`, `globus-qa/globus-qa-ksa`.

Terraform owns only the Google side. Kubernetes annotations live in manifests that this repository does not own (see `docs/runbooks/qa-wipe-checklist.md`, which describes first-time n8n binding as an out-of-band step). If only one half is in place the pod does not get the intended Google identity. Changing a runtime service account therefore means updating both sides together.

## Observed drift

The generated tables below list the service-account and pool differences between Terraform and live state. The patterns:

- Development has live service accounts that Terraform does not declare, mostly agent accounts (branch-drift, bug-diagnosis, dev-to-qa-sync, env-audit, gke-workload-health, observer, overseer, posthog, pr-hygiene, pr-latency, secrets-drift, vertex-sync, volume-testing) plus a few others (for example `data-lake-bigquery`, `domain-admin-sa`, `sevis-sdk-reader`). The agent accounts are the fleet that the agents project is meant to own; see [gcp-env-vertex](/docs/gcp-env-vertex).
- The agents project is the reverse: two accounts declared in code (`pr-hygiene-agent`, `cloudbuild-teams-notifier`) were not found live there, while several live accounts there are undeclared. The declarations appear to name development-project accounts. The vertex environment is an un-applied inventory draft, so none of its declarations are applied by CI.
- QA and prod each have a few undeclared accounts, including `manus-agent-sa` in both. The default compute accounts are platform-created. `github-actions-sandbox` is live in dev, qa and prod; the code declares a pool with that id (display name `GitHub Actions (sandbox)`), so the unmatched flag is a name-matching artefact and the live conditions agree with the code.
- `itrequestportal-sa` and `sys-n8n-sa` are declared for `alliance-platform-services` but could not be compared live.

## Security observations

These are facts from the 2026-10-01 snapshots and should be reviewed; they are not conclusions.

- Development: `roles/owner` is held by two users and by `manus-agent-sa`; `roles/editor` by the default compute and Cloud Services accounts. Three principals show as `deleted:` (`vi-gke-nodes`, `virtual-interviews-sa`, `ci-failure-alert-agent`). Two users from `cleversteam.com` hold `roles/storage.admin`; one user from `nomnomdata.com` holds `roles/bigquery.admin` and `roles/iam.serviceAccountCreator`; one user from `cloudkite.io` holds `roles/viewer` and `roles/browser`; a temporary external-style account (`gtempaccount.com` domain) also has access.
- QA: one deleted service account (`db-query-proxy-qa-sa`) remains on `roles/cloudsql.client`; a database export/import service account holds `roles/owner`; `manus-agent-sa` from the dev project also holds `roles/owner`; the same temporary external-style account holds `roles/viewer`, `roles/logging.viewer` and `roles/cloudsql.instanceUser`; `roles/editor` is held by a user.
- Production: no user, group or domain members and no conditional bindings were found; `manus-agent-sa` holds `roles/owner`, and `manus-agent-sa` from the dev project holds `roles/run.admin` and `roles/iam.serviceAccountUser`.
- The development `manus-agent-sa` holds `roles/owner` in QA, and `roles/run.admin` and `roles/iam.serviceAccountUser` in production. The gateway reads of the mcp project in this snapshot were made as that account, so its reach extends beyond the development project.

## Known gaps

- `alliance-platform-services` could not be read at all, so its service accounts, pools and IAM are unverified.
- `alliance-mcp-services` was read through a development-environment token. Project IAM policy and Workload Identity pools were denied, so the mcp pools above come from code only and are not confirmed live.
- The agents project has no Workload Identity pools in the snapshot, and its Terraform (the vertex environment) is an un-applied draft, so its service accounts are compared against code that CI never applies.
- GKE Kubernetes ServiceAccount annotations were not collected (no kubectl), so the Kubernetes half of each Workload Identity binding is unverified.
- Which service accounts the organisation-wide dev pools (`github-actions`, `manus-pool`) are bound to live was not checked; only bindings declared in code were read. Likewise the live `workloadIdentityUser` bindings behind the production `github-actions-pool` were not collected.
- The root `README.md` still says development and production are not under Terraform; the `.tf` files and apply workflows show they are.

## Identities

### Service accounts declared in Terraform

106 declared (one dedicated account per workload is the rule).

| Environment | Terraform address | Name | Defined in |
| --- | --- | --- | --- |
| `development` | `module.sandbox_runtime` | `sandbox-runtime-dev` | `branching-sandbox.tf:46` |
| `development` | `module.sandbox_deployer` | `sandbox-deployer-dev` | `branching-sandbox.tf:55` |
| `development` | `module.terraform_ci_viewer_dev` | `terraform-ci-viewer-dev` | `ci-terraform-readonly.tf:41` |
| `development` | `module.docs_update_agent_sa` | `docs-update-agent-sa` | `docs-update-agent-service-account.tf:8` |
| `development` | `module.karin_db_proxy_dev` | `karin-db-proxy-dev` | `karin-db-proxy-sa.tf:20` |
| `development` | `module.terraform_tf_development` | `terraform-tf` | `main.tf:236` |
| `development` | `module.manus_agent_sa_development` | `manus-agent-sa` | `main.tf:270` |
| `development` | `module.github_actions_deploy_development` | `github-actions-deploy` | `main.tf:317` |
| `development` | `module.cloudbuild_dev` | `cloudbuild-dev` | `main.tf:339` |
| `development` | `module.gke_node_sa` | `gke-node-sa` | `main.tf:360` |
| `development` | `module.n8n_gke_sa` | `n8n-gke-sa` | `main.tf:398` |
| `development` | `module.globus_dev_runtime` | `globus-dev-runtime` | `main.tf:415` |
| `development` | `module.host_service_potal` | `host-service-potal` | `main.tf:437` |
| `development` | `module.backend_service_potal` | `backend-service-potal` | `main.tf:456` |
| `development` | `module.ppp2_deployer_development` | `ppp2-deployer` | `main.tf:473` |
| `development` | `module.wildpacks_dev_runtime` | `wildpacks-dev-runtime` | `main.tf:593` |
| `development` | `module.nexus_hub_runtime_sa` | `nexus-hub-runtime-sa` | `main.tf:624` |
| `development` | `module.localreplitserviceaccount` | `localreplitserviceaccount` | `main.tf:638` |
| `development` | `module.applicant_service_sa_development` | `applicant-service-sa` | `main.tf:1984` |
| `development` | `module.n8n_runtime_dev` | `n8n-runtime-dev` | `n8n-dev-standalone.tf:63` |
| `development` | `module.n8n_runtime_sandbox` | `n8n-runtime-sandbox` | `n8n-sandbox-standalone.tf:61` |
| `development` | `module.n8n_workflows_dev_deploy_ci` | `n8n-deploy-ci-dev` | `n8n-workflows-dev-deploy-ci.tf:40` |
| `development` | `module.n8n_workflows_sandbox_test_ci` | `n8n-sandbox-test-ci-dev` | `n8n-workflows-sandbox-test-ci.tf:26` |
| `development` | `module.ppp2_e2e` | `ppp2-e2e` | `ppp2-e2e.tf:23` |
| `development` | `module.user_addition_agent_sa` | `user-addition-agent-sa` | `user-addition-agent-service-account.tf:10` |
| `mcp` | `module.terraform_tf_mcp` | `terraform-tf` | `ci-terraform-apply.tf:73` |
| `mcp` | `module.terraform_ci_viewer_mcp` | `terraform-ci-viewer-mcp` | `ci-terraform-readonly.tf:39` |
| `mcp` | `module.gcp_connector_deployer_sa` | `gcp-connector-deployer` | `gcp_connector_deploy.tf:38` |
| `mcp` | `module.itrequest_mcp_sa` | `itrequest-mcp-sa` | `itrequest-mcp.tf:38` |
| `mcp` | `module.alliance_mcp_gateway_sa` | `alliance-mcp-gateway-sa` | `main.tf:171` |
| `mcp` | `module.gateway_deployer_sa` | `gateway-deployer` | `main.tf:184` |
| `mcp` | `module.globus_mcp_dev_deployer_sa` | `globus-mcp-dev-deployer` | `main.tf:209` |
| `mcp` | `module.asana_mcp_sa` | `asana-mcp-sa` | `main.tf:219` |
| `mcp` | `module.auth0_mcp_sa` | `auth0-mcp-sa` | `main.tf:229` |
| `mcp` | `module.azure_mcp_sa` | `azure-mcp-sa` | `main.tf:239` |
| `mcp` | `module.gcp_mcp_sa` | `gcp-mcp-sa` | `main.tf:253` |
| `mcp` | `module.github_mcp_sa` | `github-mcp-sa` | `main.tf:269` |
| `mcp` | `module.globus_backoffice_mcp_sa` | `globus-backoffice-mcp-sa` | `main.tf:278` |
| `mcp` | `module.globus_mcp_dev_sa` | `globus-mcp-dev-sa` | `main.tf:287` |
| `mcp` | `module.godaddy_mcp_sa` | `godaddy-mcp-sa` | `main.tf:297` |
| `mcp` | `module.gws_mcp_sa` | `gws-mcp-sa` | `main.tf:307` |
| `mcp` | `module.hubspot_mcp_sa` | `hubspot-mcp-sa` | `main.tf:316` |
| `mcp` | `module.nexus_hub_sa` | `nexus-hub-sa` | `main.tf:325` |
| `mcp` | `module.onepassword_mcp_sa` | `onepassword-mcp-sa` | `main.tf:353` |
| `mcp` | `module.openstatus_mcp_sa` | `openstatus-mcp-sa` | `main.tf:366` |
| `mcp` | `module.openstatus_mcp_sa_development` | `openstatus-mcp-sa` | `main.tf:392` |
| `mcp` | `module.posthog_mcp_sa` | `posthog-mcp-sa` | `main.tf:404` |
| `mcp` | `module.teams_mcp_sa` | `teams-mcp-sa` | `main.tf:413` |
| `mcp` | `module.platform_hub_sa` | `platform-hub-sa` | `main.tf:431` |
| `mcp` | `module.platform_hub_deploy_sa` | `platform-hub-deploy` | `main.tf:445` |
| `mcp` | `module.development_mcp_aag_sa` | `development-mcp-aag-sa` | `main.tf:480` |
| `mcp` | `module.globus_mcp_sa_orphaned` | `globus-mcp-sa` | `main.tf:492` |
| `mcp` | `module.godaddy_mcp_server_sa_orphaned` | `godaddy-mcp-server-sa` | `main.tf:503` |
| `mcp` | `module.manus_mcp_sa_orphaned` | `manus-mcp-sa` | `main.tf:516` |
| `mcp` | `module.replitmcp_sa` | `replitmcp-sa` | `replitmcp.tf:33` |
| `mcp` | `module.replitmcp_deploy_sa` | `replitmcp-deploy` | `replitmcp.tf:46` |
| `mcp` | `module.wordpress_mcp_sa` | `wordpress-mcp-sa` | `wordpress-mcp.tf:12` |
| `mcp` | `module.wordpress_mcp_deploy_sa` | `wordpress-mcp-deploy` | `wordpress-mcp.tf:52` |
| `platform-services` | `google_service_account.itrequestportal_sa` | `itrequestportal-sa` | `itrequestportal.tf:76` |
| `platform-services` | `module.sys_n8n_sa` | `sys-n8n-sa` | `sys-n8n.tf:58` |
| `production` | `module.sandbox_runtime` | `sandbox-runtime-prod` | `branching-sandbox.tf:44` |
| `production` | `module.sandbox_deployer` | `sandbox-deployer-prod` | `branching-sandbox.tf:53` |
| `production` | `module.db_query_proxy_prod_broker_runtime` | `db-query-proxy-prod-broker` | `db-query-proxy-broker.tf:51` |
| `production` | `module.db_query_proxy_prod_runtime` | `db-query-proxy-prod-runtime` | `db-query-proxy.tf:55` |
| `production` | `module.karin_proxy_caller_prod` | `karin-proxy-caller-prod` | `db-query-proxy.tf:72` |
| `production` | `module.terraform_ci_viewer_prod` | `terraform-ci-viewer-prod` | `main.tf:266` |
| `production` | `module.external_secrets_prod_runtime` | `external-secrets-prod-runtime` | `main.tf:353` |
| `production` | `module.n8n_runtime_prod` | `n8n-runtime-prod` | `main.tf:362` |
| `production` | `module.wildpacks_prod_runtime` | `wildpacks-prod-runtime` | `main.tf:384` |
| `production` | `module.cloudbuild_teams_notifier_sa` | `cloudbuild-teams-notifier` | `main.tf:393` |
| `production` | `module.cloudbuild_prod` | `cloudbuild-prod` | `main.tf:404` |
| `production` | `module.globus_prod_runtime` | `globus-prod-runtime` | `main.tf:424` |
| `production` | `module.ppp2_deployer` | `ppp2-deployer` | `main.tf:468` |
| `production` | `module.ppp2_server_prod` | `ppp2-server-prod` | `main.tf:520` |
| `production` | `module.ppp2_client_prod` | `ppp2-client-prod` | `main.tf:530` |
| `production` | `module.keycloak_runtime_prod` | `keycloak-runtime-prod` | `main.tf:539` |
| `production` | `module.github_actions_mcp_prod` | `github-actions-mcp-prod` | `main.tf:552` |
| `production` | `module.terraform_tf_prod` | `terraform-tf` | `main.tf:595` |
| `production` | `module.host_portal_server_prod_run` | `host-portal-server-run-prod` | `main.tf:2187` |
| `production` | `module.host_portal_client_prod_run` | `host-portal-client-run-prod` | `main.tf:2197` |
| `production` | `module.host_portal_deployer` | `host-portal-deployer` | `main.tf:2210` |
| `qa` | `module.sandbox_runtime` | `sandbox-runtime-qa` | `branching-sandbox.tf:42` |
| `qa` | `module.sandbox_deployer` | `sandbox-deployer-qa` | `branching-sandbox.tf:51` |
| `qa` | `module.mishkin_db_export_import` | `mishkin-db-export-import` | `drift-reconciliation-2026-09-03.tf:25` |
| `qa` | `module.terraform_ci_viewer_qa` | `terraform-ci-viewer-qa` | `main.tf:275` |
| `qa` | `module.external_secrets_qa_runtime` | `external-secrets-qa-runtime` | `main.tf:355` |
| `qa` | `module.n8n_runtime_qa` | `n8n-runtime-qa` | `main.tf:364` |
| `qa` | `module.host_portal_qa_runtime` | `host-portal-qa-runtime` | `main.tf:376` |
| `qa` | `module.wildpacks_qa_runtime` | `wildpacks-qa-runtime` | `main.tf:390` |
| `qa` | `module.cloudbuild_teams_notifier_sa` | `cloudbuild-teams-notifier` | `main.tf:399` |
| `qa` | `module.cloudbuild_qa` | `cloudbuild-qa` | `main.tf:414` |
| `qa` | `module.globus_qa_runtime` | `globus-qa-runtime` | `main.tf:434` |
| `qa` | `module.ppp2_deployer` | `ppp2-deployer` | `main.tf:481` |
| `qa` | `module.ppp2_server_qa` | `ppp2-server-qa` | `main.tf:544` |
| `qa` | `module.ppp2_client_qa` | `ppp2-client-qa` | `main.tf:554` |
| `qa` | `module.keycloak_runtime_qa` | `keycloak-runtime-qa` | `main.tf:563` |
| `qa` | `module.github_actions_mcp_qa` | `github-actions-mcp-qa` | `main.tf:576` |
| `qa` | `module.terraform_tf_qa` | `terraform-tf` | `main.tf:620` |
| `vertex` | `module.env_audit_agent_sa_development` | `env-audit-agent-sa` | `main.tf:241` |
| `vertex` | `module.security_agent_sa` | `security-agent-sa` | `main.tf:258` |
| `vertex` | `module.asana_hygiene_agent_sa` | `asana-hygiene-agent-sa` | `main.tf:280` |
| `vertex` | `module.pr_hygiene_agent_sa` | `pr-hygiene-agent` | `main.tf:299` |
| `vertex` | `module.posthog_agent_sa` | `posthog-agent-sa` | `main.tf:334` |
| `vertex` | `module.vertex_sync_agent_sa_development` | `vertex-sync-agent-sa` | `main.tf:349` |
| `vertex` | `module.dev_to_qa_sync_agent_sa_development` | `dev-to-qa-sync-agent-sa` | `main.tf:364` |
| `vertex` | `module.cloudbuild_teams_notifier_development` | `cloudbuild-teams-notifier` | `main.tf:380` |

### Workload Identity pools declared in Terraform

| Environment | Terraform address | Pool | Defined in |
| --- | --- | --- | --- |
| `development` | `google_iam_workload_identity_pool.sandbox` | `GitHub Actions (sandbox)` | `branching-sandbox.tf:65` |
| `development` | `google_iam_workload_identity_pool_provider.sandbox` | `GitHub` | `branching-sandbox.tf:72` |
| `development` | `module.wif_pool_github_actions_apply_dev` | `github-actions-apply-dev` | `ci-terraform-apply.tf:25` |
| `development` | `module.wif_pool_github_actions_iac_development` | `github-actions-iac` | `ci-terraform-readonly.tf:19` |
| `development` | `module.wif_pool_manus` | `manus-pool` | `main.tf:2461` |
| `development` | `module.wif_pool_github_actions_development` | `github-actions` | `main.tf:2481` |
| `development` | `module.wif_pool_github_actions_n8n_workflows` | `github-actions-n8n-wf` | `n8n-workflows-dev-deploy-ci.tf:18` |
| `mcp` | `module.wif_pool_github_actions_apply_mcp` | `github-actions-apply-mcp` | `ci-terraform-apply.tf:103` |
| `mcp` | `module.wif_pool_github_actions_iac_mcp` | `github-actions-iac` | `ci-terraform-readonly.tf:17` |
| `mcp` | `module.github_gcp_connector_wif` | `github-gcp-connector` | `gcp_connector_deploy.tf:18` |
| `mcp` | `module.github_replitmcp_wif` | `github-replitmcp` | `main.tf:541` |
| `mcp` | `module.wif_pool_github_actions_platform_hub` | `github-actions-platform-hub` | `main.tf:877` |
| `mcp` | `module.wif_pool_github_actions_replitmcp` | `github-actions-replitmcp` | `replitmcp.tf:79` |
| `mcp` | `module.wif_pool_github_actions_wordpress_mcp` | `github-actions-wordpress-mcp` | `wordpress-mcp.tf:81` |
| `production` | `google_iam_workload_identity_pool.sandbox` | `GitHub Actions (sandbox)` | `branching-sandbox.tf:62` |
| `production` | `google_iam_workload_identity_pool_provider.sandbox` | `GitHub` | `branching-sandbox.tf:69` |
| `production` | `module.wif_pool_github_actions_apply_prod` | `github-actions-apply-prod` | `ci-terraform-apply.tf:33` |
| `production` | `module.wif_pool_github_actions_iac` | `github-actions-iac` | `main.tf:227` |
| `production` | `module.wif_pool_github_actions` | `github-actions-pool` | `main.tf:294` |
| `qa` | `google_iam_workload_identity_pool.sandbox` | `GitHub Actions (sandbox)` | `branching-sandbox.tf:60` |
| `qa` | `google_iam_workload_identity_pool_provider.sandbox` | `GitHub` | `branching-sandbox.tf:67` |
| `qa` | `module.wif_pool_github_actions_apply_qa` | `github-actions-apply-qa` | `ci-terraform-apply.tf:22` |
| `qa` | `module.wif_pool_github_actions_iac` | `github-actions-iac` | `main.tf:228` |
| `qa` | `module.wif_pool_github_actions` | `github-actions-pool` | `main.tf:306` |

### Service accounts and pools that exist live without a Terraform declaration

| Environment | Type | Name |
| --- | --- | --- |
| `development` | `service_account` | `branch-drift-agent-sa` |
| `development` | `service_account` | `branch-drift-scheduler` |
| `development` | `service_account` | `bug-diagnosis-agent` |
| `development` | `service_account` | `bug-diagnosis-reasoner` |
| `development` | `service_account` | `cloudbuild-teams-notifier` |
| `development` | `service_account` | `data-lake-bigquery` |
| `development` | `service_account` | `dev-to-qa-sync-agent-sa` |
| `development` | `service_account` | `domain-admin-sa` |
| `development` | `service_account` | `env-audit-agent-sa` |
| `development` | `service_account` | `gke-workload-health-agent-sa` |
| `development` | `service_account` | `observer-agent-v2-sa` |
| `development` | `service_account` | `openstatus-alert-agent-sa` |
| `development` | `service_account` | `openstatus-mcp-sa` |
| `development` | `service_account` | `overseer-agent-sa` |
| `development` | `service_account` | `posthog-agent-sa` |
| `development` | `service_account` | `pr-hygiene-agent` |
| `development` | `service_account` | `pr-latency-agent-sa` |
| `development` | `service_account` | `pr-review-eligibility-agent` |
| `development` | `service_account` | `secrets-drift-agent-sa` |
| `development` | `service_account` | `sevis-sdk-reader` |
| `development` | `service_account` | `skilled-script-469819-f2` |
| `development` | `service_account` | `vertex-sync-agent-sa` |
| `development` | `service_account` | `volume-testing-agent-sa` |
| `development` | `wif_pool` | `github-actions-sandbox` |
| `mcp` | `service_account` | `openstatus-mcp-deploy` |
| `production` | `service_account` | `data-lake-bigquery` |
| `production` | `service_account` | `manus-agent-sa` |
| `production` | `service_account` | `ocr-service-prod-deployer` |
| `production` | `wif_pool` | `github-actions-sandbox` |
| `qa` | `service_account` | `db-query-proxy-qa-runtime` |
| `qa` | `service_account` | `karin-proxy-caller-qa` |
| `qa` | `service_account` | `manus-agent-sa` |
| `qa` | `wif_pool` | `github-actions-sandbox` |
| `vertex` | `service_account` | `cloudbuild-teams-notifier-sa` |
| `vertex` | `service_account` | `github-token-minter-sa` |
| `vertex` | `service_account` | `manus-agent-sa` |
| `vertex` | `service_account` | `observer-agent-v2-sa` |
| `vertex` | `service_account` | `overseer-agent-sa` |
| `vertex` | `service_account` | `pr-hygiene-agent-sa` |
| `vertex` | `service_account` | `qa-to-prod-sync-agent-sa` |
