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:

ModoCómo se eligeQuién puede leer los secretosArchivos escritos
Entorno del job (por defecto)Solo map-fileTodos los pasos posteriores del jobEl archivo GITHUB_ENV del runner, en su directorio temporal (ningún .env)
ComandocommandSolo ese comando y los procesos que iniciaNinguno
Archivo .envenv-fileCualquier paso o herramienta que lea el archivoEl 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.

.github/workflows/compose.ymlyaml
- 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 -d

No 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.

trust-policy.jsonjson
{
  "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:

envilder-read.jsonjson
{
  "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.

envilder.jsonjson
{
  "DB_PASSWORD": "/myapp/prod/db-password",
  "STRIPE_KEY": "/myapp/prod/stripe-key"
}
.github/workflows/deploy.ymlyaml
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 deploy

Salida 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).

envilder.jsonjson
{
  "$config": {
    "provider": "azure",
    "vaultUrl": "https://my-vault.vault.azure.net"
  },
  "DB_PASSWORD": "myapp-prod-db-password",
  "STRIPE_KEY": "/myapp/prod/stripe-key"
}
.github/workflows/deploy-azure.ymlyaml
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 deploy

Los 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.

.github/workflows/deploy-env.ymlyaml
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 deploy

Ejecutar 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.

.github/workflows/deploy.ymlyaml
- 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 step

Tambié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.

Lee la guía completa de exec →

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.json

Falta 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:AssumeRoleWithWebIdentity

Este 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 providers

Ningú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 action

El 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) — skipped

Revisa 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

InputRequeridoPor defectoDescripción
map-fileSí-Ruta al archivo JSON de mapeo
env-fileNo-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
commandNo-Opcional. Comando que se ejecuta (con bash) con los secretos solo en su entorno. No se puede combinar con env-file
providerNoawsaws o azure
vault-urlNo-URL de Azure Key Vault

Outputs

OutputDescripción
env-file-pathRuta al archivo .env generado (vacía si no se define env-file)