← Blog Cloud y DevOps

CI/CD sin llaves: cómo despliego más de 20 repositorios a Cloud Run con Workload Identity Federation

10 de octubre de 2026 · 6 min de lectura

Durante años, desplegar a Google Cloud desde GitHub significó lo mismo en casi todos los equipos: crear una cuenta de servicio, descargar su llave en JSON y pegarla en los secretos del repositorio. Funciona, pero esa llave no vence, da acceso a la nube a quien la tenga y termina copiada en más lugares de los que uno recuerda.

Hoy despliego más de 20 repositorios a Cloud Run sin una sola llave guardada. Cuento cómo lo armé con Workload Identity Federation y lo que fui aprendiendo en el camino.

El problema con la llave en JSON

Una llave de cuenta de servicio es una contraseña que no expira. Si se filtra (un log que la imprime, un fork, un backup, una persona que se va del equipo con una copia), quien la tenga puede actuar como esa cuenta hasta que alguien se dé cuenta y la revoque.

Y con muchos repositorios el riesgo se multiplica: veinte repos con su propia llave son veinte copias que rotar, vigilar y recordar. En la práctica, nadie las rota.

Cómo funciona Workload Identity Federation

La idea es simple: en lugar de guardar una credencial, GitHub le demuestra a Google quién es en el momento del despliegue.

 GitHub Actions                 Google Cloud
 ┌────────────────┐   token     ┌──────────────────────────┐
 │ workflow del   │ ──OIDC────▶ │ Workload Identity Pool   │
 │ repositorio    │  (firmado   │ ¿es de mi organización?  │
 └────────────────┘  por GitHub)│ ¿es este repositorio?    │
                                └────────────┬─────────────┘
                                             │ credencial temporal
                                             ▼
                                ┌──────────────────────────┐
                                │ cuenta de servicio       │
                                │ (permisos mínimos)       │
                                └────────────┬─────────────┘
                                             ▼
                                         Cloud Run

Cada ejecución del workflow recibe de GitHub un token firmado que dice qué repositorio lo pidió, en qué rama y en qué workflow. Google valida esa firma, revisa las condiciones que le configuraste y entrega una credencial que dura una hora. No hay nada que guardar ni nada que rotar.

El paso a paso

1. El pool y el proveedor

El pool agrupa las identidades externas en las que confías. El proveedor le dice a Google cómo validar los tokens de GitHub y qué datos tomar de ellos:

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

gcloud iam workload-identity-pools providers create-oidc github-actions \
  --location=global --workload-identity-pool=github \
  --issuer-uri="https://token.actions.githubusercontent.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == 'mi-organizacion'"

La condición es la línea más importante. Sin ella, cualquier repositorio de GitHub podría intentar presentarse ante tu pool. Con ella, solo entran los de tu organización.

2. Quién puede usar la cuenta de servicio

La cuenta de servicio es la que tiene los permisos para desplegar. Lo que se configura es qué repositorio puede actuar como ella:

gcloud iam service-accounts add-iam-policy-binding \
  deployer@mi-proyecto.iam.gserviceaccount.com \
  --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/NUMERO_DEL_PROYECTO/locations/global/workloadIdentityPools/github/attribute.repository/mi-organizacion/mi-repo"

A esa cuenta le doy solo lo que necesita para desplegar: lanzar el build, publicar en Cloud Run, leer la imagen y los secretos, y actuar como la cuenta con la que corre el servicio. Nada de roles de propietario o editor del proyecto. Uso una cuenta de servicio por repositorio, así cada módulo despliega solo lo suyo.

3. El workflow

name: deploy
on:
  workflow_dispatch:

permissions:
  contents: read
  id-token: write   # sin esto GitHub no entrega el token

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: produccion
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/NUMERO_DEL_PROYECTO/locations/global/workloadIdentityPools/github/providers/github-actions
          service_account: deployer@mi-proyecto.iam.gserviceaccount.com
      - uses: google-github-actions/setup-gcloud@v2
      - run: gcloud run deploy mi-servicio --source . --region us-central1

