Integrating HashiCorp Vault with Backstage
Hardcoded secrets in config files are the classic portal foot-gun: they leak into git history, sit in plaintext on disk, and never rotate. Integrating HashiCorp Vault moves secrets to a central store the portal reads at runtime, and surfaces per-entity secret metadata so a team can see which secrets a service uses without seeing the values. This how-to wires Vault into Backstage for both runtime config and the Vault plugin. It extends Secrets Management & Rotation within Authentication, RBAC & Security Governance.
Prerequisites
- Backstage 1.20+ running where it can reach your Vault server.
- A Vault instance with a mount for portal secrets and a policy scoped to read only what the portal needs.
- An auth method for the portal — AppRole or Kubernetes auth — rather than a static root token.
- The
@backstage-community/plugin-vault(and backend) installed if you want per-entity secret visibility.
Exact Configuration
-
Write a least-privilege Vault policy granting the portal read on only its path.
# portal-policy.hcl path "secret/data/portal/*" { capabilities = ["read"] } -
Configure the Vault plugin so per-entity secrets surface in the portal.
# app-config.yaml # Requires @backstage-community/plugin-vault-backend >= 0.4.0 vault: baseUrl: ${VAULT_ADDR} token: ${VAULT_TOKEN} # obtained via AppRole, short-lived secretEngine: secret -
Annotate services with their Vault path so the plugin shows which secrets they use — metadata only, never values.
# catalog-info.yaml metadata: name: payments-api annotations: vault.io/secrets-path: portal/payments-api
Validation
# Requires vault CLI, curl, jq
# 1. The portal policy reads only its path
vault policy read portal-policy | grep -c 'secret/data/portal'
# Expected: >= 1, and no broader paths
# 2. The portal token is short-lived, not root
vault token lookup "${VAULT_TOKEN}" | grep -E 'ttl|policies'
# Expected: a finite ttl and portal-policy, not root
# 3. Entity secret metadata resolves without exposing values
curl -s "${PORTAL_URL}/api/vault/secrets?entity=component:default/payments-api" \
| jq '.items[0] | keys'
# Expected: names/paths present, no "value" field
Edge Cases & Troubleshooting
| Symptom | Root Cause | Resolution |
|---|---|---|
| Permission denied reading secrets | Policy too narrow or wrong path | Align the policy path with the mount and annotation |
| Portal loses access after a while | Token expired, no renewal | Use AppRole with renewal; do not pin a static token |
| Values visible in the UI | Plugin misconfigured to read data | Restrict to metadata endpoints; audit the config |
| Secrets not shown for a service | Missing vault.io/secrets-path |
Add the annotation matching the Vault path |
| Root token in config | Static high-privilege credential | Replace with a scoped AppRole login |
Frequently Asked Questions
Should the portal fetch secret values or just metadata?
Metadata by default — names, paths, last-rotated timestamps — so teams can see what a service uses without the portal ever holding the value. Fetching values should be reserved for narrow, audited cases; the more the portal reads actual secrets, the bigger a target it becomes.
How does this connect to secret rotation?
Vault becomes the single place a secret is stored and rotated, and the portal reflects the rotation timestamp so teams can spot secrets overdue for a change. Combine this with the rotation-without-downtime pattern elsewhere in this section: Vault holds the value, rotation updates it, and the portal surfaces that it happened.