Carga secretos de AWS SSM y Azure Key Vault en GitHub Actions
¿Cómo usas secretos de AWS SSM Parameter Store o Azure Key Vault en un job de GitHub Actions sin copiarlos a GitHub Secrets? Autentica el job con OIDC y deja que la Action de Envilder lea tu envilder.json versionado y exporte los secretos a los pasos siguientes.
Cómo funciona la Action
La Action es un paso compuesto: instala Node.js 22, lee el archivo de mapeo, obtiene cada secreto con las credenciales de nube que ya tiene el job y enmascara cada valor en el log. Por defecto, después exporta los secretos al entorno de todos los pasos posteriores del job; con command o env-file, en cambio, ejecuta un comando o escribe un archivo .env. No inicia sesión en AWS ni en Azure por sí misma, así que debe hacerlo un paso anterior (aws-actions/configure-aws-credentials o azure/login). Usa la etiqueta publicada macalbert/envilder/github-action@v0; no hace falta ningún paso de compilación.
Prerrequisitos
- actions/checkout antes de la Action, para que el archivo de mapeo exista en el runner.
- El permiso id-token: write, para que el job pueda pedir un token OIDC, y contents: read.
- AWS: un rol IAM que GitHub pueda asumir, usado a través de aws-actions/configure-aws-credentials.
- Azure: un registro de aplicación con una credencial federada, usado a través de azure/login.
- Un envilder.json en tu repositorio
La GitHub Action solo soporta el modo pull (sin push).
Elegir un modo
La Action tiene tres modos, y los inputs que defines eligen uno. El modo por defecto solo necesita map-file:
| Modo | Cómo se elige | Quién puede leer los secretos | Archivos escritos |
|---|---|---|---|
| Entorno del job (por defecto) | Solo map-file | Todos los pasos posteriores del job | El archivo GITHUB_ENV del runner, en su directorio temporal (ningún .env) |
| Comando | command | Solo ese comando y los procesos que inicia | Ninguno |
| Archivo .env | env-file | Cualquier paso o herramienta que lea el archivo | El archivo .env |
En el modo por defecto, la Action añade los secretos al archivo GITHUB_ENV del runner, y el runner los pone en el entorno de cada paso que se ejecuta después dentro del mismo job, incluidas las actions de terceros, las actions de contenedor Docker y los pasos que se ejecutan dentro de un container del job. Los pasos los leen como cualquier otra variable: process.env.DB_PASSWORD, os.environ["DB_PASSWORD"] o $DB_PASSWORD. Los demás jobs no los reciben, así que cada job que necesita secretos vuelve a ejecutar la Action. Un contenedor que inicias tú mismo con docker run no los hereda: pásalos explícitamente, por ejemplo con docker run -e DB_PASSWORD.
Usa env-file solo cuando una herramienta no puede leer los secretos de otra forma que de un archivo, por ejemplo docker compose --env-file. Mejor una ruta dentro de runner.temp, para que el archivo quede fuera del espacio de trabajo del repositorio. env-file y command no se pueden combinar.
- uses: macalbert/envilder/github-action@v0
with:
map-file: envilder.json
env-file: ${{ runner.temp }}/envilder.env
- run: docker compose --env-file "${{ runner.temp }}/envilder.env" up -dNo escribir ningún archivo .env elimina la vía más habitual por la que se filtran secretos en CI: un archivo que acaba en upload-artifact, en una caché, en un COPY . . de un Dockerfile o en un git add -A. Las herramientas posteriores todavía pueden escribir las variables en archivos, así que trata los resultados de build y de tests como lo harías sin Envilder. Tampoco protege contra código malicioso dentro del job, que puede leer las variables de entorno tan fácilmente como un archivo. Todos los modos enmascaran los valores en el log. Los modos de entorno del job y de comando fallan de forma segura: si falta algún secreto o está vacío, el paso falla y no se exporta ni se inicia nada.
AWS SSM Parameter Store con OIDC
1. Permite que GitHub asuma un rol IAM
Añade token.actions.githubusercontent.com como proveedor de identidad OpenID Connect en IAM (audiencia sts.amazonaws.com, una vez por cuenta) y crea un rol con esta política de confianza. La condición sub lo limita a un repositorio y una rama; usa repo:my-org/my-repo:environment:production para limitarlo a un entorno de GitHub.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}]
}En github.com, los repositorios creados, renombrados o transferidos a partir del 15 de julio de 2026, y los que han activado los sujetos inmutables, reciben un sub que también incluye los ID del propietario y del repositorio: repo:my-org@OWNER_ID/my-repo@REPO_ID:ref:refs/heads/main. El resto mantiene el formato solo con nombres que se muestra arriba. Consulta la configuración OIDC de tu repositorio para conocer el prefijo exacto y usa el mismo valor en la política de confianza de AWS y en la credencial federada de Azure.
2. Permisos mínimos
La Action llama a ssm:GetParameter con descifrado, una vez por cada entrada del archivo de mapeo. No lista, escribe ni borra parámetros, así que es el único permiso que necesita el rol. Limítalo a la ruta de tu aplicación:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "ssm:GetParameter",
"Resource": "arn:aws:ssm:eu-west-1:123456789012:parameter/myapp/prod/*"
}]
}Los parámetros SecureString cifrados con la clave por defecto aws/ssm no necesitan nada más. Si usan una clave KMS administrada por el cliente, permite también kms:Decrypt sobre esa clave. La cuenta que aparece en el log viene de sts:GetCallerIdentity, que no necesita ningún permiso.
3. Archivo de mapeo y workflow
El archivo de mapeo es el mismo que usas con la CLI: cada variable de entorno apunta al nombre de un parámetro.
{
"DB_PASSWORD": "/myapp/prod/db-password",
"STRIPE_KEY": "/myapp/prod/stripe-key"
}name: Deploy Application
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/github-envilder
aws-region: eu-west-1
- name: Pull Secrets from AWS SSM
uses: macalbert/envilder/github-action@v0
with:
map-file: envilder.json
- run: npm ci
- run: npm run deploySalida esperada
El log del paso muestra el modo, la identidad de AWS en uso, cada variable resuelta con el valor enmascarado y un resumen:
🔑 Envilder GitHub Action - Starting...
📋 Map file: envilder.json
🧠 Mode: job environment (secrets in every later step)
☁ AWS identity · account=123456789012 · region=eu-west-1 · profile=default
🪙 RESOLVING SECRETS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ DB_PASSWORD → *****************x9Q
✓ STRIPE_KEY → *****************************4Rt
envilder: 2 secrets exported to the job environment (DB_PASSWORD, STRIPE_KEY)
✅ Secrets loaded successfully!Cada valor se registra también con el comando add-mask de GitHub, de modo que un paso posterior que lo imprima muestra ***. Si usas env-file, el archivo .env se queda en el runner; no lo subas como artefacto.
Azure Key Vault con OIDC
1. Crea una credencial federada
Crea un registro de aplicación y su service principal, y añade una credencial federada que confíe en los tokens de un repositorio y una rama. No se crea ningún secreto de cliente. El sujeto usa el mismo formato que el sub de AWS, incluida la variante inmutable cuando tu repositorio la emite.
az ad app create --display-name github-envilder
az ad sp create --id {APP_ID}
az ad app federated-credential create --id {APP_ID} --parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:my-org/my-repo:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'2. Rol mínimo
Key Vault Secrets User permite leer los valores de los secretos y nada más. Asígnalo sobre el vault, no sobre el grupo de recursos ni la suscripción:
az role assignment create \
--role "Key Vault Secrets User" \
--assignee {APP_ID} \
--scope /subscriptions/{SUB}/resourceGroups/{RG}/providers/Microsoft.KeyVault/vaults/{VAULT}Si el vault usa directivas de acceso en lugar de Azure RBAC, concede get sobre los secretos: az keyvault set-policy --name {VAULT} --spn {APP_ID} --secret-permissions get.
3. Archivo de mapeo y workflow
Con $config en el archivo de mapeo, el workflow no necesita los inputs provider ni vault-url. Los nombres de Key Vault solo admiten letras, dígitos y guiones, así que Envilder convierte las rutas: /myapp/prod/stripe-key se lee como myapp-prod-stripe-key (sin la barra inicial, barras convertidas en guiones).
{
"$config": {
"provider": "azure",
"vaultUrl": "https://my-vault.vault.azure.net"
},
"DB_PASSWORD": "myapp-prod-db-password",
"STRIPE_KEY": "/myapp/prod/stripe-key"
}name: Deploy with Azure Key Vault
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- name: Pull Secrets from Azure Key Vault
uses: macalbert/envilder/github-action@v0
with:
map-file: envilder.json
- run: npm ci
- run: npm run deployLos ID de cliente, tenant y suscripción identifican la aplicación; no son credenciales, así que basta con variables del repositorio. Si se informan, los inputs provider y vault-url sobrescriben el $config del archivo de mapeo.
Workflow multi-entorno
Ten un archivo de mapeo por entorno y elígelo desde el input del workflow. Un job que define environment recibe un token OIDC con un sub que termina en :environment:<nombre> en lugar de :ref:refs/heads/main, así que la política de confianza de AWS (o la credencial federada de Azure) de cada entorno debe usar ese sujeto: repo:my-org/my-repo:environment:production, o repo:my-org@OWNER_ID/my-repo@REPO_ID:environment:production si tu repositorio emite sujetos inmutables. Después, cada entorno de GitHub puede tener su ARN de rol y sus reglas de protección.
name: Deploy to Environment
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
required: true
type: choice
options: [dev, staging, production]
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@v5
- uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: Pull ${{ inputs.environment }} secrets
uses: macalbert/envilder/github-action@v0
with:
map-file: config/${{ inputs.environment }}/envilder.json
- run: npm ci
- run: npm run deployEjecutar un comando con los secretos
Con el input command, la Action ejecuta ese comando con bash, desde la raíz del repositorio, con los secretos en su entorno, y termina el paso con el código de salida del comando. Los secretos no se exportan, así que los pasos posteriores, incluidas las actions de terceros, nunca los ven. Úsalo cuando solo un paso necesita los secretos y quieres la mínima exposición.
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: arn:aws:iam::123456789012:role/github-envilder
aws-region: eu-west-1
- name: Deploy with secrets
uses: macalbert/envilder/github-action@v0
with:
map-file: envilder.json
command: ./deploy.sh --prod
- run: echo "$DB_PASSWORD" # empty: the secrets stayed in the deploy stepTambién puedes ejecutar la CLI tú mismo con npx envilder exec --map=envilder.json -- <comando> en un paso run. Necesita Node.js 22.12 o posterior (actions/setup-node), y su stderr debe quedarse en el log del paso, porque es de ahí de donde GitHub lee las máscaras.
Por qué no copiar los secretos a GitHub Secrets
Copiar los valores a los secretos del repositorio funciona hasta que un valor cambia. A partir de ahí hay dos copias:
- Rotación: cuando se rota un secreto en SSM o Key Vault, cada repositorio con una copia sigue desplegando el valor antiguo hasta que alguien lo actualiza a mano.
- Divergencia y auditoría: GitHub Secrets no se puede volver a leer, así que no puedes comprobar si la copia sigue coincidiendo con el origen. Leer de la nube queda registrado con el rol o la aplicación que hizo cada petición: en CloudTrail en AWS, y en los logs de auditoría de Key Vault en Azure una vez activada la configuración de diagnóstico del vault.
- Claves de larga duración: copiar secretos suele ir acompañado de una clave de acceso de AWS o un secreto de cliente de Azure guardados también en GitHub. Con OIDC el job recibe un token de corta duración, y nada de lo guardado en GitHub funciona fuera del workflow.
- Una sola lista: el archivo de mapeo se versiona con el código, se revisa en las pull requests y es el mismo archivo que usan los desarrolladores en local con la CLI.
GitHub Secrets sigue siendo una buena opción para valores que solo existen para el pipeline, como un token de un servicio de despliegue externo.
Errores habituales
Mensajes tal como aparecen en el log del paso y cómo resolverlos. Envilder enmascara la ruta del secreto que muestra junto a cada variable, salvo los tres últimos caracteres. Los mensajes que vienen de AWS o de Azure se muestran tal cual y pueden incluir el nombre completo del parámetro o del recurso.
No se puede leer el archivo de mapeo
🚨 Failed to initialize: Failed to read map file: envilder.jsonFalta el paso de checkout o se ejecuta después de la Action, o map-file apunta a un lugar incorrecto. Las rutas son relativas a la raíz del repositorio.
No se puede asumir el rol
Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentityEste error viene de aws-actions/configure-aws-credentials, antes de que se ejecute Envilder. Comprueba que el workflow tiene id-token: write y que el sub de la política de confianza coincide exactamente con el repositorio, la rama o el entorno, en el formato de sujeto que usa tu repositorio (con o sin ID inmutables). Los jobs de tag, de pull_request y con environment generan un sub distinto.
Sin credenciales de AWS
💥 GAME OVER — some secrets could not be fetched
✗ DB_PASSWORD → *********************ord
⭐ WHY?
Could not load credentials from any providersNingún paso ha autenticado el job en AWS antes de la Action. Añade aws-actions/configure-aws-credentials antes, en el mismo job: las credenciales no pasan de un job a otro.
Acceso denegado a un parámetro
User: arn:aws:sts::123456789012:assumed-role/github-envilder/GitHubActions is not authorized to perform: ssm:GetParameter on resource: arn:aws:ssm:eu-west-1:123456789012:parameter/myapp/prod/db-password because no identity-based policy allows the ssm:GetParameter actionEl rol es válido pero su política no cubre ese parámetro. Revisa la ruta de Resource y la región del ARN, que debe coincidir con el aws-region del paso de credenciales.
Credenciales caducadas
AWS credentials are expired or invalid. Your security token or SSO session may have expired. Refresh your credentials and retry (for SSO, run: aws sso login).La sesión del rol terminó antes de que se ejecutara el paso (una hora por defecto). Ejecuta Envilder justo después del paso de credenciales, o aumenta role-duration-seconds junto con la duración máxima de sesión del rol.
Parámetro o secreto inexistente
✗ STRIPE_KEY secret not found (path: *******************key) — skippedRevisa el nombre, la región y, en Azure, el nombre convertido. En el modo por defecto y con command, un secreto que falta o está vacío hace fallar el paso: no se exporta nada y el comando no se inicia. Con env-file es un aviso, no un error: el paso termina bien, con menos secretos en el resumen que en el mapeo. En un archivo .env nuevo la variable queda fuera. Si el archivo ya existía (en el repositorio, en caché o escrito por un paso anterior), se conserva el valor antiguo, así que borra el archivo antes de la Action cuando un valor obsoleto no deba sobrevivir. Si un valor que falta debe detener el despliegue, usa el modo por defecto o command en lugar de env-file.
Falta la URL del vault de Azure
vaultUrl is required when using Azure provider. Set it in $config.vaultUrl in your map file or via --vault-url flag.Informa el input vault-url o $config.vaultUrl en el archivo de mapeo. Usa el URI del vault que muestra el portal de Azure, por ejemplo https://my-vault.vault.azure.net.
Acceso denegado en Azure Key Vault
Caller is not authorized to perform action on resource.azure/login ha funcionado pero la identidad no tiene ningún rol de plano de datos sobre el vault. Asigna Key Vault Secrets User sobre el vault y espera unos minutos, porque las asignaciones de rol tardan en propagarse. Los vaults con directivas de acceso indican en cambio que la identidad no tiene el permiso get de secretos.
Perfil de AWS en el archivo de mapeo
Si envilder.json define $config.profile para uso local, la Action pasa ese perfil al SDK de AWS. Los runners de GitHub no tienen perfiles de AWS, y con un perfil definido el SDK ignora las credenciales de configure-aws-credentials. Quita el perfil del mapeo que usas en CI, o ten un mapeo separado para CI.
Inputs y outputs de la Action
Inputs
| Input | Requerido | Por defecto | Descripción |
|---|---|---|---|
map-file | Sí | - | Ruta al archivo JSON de mapeo |
env-file | No | - | Opcional. Ruta a un archivo .env que escribir, para herramientas que necesitan un archivo. Sin env-file ni command, los secretos van al entorno del job |
command | No | - | Opcional. Comando que se ejecuta (con bash) con los secretos solo en su entorno. No se puede combinar con env-file |
provider | No | aws | aws o azure |
vault-url | No | - | URL de Azure Key Vault |
Outputs
| Output | Descripción |
|---|---|
env-file-path | Ruta al archivo .env generado (vacía si no se define env-file) |