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.
Prerequisites for Vault integration A reachable Vault, a scoped policy, an auth method, and the Vault plugin enable central secret management. reachable Vault scoped policy AppRole / K8s auth Vault plugin Central secrets no plaintext
Authenticate the portal with AppRole or Kubernetes auth, never a static root token — the token itself must be short-lived and scoped.

Exact Configuration

  1. Write a least-privilege Vault policy granting the portal read on only its path.

    # portal-policy.hcl
    path "secret/data/portal/*" {
      capabilities = ["read"]
    }
    
  2. 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
    
  3. 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
    
Portal reads Vault at runtime The portal authenticates with a short-lived token, reads only its scoped path, and surfaces secret metadata per entity. portal backend AppRole login short-lived token scoped policy Vault secret/data/portal/* entity shows metadata only
The portal shows which secrets exist and when they were updated, never the values — visibility without exposure.

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
Vault validation checks The policy is scoped, the token is short-lived, and entity metadata resolves without values. Policy scoped one path, read Token short-lived finite ttl Metadata only no values
Confirm the plugin exposes names and timestamps but no values — a secrets viewer that shows secrets is a breach, not a feature.

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
Vault pitfalls by class Permission and expiry errors are access issues; visible values and root tokens are exposure issues. Access permission denied token expired → policy + AppRole renew Exposure values visible root token → metadata + scoped login
The two exposure failures — visible values and a root token — are the ones worth a hard CI gate; both turn the portal into a liability.

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.