Fíjate en lo que no aparece: ningún secrets.GCP_KEY. Los dos valores del paso de autenticación no son secretos, son identificadores. Si alguien los ve, no puede hacer nada con ellos desde fuera de tu organización.

Lo que aprendí con más de 20 repositorios

Un repositorio nuevo nace sin permiso

El permiso de usar la cuenta de servicio se da por repositorio. Así que el primer despliegue de un módulo nuevo falla con un 403 que dice algo como "Permission 'iam.serviceAccounts.getAccessToken' denied". No es un error del workflow: falta agregar ese repositorio a la lista.

Por eso el alta de cada módulo nuevo tiene tres pasos fijos: su cuenta de servicio, el permiso sobre el pool y el workflow. La alternativa es autorizar a toda la organización con attribute.repository_owner; es más cómodo, pero cualquier repositorio nuevo podría desplegar, y prefiero que eso sea una decisión explícita.

El otro 403 clásico llega en el primer despliegue: el workflow se autentica bien, pero un paso falla con PERMISSION_DENIED porque a la cuenta le falta un permiso concreto. A mí me faltó el de leer la imagen del registro de artefactos. El mensaje dice exactamente qué permiso falta, así que se resuelve rápido, pero conviene tener la lista completa de roles antes de replicar el esquema en veinte repositorios.

Un despliegue en verde no significa que el servicio funcione

Este me costó caro. gcloud run deploy intenta dejar el servicio público y, si la cuenta no tiene permiso para cambiar su política de acceso, lo reporta como una advertencia y termina en éxito igual. El build sale en verde, el workflow sale en verde, y el servicio responde 403 a todo el mundo. Un módulo que todavía nadie usaba pasó semanas así, con cuatro despliegues en verde, hasta que alguien intentó entrar.

Desde entonces, después de cada despliegue verifico que el servicio responda, no solo que el workflow haya terminado bien.

Nada a producción sin una aprobación

Los despliegues son manuales (workflow_dispatch), también los de pruebas. El de producción usa un environment de GitHub con revisores obligatorios y despliega una versión que ya pasó por pruebas, no lo último que haya en la rama. Alguien tiene que aprobarlo, y queda registrado quién lo hizo y cuándo. Además, cada ambiente es un proyecto de Google Cloud distinto con sus propias cuentas de servicio: la de pruebas no puede tocar producción aunque alguien se equivoque de workflow.

El pipeline tiene que ser la única puerta

Si alguien puede desplegar desde su computadora o desde la consola web, todo lo anterior se puede saltar. Los logs de auditoría de Google Cloud registran quién desplegó cada revisión y desde dónde, así que armé una alerta sobre esos logs: cuando alguien despliega por fuera del pipeline, el equipo recibe un aviso con quién fue, qué cambió y desde qué herramienta. La consola queda solo para emergencias, y cuando se usa, se sabe.

Qué haría distinto si empezara hoy

  • Prohibir las llaves desde el primer día. La política de organización iam.disableServiceAccountKeyCreation impide crear llaves nuevas. Con eso, nadie vuelve a la llave en JSON "solo por esta vez".
  • Una plantilla de workflow compartida. Con más de 20 repositorios, cada mejora al pipeline se repetía veinte veces. Un workflow reutilizable lo resuelve en un solo lugar.
  • Verificar que el servicio responda, como paso del propio workflow. Que el pipeline falle si el servicio no contesta, en lugar de depender de que alguien lo revise.
  • Escribir el despliegue para Linux desde el principio. Para arrancar rápido reutilicé los scripts de despliegue que ya existían, y eso me ató a runners de Windows. Moverlo después cuesta más que hacerlo bien al inicio.

Si estás por empezar

Si hoy tienes una llave en JSON en los secretos de tu repositorio, migrar a Workload Identity Federation toma una tarde: crear el pool, el proveedor, el permiso y cambiar un paso del workflow. Después borras la llave, y esa es la mejor parte.

Si ya lo hiciste o te trabaste en algún paso, escríbeme por LinkedIn. Me interesa saber cómo lo resolvieron otros equipos.

CI/CDGitHub ActionsGoogle CloudCloud RunWorkload Identity FederationSeguridad