Workload Identity Federation: desplegar desde GitHub Actions a Azure sin secretos
Cómo desplegar desde GitHub Actions sin guardar un client secret, qué compara Entra ID antes de confiar en el pipeline y la letra pequeña del subject que rompe los tutoriales.

Un viernes por la tarde, el despliegue a producción falla sin que nadie haya tocado el workflow. El motivo está en Settings → Secrets and variables → Actions: un secreto llamado AZURE_CLIENT_SECRET, creado hace un año por alguien que ya no está en el equipo, acaba de caducar. Lo renuevas, lo pegas otra vez en GitHub y apuntas en algún sitio que dentro de un año volverá a pasar. Mientras tanto, esa contraseña ha vivido en dos lugares, la ha visto quien la copió y cualquier workflow del repositorio podía leerla.
La pregunta útil no es cómo rotar mejor ese secreto. Es por qué el pipeline necesita uno.
Workload Identity Federation existe para que no lo necesite.
La idea en una frase
El pipeline no guarda ninguna llave; GitHub firma quién es y Microsoft Entra ID decide si confía.
En lugar de una contraseña compartida, hay una relación de confianza. Le dices a Entra ID: "acepta los tokens que firme GitHub para este repositorio y esta rama". Cada vez que el job se ejecuta, GitHub emite un token nuevo que solo sirve para ese job, y Entra ID lo cambia por un acceso a Azure. No hay nada que copiar, nada que caducar a mitad de año y nada que se pueda filtrar desde la configuración del repositorio.
Cómo funciona
Supongamos un workflow de GitHub Actions que publica una API en Azure App Service cada vez que alguien hace push a main. Recorre el flujo paso a paso:
El paso que más se olvida es el tercero. Entra ID no pregunta "¿esto viene de GitHub?", sino "¿esto viene exactamente de este repositorio, en esta rama, para esta audiencia?". Esa comparación carácter por carácter es lo que hace segura la federación, y es también donde se rompe casi siempre.
Lo que cambia en el workflow
La diferencia en el YAML es pequeña. Este es el paso de login antes y después, tal como quedó en el repositorio del laboratorio:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# checkout, setup-dotnet y publish
- name: Azure login (client secret)
uses: azure/login@v3
with:
creds: >-
{"clientId":"${{ secrets.AZURE_CLIENT_ID }}",
"clientSecret":"${{ secrets.AZURE_CLIENT_SECRET }}",
"subscriptionId":"${{ secrets.AZURE_SUBSCRIPTION_ID }}",
"tenantId":"${{ secrets.AZURE_TENANT_ID }}"}permissions:
id-token: write # permite al job pedir un token OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
# checkout, setup-dotnet y publish
- name: Azure login (OIDC)
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}Desaparece clientSecret y aparece un bloque permissions. Los tres valores que quedan son identificadores, no credenciales: siguen como secrets solo para que GitHub los oculte en los logs. id-token: write no da permiso para escribir en ningún sitio; solo permite al job pedir el token OIDC. Y azure/login usa por defecto la audiencia api://AzureADTokenExchange, la misma que Microsoft recomienda en la credencial federada.
Antes, la misma llave en dos sitios; después, ninguna llave y una regla.
Lo que me gusta de este esquema es que cambia la pregunta de seguridad. Con un secreto, la pregunta es quién ha podido verlo. Con federación, es quién puede ejecutar un workflow en esa rama de ese repositorio. La segunda se responde con las reglas de protección de GitHub; la primera, casi nunca.
El subject, la parte que decide todo
De los tres valores que compara Entra ID, el emisor y la audiencia son siempre los mismos para GitHub. El que cambia es el subject, y Entra ID exige que coincida exactamente, incluidas mayúsculas y minúsculas.
Con un job en main, el formato que aparece en casi todos los tutoriales es este:
repo:mi-org/mi-repo:ref:refs/heads/mainEs el que copié en mi primera credencial federada. A mí me falló en la propia rama main, con un error AADSTS700213 que decía que no había ninguna credencial para el subject presentado. El subject que GitHub había firmado era otro:
repo:mbarretot@102126994/wif-lab-clean-architecture@1436045848:ref:refs/heads/mainNo era un error mío de tipeo. Desde el 15 de julio de 2026, GitHub usa por defecto un subject inmutable en los repositorios nuevos: conserva los nombres del propietario y del repositorio, y añade detrás de cada uno su ID numérico. Los IDs no se reutilizan nunca, así que otro repositorio que más adelante se llame igual no puede presentar un token que encaje con tu credencial. Microsoft llama a ese riesgo subject recycling.
Cada pieza del subject puede romper la federación si cambia.
El portal de Azure ya lo refleja. Al crear la credencial con el escenario de GitHub Actions, pide el ID de la organización y el del repositorio, y avisa de que uses el formato nuevo:
El portal ya construye el subject inmutable; los tutoriales, todavía no.
El otro fallo de esa primera tarde fue más tonto. Cambié el login a OIDC y olvidé el bloque permissions. El job falló en azure/login con un mensaje pidiendo permisos de escritura para id-token, y sin ningún código AADSTS. Esa ausencia es la pista: sin id-token: write, el runner ni siquiera recibe la URL para pedir el token, así que el fallo ocurre dentro de GitHub y nunca llega a Entra ID. Si el error trae un código AADSTS, el problema está del lado de Entra; si no lo trae, mira primero el workflow.
La letra pequeña
El laboratorio completo, con cada comando y cada error, está en GitHub Actions a Azure con OIDC. Aquí van las cuatro cosas que conviene saber antes de empezar:
- 20credenciales federadas como máximo por app registration o identidad user-assignedMicrosoft Learn
- 15 jul 2026desde esa fecha, los repositorios nuevos, renombrados o transferidos usan el subject inmutableGitHub Docs
- Renombrar el repositorio cambia el subject. El laboratorio empezó como
wif-lab-clean-architecturey lo renombré después. Esperaba que la credencial sobreviviera porque el subject lleva IDs. No fue así: los IDs no cambian, pero el subject lleva también los nombres, y el nombre nuevo dejó de coincidir con la credencial. OtroAADSTS700213, hasta que actualicé el subject. environment:cambia la identidad del job. Cuando un job declara un entorno, GitHub poneenvironment:<nombre>en el subject en lugar de la rama. A mí me pasó al añadirenvironment: productionpara tener reglas de protección: un despliegue que funcionaba dejó de hacerlo, aunque seguía enmain. La solución fue una segunda credencial con…:environment:production.- Una credencial recién creada puede no existir todavía. Un minuto después de crear la primera credencial en una app nueva, el job falló con
AADSTS70025: la app no tenía credenciales federadas configuradas. No había ningún error de configuración; el reintento pasó. Microsoft documenta que los cambios tardan en propagarse y recomienda esperar un poco y reintentar antes de concluir que algo está mal. - Una credencial por cada subject. Cada rama o entorno que despliega necesita la suya, y el límite es 20 por app. Los comodines no están soportados en las credenciales normales. Si necesitas más combinaciones, Microsoft documenta credenciales flexibles basadas en expresiones sobre los claims del token.
¿Y Managed Identity no hacía esto?
No exactamente, y es la confusión más común. En el artículo sobre Managed Identity la condición era que el código corriera en Azure: el recurso es la identidad porque Azure sabe qué recurso está pidiendo el token. Un runner de GitHub no es un recurso de tu suscripción, así que Azure no puede dar fe de él. Alguien tiene que hacerlo, y ese alguien es GitHub, firmando el token.
Las dos piezas no compiten. Una credencial federada se puede añadir a una app registration o a una managed identity user-assigned. En ese caso, la identidad sigue siendo una managed identity; lo que cambia es quién demuestra que el workload tiene derecho a usarla.
Si el código corre en Azure, Managed Identity. Si corre fuera y su plataforma firma tokens, federación.
Cuándo Workload Identity Federation no es la respuesta
La federación necesita que alguien firme por tu workload: una plataforma que emita tokens OIDC que Entra ID pueda verificar. Si dudas, responde estas preguntas:
¿Cómo se autentica tu pipeline o workload en Azure?
¿Dónde se ejecuta el código?
Quién confía en quién
Workload Identity Federation no te exime de decidir qué puede hacer el pipeline; te libera de guardar y rotar su contraseña. El rol sigue siendo tuyo, y también las reglas sobre quién puede ejecutar el workflow.
Si mañana quieres quitar el client secret de un pipeline, empieza por tres cosas:
- Busca en tus workflows
creds:,AZURE_CREDENTIALSoclient-secret. Cada uno es un secreto que caduca y que alguien tiene que rotar. - Sigue el laboratorio paso a paso en un repositorio de prueba, o parte del repositorio público. Crea la credencial con el subject que imprime
azure/login, no con el de un tutorial. - Cuando el workflow despliegue sin él, borra el secreto de GitHub y de la app registration. Mientras siga existiendo en alguno de los dos sitios, el problema sigue ahí.
Fuentes
- Workload identity federation, Microsoft Learn.
- Workload identity federation for app considerations, Microsoft Learn.
- Migrate GitHub Actions federated credentials to immutable subjects, Microsoft Learn.
- Authenticate to Azure from GitHub Actions by OpenID Connect, Microsoft Learn.
- OpenID Connect reference, GitHub Docs.
- Configuring OpenID Connect in Azure, GitHub Docs.
- OpenID Connect, GitHub Docs.
- Immutable subject claims for GitHub Actions OIDC tokens, GitHub Changelog.
