Load AWS SSM and Azure Key Vault secrets in GitHub Actions
How do you use secrets from AWS SSM Parameter Store or Azure Key Vault in a GitHub Actions job without copying them into GitHub Secrets? Sign the job in with OIDC, then let the Envilder Action read your versioned envilder.json and export the secrets to the steps that follow.
How the action works
The action is a composite step: it installs Node.js 22, reads the map file, fetches each secret with the cloud credentials the job already has and masks every value in the log. By default it then exports the secrets to the environment of every later step of the job; with command or env-file it runs one command or writes a .env file instead. It does not sign in to AWS or Azure itself, so an earlier step (aws-actions/configure-aws-credentials or azure/login) must do that. Use the published macalbert/envilder/github-action@v0 tag; no build step is needed.
Prerequisites
- actions/checkout before the action, so the map file exists on the runner.
- The id-token: write permission, so the job can request an OIDC token, plus contents: read.
- AWS: an IAM role that GitHub can assume, used through aws-actions/configure-aws-credentials.
- Azure: an app registration with a federated credential, used through azure/login.
- An envilder.json committed to your repository
The GitHub Action only supports pull mode (no push).
Choosing a mode
The action has three modes, and the inputs you set pick one. The default needs nothing but map-file:
| Mode | How to select it | Who can read the secrets | Files written |
|---|---|---|---|
| Job environment (default) | Only map-file | Every later step of the job | The runner's GITHUB_ENV file, in its temporary directory (no .env) |
| Command | command | Only that command and the processes it starts | None |
| .env file | env-file | Any step or tool that reads the file | The .env file |
In the default mode the action appends the secrets to the GITHUB_ENV file of the runner, and the runner sets them in the environment of every step that runs after it in the same job, including third-party actions, Docker container actions and steps that run in a job container. Steps read them like any other variable: process.env.DB_PASSWORD, os.environ["DB_PASSWORD"] or $DB_PASSWORD. Other jobs do not get them, so each job that needs secrets runs the action again. A container you start yourself with docker run does not inherit them: pass them explicitly, for example with docker run -e DB_PASSWORD.
Use env-file only when a tool can read the secrets from a file and nothing else, for example docker compose --env-file. Prefer a path under runner.temp to keep the file out of the repository workspace. env-file and command cannot be combined.
- 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 -dNot writing a .env file removes the most common way secrets leak from CI: a file picked up by upload-artifact, a cache, COPY . . in a Dockerfile or git add -A. Later tools can still write the variables to files, so treat build and test outputs as you would without Envilder. It also does not protect against malicious code in the job, which can read environment variables as easily as a file. Every mode masks the values in the log. The job environment and command modes are fail-closed: if any secret is missing or empty, the step fails and nothing is exported or started.
AWS SSM Parameter Store with OIDC
1. Let GitHub assume an IAM role
Add token.actions.githubusercontent.com as an OpenID Connect identity provider in IAM (audience sts.amazonaws.com, once per account), then create a role with this trust policy. The sub condition limits it to one repository and branch; use repo:my-org/my-repo:environment:production to limit it to a GitHub environment instead.
{
"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"
}
}
}]
}On github.com, repositories created, renamed or transferred on or after 15 July 2026, and those opted in to immutable subject claims, get a sub that also carries the owner and repository IDs: repo:my-org@OWNER_ID/my-repo@REPO_ID:ref:refs/heads/main. Other repositories keep the name-only format shown above. Check the OIDC settings of your repository for the exact prefix and use the same value in the AWS trust policy and the Azure federated credential.
2. Minimum permissions
The action calls ssm:GetParameter with decryption, once per entry in the map file. It never lists, writes or deletes parameters, so this is the only permission the role needs. Scope it to your application's path:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": "ssm:GetParameter",
"Resource": "arn:aws:ssm:eu-west-1:123456789012:parameter/myapp/prod/*"
}]
}SecureString parameters encrypted with the default aws/ssm key need nothing else. If they use a customer managed KMS key, also allow kms:Decrypt on that key. The account shown in the log comes from sts:GetCallerIdentity, which needs no permission.
3. Map file and workflow
The map file is the same one you use with the CLI: each environment variable points to a parameter name.
{
"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 deployExpected output
The step log shows the mode, the AWS identity in use, each resolved variable with its value masked, and a summary:
π 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!Every value is also registered with GitHub's add-mask command, so a later step that prints it shows *** instead. If you use env-file, the .env file stays on the runner; don't upload it as an artifact.
Azure Key Vault with OIDC
1. Create a federated credential
Create an app registration and its service principal, then add a federated credential that trusts tokens from one repository and branch. No client secret is created. The subject uses the same format as the AWS sub, including the immutable variant when your repository emits it.
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. Minimum role
Key Vault Secrets User lets the identity read secret values and nothing more. Assign it on the vault, not on the resource group or subscription:
az role assignment create \
--role "Key Vault Secrets User" \
--assignee {APP_ID} \
--scope /subscriptions/{SUB}/resourceGroups/{RG}/providers/Microsoft.KeyVault/vaults/{VAULT}If the vault uses access policies instead of Azure RBAC, grant get on secrets: az keyvault set-policy --name {VAULT} --spn {APP_ID} --secret-permissions get.
3. Map file and workflow
With $config in the map file, the workflow doesn't need the provider or vault-url inputs. Key Vault names only allow letters, digits and hyphens, so Envilder converts paths: /myapp/prod/stripe-key is read as myapp-prod-stripe-key (leading slash removed, slashes turned into hyphens).
{
"$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 deployThe client, tenant and subscription IDs identify the app; they are not credentials, so repository variables are enough. When set, the provider and vault-url inputs override $config in the map file.
Multi-environment workflow
Keep one map file per environment and pick it from the workflow input. A job that sets environment gets an OIDC token whose sub ends in :environment:<name> instead of :ref:refs/heads/main, so each environment's AWS trust policy (or Azure federated credential) must use that subject: repo:my-org/my-repo:environment:production, or repo:my-org@OWNER_ID/my-repo@REPO_ID:environment:production if your repository emits immutable subjects. Each GitHub environment can then hold its own role ARN and protection rules.
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 deployRun one command with the secrets
With the command input, the action runs that command with bash, from the repository root, with the secrets in its environment, and ends the step with the command's exit code. The secrets are not exported, so later steps, including third-party actions, never see them. Use it when a single step needs the secrets and you want the smallest exposure.
- 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 stepYou can also run the CLI yourself with npx envilder exec --map=envilder.json -- <command> in a run step. It needs Node.js 22.12 or later (actions/setup-node), and its stderr must stay in the step log, because GitHub reads the masks from there.
Why not copy the secrets into GitHub Secrets
Copying values into repository secrets works until a value changes. From then on there are two copies:
- Rotation: when a secret is rotated in SSM or Key Vault, every repository with a copy keeps deploying the old value until someone updates it by hand.
- Drift and audit: GitHub Secrets can't be read back, so you can't check whether the copy still matches the source. Reading from the cloud is recorded with the role or app behind each request: in CloudTrail on AWS, and in the Key Vault audit logs on Azure once diagnostic settings are enabled for the vault.
- Long-lived keys: copying secrets usually comes with an AWS access key or Azure client secret stored in GitHub as well. With OIDC the job gets a short-lived token, and nothing stored in GitHub works outside the workflow.
- One list: the map file is versioned with the code, reviewed in pull requests, and is the same file developers use locally with the CLI.
GitHub Secrets is still a good fit for values that only exist for the pipeline, such as a token for a third-party deploy service.
Common errors
Messages as they appear in the step log, and how to fix them. Envilder masks the secret path it prints next to each variable, except for the last three characters. Messages that come from AWS or Azure are shown as they are and can include the full parameter or resource name.
The map file can't be read
π¨ Failed to initialize: Failed to read map file: envilder.jsonThe checkout step is missing or runs after the action, or map-file points to the wrong place. Paths are relative to the repository root.
The role can't be assumed
Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentityThis comes from aws-actions/configure-aws-credentials, before Envilder runs. Check that the workflow has id-token: write and that the sub in the trust policy matches the repository, branch or environment exactly, in the subject format your repository uses (with or without immutable IDs). Tag, pull_request and environment jobs produce a different sub.
No AWS credentials
π₯ GAME OVER β some secrets could not be fetched
β DB_PASSWORD β *********************ord
β WHY?
Could not load credentials from any providersNo step signed the job in to AWS before the action. Add aws-actions/configure-aws-credentials above it, in the same job: credentials don't carry over between jobs.
Access denied to a parameter
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 actionThe role is valid but its policy doesn't cover that parameter. Check the path in Resource and the region in the ARN, which must match the aws-region of the credentials step.
Expired credentials
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).The role session ended before the step ran (one hour by default). Run Envilder right after the credentials step, or raise role-duration-seconds together with the role's maximum session duration.
Parameter or secret not found
β STRIPE_KEY secret not found (path: *******************key) β skippedCheck the name, the region and, on Azure, the converted name. In the default mode and with command, a missing or empty secret fails the step: nothing is exported and the command does not start. With env-file it is a warning, not a failure: the step still succeeds, with fewer secrets in the summary than in the map. In a new .env file the variable is left out. If the file already existed (committed, cached or written by an earlier step), its old value is kept, so delete the file before the action when a stale value must not survive. If a missing value must stop the deploy, use the default mode or command instead of env-file.
Azure vault URL missing
vaultUrl is required when using Azure provider. Set it in $config.vaultUrl in your map file or via --vault-url flag.Set the vault-url input or $config.vaultUrl in the map file. Use the vault URI shown in the Azure portal, for example https://my-vault.vault.azure.net.
Access denied on Azure Key Vault
Caller is not authorized to perform action on resource.azure/login worked but the identity has no data-plane role on the vault. Assign Key Vault Secrets User on the vault and wait a few minutes, since role assignments take time to propagate. Vaults that use access policies report instead that the identity doesn't have secrets get permission.
AWS profile in the map file
If envilder.json sets $config.profile for local use, the action passes that profile to the AWS SDK. GitHub runners have no AWS profiles, and with a profile set the SDK ignores the credentials from configure-aws-credentials. Remove the profile from the map used in CI, or keep a separate map for CI.
Action inputs & outputs
Inputs
| Input | Required | Default | Description |
|---|---|---|---|
map-file | Yes | - | Path to JSON mapping file |
env-file | No | - | Optional. Path to a .env file to write, for tools that need a file. Without env-file or command, the secrets go to the job environment |
command | No | - | Optional. Command to run (with bash) with the secrets only in its environment. Cannot be combined with env-file |
provider | No | aws | aws or azure |
vault-url | No | - | Azure Key Vault URL |
Outputs
| Output | Description |
|---|---|
env-file-path | Path to the generated .env file (empty unless env-file is set) |