En corto

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
export PROJECT_ID=my-project
export PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
export GITHUB_ORG=my-org
export REPO=my-org/my-repo

gcloud iam workload-identity-pools create github \
  --project=$PROJECT_ID --location=global --display-name="GitHub Actions"

gcloud iam workload-identity-pools providers create-oidc github-oidc \
  --project=$PROJECT_ID --location=global --workload-identity-pool=github \
  --display-name="GitHub OIDC" \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == '$GITHUB_ORG'"

gcloud iam service-accounts create github-actions --project=$PROJECT_ID --display-name="GitHub Actions"
gcloud iam service-accounts add-iam-policy-binding github-actions@$PROJECT_ID.iam.gserviceaccount.com \
  --project=$PROJECT_ID --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/github/attribute.repository/$REPO"

Y en el workflow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write            # sin esto GitHub no emite el token OIDC
    steps:
      - uses: actions/checkout@v4
      - id: auth
        uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: projects/123456789012/locations/global/workloadIdentityPools/github/providers/github-oidc
          service_account: github-actions@my-project.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v3
      - run: gcloud storage ls

Los roles que necesite el pipeline (Artifact Registry, Cloud Run, GKE…) se los das a github-actions@… como a cualquier service account.

Por qué

La alternativa clásica es exportar una llave JSON del service account y guardarla como secreto en GitHub. Esa llave no caduca, cualquiera con acceso al repo o a los logs puede copiarla, y rotarla es un trabajo manual que nadie hace. Con Workload Identity Federation GitHub emite un token OIDC de corta vida para cada job, Google lo verifica contra el issuer de GitHub, comprueba tu attribute condition y entrega credenciales temporales del service account. No hay secreto que guardar ni rotar, y puedes limitar quién entra hasta el nivel de repo o rama.

Las piezas

PiezaQué es
Pool (github)Contenedor de identidades externas; uno por proveedor basta
Proveedor OIDC (github-oidc)Confía en https://token.actions.githubusercontent.com y mapea claims del token (repository, repository_owner, actor, ref) a atributos
Attribute conditionFiltro CEL sobre esos claims. Sin ella, cualquier repo de GitHub podría intentar entrar al pool; el README oficial pide ponerla siempre
Binding workloadIdentityUserQué identidades del pool pueden hacerse pasar por el service account: attribute.repository/my-org/my-repo, o attribute.repository_owner/my-org para toda la organización
auth@v3Cambia el token de GitHub por credenciales de Google y las deja configuradas para gcloud, los SDK y Terraform

Para restringir a una rama, mapea el claim y úsalo en la condición y en el binding:

1
2
--attribute-mapping="…,attribute.ref=assertion.ref"
--attribute-condition="assertion.repository_owner == 'my-org' && assertion.ref == 'refs/heads/main'"

Con Terraform

El módulo oficial hace pool, proveedor y bindings en un bloque:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module "github_actions_sa" {
  source     = "terraform-google-modules/service-accounts/google"
  version    = "~> 4.0"
  project_id = var.project_id
  names      = ["github-actions"]
  project_roles = ["${var.project_id}=>roles/artifactregistry.writer"]
}

module "gh_oidc" {
  source              = "terraform-google-modules/github-actions-runners/google//modules/gh-oidc"
  project_id          = var.project_id
  pool_id             = "github"
  provider_id         = "github-oidc"
  attribute_condition = "assertion.repository_owner == 'my-org'"
  sa_mapping = {
    "github-actions" = {
      sa_name   = "projects/${var.project_id}/serviceAccounts/${module.github_actions_sa.email}"
      attribute = "attribute.repository/my-org/my-repo"
    }
  }
}

output "workload_identity_provider" { value = module.gh_oidc.provider_name }

El output es exactamente el valor que va en workload_identity_provider del workflow.

Sin service account

auth@v3 también admite federación directa: el principalSet recibe roles IAM sin pasar por un service account (workload_identity_provider y project_id, sin service_account). Menos piezas, pero no todos los servicios aceptan principales federados; para Artifact Registry, GCS y la mayoría de APIs sí, y es el camino que Google recomienda para casos nuevos.

Trampas

  • id-token: write en permissions del job. Sin él, auth falla con “Unable to get ACTIONS_ID_TOKEN_REQUEST_URL”.
  • Número de proyecto, no ID, en la ruta del proveedor y del principalSet: projects/123456789012/….
  • Permission 'iam.serviceAccounts.getAccessToken' denied: falta el binding roles/iam.workloadIdentityUser o el attribute.repository no coincide (mayúsculas y nombre exacto del repo).
  • La condición debe referirse a claims mapeados: si usas assertion.ref en la condición sin haberlo mapeado a un atributo, el proveedor se crea pero nunca casa.
  • Forks y PRs externos: los tokens de un PR desde un fork llevan repository del fork; una condición por repository_owner los deja fuera, que suele ser lo que quieres.
  • Rotación: no hay nada que rotar, pero sí que revisar. gcloud iam workload-identity-pools providers describe y los logs de auditoría de STS dicen quién está entrando.

Referencia: google-github-actions/auth y Workload Identity Federation en la documentación de Google Cloud.