Great question! You're asking:
How can GitHub Actions use different Vault roles per environment (e.g., staging, production), and how does Vault know which role to use?
Let’s break this down clearly.
You want:
staging→ usegithub-actions-stagingrole in Vaultproduction→ usegithub-actions-prodrole in Vault
And you want Vault to trust the environment context from GitHub Actions securely.
Vault does not automatically know which role to use. Instead, your GitHub Actions workflow explicitly tells Vault which role to assume during authentication.
So, you control the role at the workflow level — and can vary it by branch, environment, or job.
Here’s how to structure your workflow to use different roles per environment:
# .github/workflows/deploy.yml
name: Deploy with Vault
on:
push:
branches:
- main
- staging
permissions:
id-token: write
contents: read
jobs:
deploy-staging:
if: github.ref == 'refs/heads/staging'
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get secrets from Vault (staging role)
uses: hashicorp/vault-action@v3
with:
url: https://vault.example.com
method: jwt
jwtGithubWorkflow: true
role: github-actions-staging # 👈 Staging role
secrets: |
secret/data/staging/db-password db_password
- name: Deploy to Staging
run: echo "Using: $VAULT_SECRETS_secret_data_staging_db_password_db_password"
deploy-prod:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Get secrets from Vault (prod role)
uses: hashicorp/vault-action@v3
with:
url: https://vault.example.com
method: jwt
jwtGithubWorkflow: true
role: github-actions-prod # 👈 Production role
secrets: |
secret/data/prod/db-password db_password
- name: Deploy to Production
run: echo "Deploying with prod secrets..."When the vault-action runs:
-
GitHub issues an OIDC token containing claims like:
repository: owner/reporef:refs/heads/mainorrefs/heads/stagingenvironment: if using GitHub Environmentsactor: who triggered it
-
The action sends:
- This JWT
- The
rolename (github-actions-stagingorgithub-actions-prod)
-
Vault checks:
- Is the JWT signed by GitHub?
- Does the
roleexist? - Does the role allow this repo/branch/environment?
You can make roles context-aware using bound claims.
# Create staging role - only allows staging branch
vault write auth/jwt/role/github-actions-staging \
bound_issuer="https://token.actions.githubusercontent.com" \
user_claim="sub" \
bound_subject="repo:myorg/myrepo:ref:refs/heads/staging" \
token_policies="staging-secrets" \
ttl="10m"# Create prod role - only allows main branch
vault write auth/jwt/role/github-actions-prod \
bound_issuer="https://token.actions.githubusercontent.com" \
user_claim="sub" \
bound_subject="repo:myorg/myrepo:ref:refs/heads/main" \
token_policies="prod-secrets" \
ttl="5m"✅ This ensures that only workflows running on
maincan assume thegithub-actions-prodrole.
🔐 The
bound_subjectenforces branch-level security.
If you use GitHub Environments (like production), you can bind to that:
vault write auth/jwt/role/github-actions-prod \
bound_issuer="https://token.actions.githubusercontent.com" \
bound_claims_type=string \
bound_claims='{"environment":"production"}' \
token_policies="prod-secrets"Then in GitHub Actions:
env:
VAULT_ROLE: github-actions-${{ github.environment }}And use dynamically (requires scripting):
- name: Login to Vault
run: |
curl -s -X POST $VAULT_ADDR/v1/auth/jwt/login \
-d @- <<EOF
{
"jwt": "${{ id_token }}",
"role": "${{ env.VAULT_ROLE }}"
}
EOF
⚠️ Thevault-actiondoesn’t support dynamic roles directly, so for full dynamic control, you may needcurl.
| Question | Answer |
|---|---|
| ❓ How does Vault know which role to use? | ❗ It doesn't — the workflow specifies the role. |
| ❓ How is it secure? | ✅ Vault validates the OIDC token and checks role bindings (e.g., branch, repo, env). |
| ❓ Can I have per-env roles? | ✅ Yes — use different roles per job, with bound claims for security. |
| ❓ Best practice? | ✅ Smallest privilege: one role per env with minimal policies. |
| GitHub Branch | GitHub Env | Vault Role | Vault Policy | Bound Subject/Claims |
|---|---|---|---|---|
staging |
staging |
gh-staging |
staging-ro |
repo:org/repo:ref:refs/heads/staging |
main |
production |
gh-prod |
prod-rw |
{"environment":"production"} |
Let me know if you want a full example using curl + dynamic role selection based on github.environment!
name: Test Vault - List Secrets
on: workflow_dispatch: # Allow manual run push: branches: - main
permissions: id-token: write contents: read
jobs: test-vault: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4
- name: Login to Vault and List Secrets
uses: hashicorp/vault-action@v3
with:
url: https://your-vault-address.com
caCertificate: ${{ secrets.VAULT_CA_CERT }} # Optional: for self-signed certs
method: jwt
jwtGithubWorkflow: true
role: github-actions-test
# We won't read secrets, just list
# But action requires at least one 'secrets' line to trigger auth
secrets: |
secret/data/test/placeholder placeholder # dummy to trigger login
- name: List secrets in 'test/' folder
run: |
# Use VAULT_TOKEN from vault-action
export VAULT_TOKEN=${{ env.VAULT_TOKEN }}
# List secrets under secret/test/
vault list secret/metadata/test/