Vault Provider
The Vault provider integrates with HashiCorp Vault for centralized secret management using the KV (Key-Value) secrets engine.
At a glance
Section titled “At a glance”| Provider | vault |
| URI | vault://[namespace@]host[:port][/mount][?options] |
| Access | Read, write, and delete (0.17+); secret references are read-only |
| Best for | Self-managed, policy-controlled secret infrastructure |
| Authentication | Token or AppRole; JWT/OIDC (0.17+) |
| Build feature | vault |
| Default storage | KV path secretspec/{project}/{profile}/{key}, field value |
Quick start
Section titled “Quick start”# With default "secret" mount$ secretspec set DATABASE_URL --provider vault://vault.example.com:8200Enter value for DATABASE_URL: postgresql://localhost/mydb✓ Secret 'DATABASE_URL' saved to vault (profile: default)Prerequisites
Section titled “Prerequisites”- A running Vault server
- Authentication credentials
- KV secrets engine enabled (v1 or v2)
- Build with
--features vault
Token authentication
Section titled “Token authentication”Token authentication is the default. SecretSpec reads VAULT_TOKEN or
~/.vault-token:
$ export VAULT_TOKEN=hvs.your-token-hereAppRole authentication
Section titled “AppRole authentication”Select AppRole with ?auth=approle and provide both environment variables:
$ export VAULT_ROLE_ID=your-role-id$ export VAULT_SECRET_ID=your-secret-idStarting with SecretSpec 0.15, these credentials can instead be read from another provider so they do not live in a shell profile:
[providers.vault_approle]uri = "vault://vault.example.com:8200/secret?auth=approle"
[providers.vault_approle.credentials]role_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "role_id" } }secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "vault-approle", field = "secret_id" } }SecretSpec 0.14 supports only VAULT_ROLE_ID and VAULT_SECRET_ID.
JWT / OIDC authentication (0.17+)
Section titled “JWT / OIDC authentication (0.17+)”Select JWT with ?auth=jwt and a role. The provider performs the
auth/jwt/login exchange itself. The JWT comes from VAULT_JWT when set.
Otherwise, in a GitHub Actions or Forgejo job with id-token: write, the
provider mints one from the runner’s OIDC identity, so CI stores no static
secret.
Both role and audience accept a URI query parameter or an environment
variable:
?role=orVAULT_JWT_ROLE(required)?audience=orVAULT_JWT_AUDIENCE, matched against the role’sbound_audiences
Configuration
Section titled “Configuration”URI format
Section titled “URI format”vault://[namespace@]host[:port][/mount][?key=value&...]host[:port]: Vault server address (falls back toVAULT_ADDR)mount: KV engine mount path (default:secret)namespace@: Optional Vault namespace (also readsVAULT_NAMESPACE)?auth=approle: Use AppRole authentication (default:token)?auth=jwt(0.17+): Use JWT/OIDC authentication (requires?role=)?role=(0.17+): Vault role for JWT auth (orVAULT_JWT_ROLE)?audience=(0.17+): OIDC audience (orVAULT_JWT_AUDIENCE)?kv=1: Use KV v1 (default: v2)?tls=false: Disable TLS for development servers
Concurrent resolution
Section titled “Concurrent resolution”- One HTTP client is reused per provider instance (connection pool / h2 reuse).
- Concurrent unique-address fetches are capped at 8 by default.
- Override the cap with
SECRETSPEC_PROVIDER_CONCURRENCY(integer ≥ 1) when your Vault proxy tolerates more or less parallel load.
URI examples
Section titled “URI examples”vault://vault.example.com:8200/secretvault://team-a@vault.example.com:8200/secretvault://vault.example.com:8200/secret?auth=approle# SecretSpec 0.17+vault://vault.example.com:8200/secret?auth=jwt&role=ciProject configuration
Section titled “Project configuration”[providers]vault_prod = "vault://vault.example.com:8200/secret"
[profiles.production]DATABASE_URL = { description = "Database URL", providers = ["vault_prod"] }Storage model
Section titled “Storage model”Each secret is stored at secretspec/{project}/{profile}/{key} under the
configured mount, with its value in a field named value.
For KV v2, DATABASE_URL for project myapp and profile production is read
from GET /v1/secret/data/secretspec/myapp/production/DATABASE_URL.
Provider caching (0.17+)
Section titled “Provider caching (0.17+)”A KV v2 mount can hold a cached provider route’s
entries. Vault expires them itself: the cache’s max_age is written to the
path’s delete_version_after metadata, so a cached copy of another store’s
secret stops existing at that age even if SecretSpec never runs again.
[providers]slow = "onepassword://Production"shared_cache = "vault://vault.example.com:8200/secret"
myprovider = { fallback = ["slow"], cache = { provider = "shared_cache", max_age = "8h" } }This needs write access to the path’s metadata as well as its data. KV v1 has no expiry and is refused as a cache, rather than storing a copy that would never expire.
Deleting — cache clear and automatic
invalidation — removes the KV path’s metadata and every version, so no
soft-deleted version keeps the value recoverable. It is confined to entries
SecretSpec owns: a secret reference is never deleted, since the path it names is
managed outside SecretSpec.
Use existing secrets
Section titled “Use existing secrets”A secret’s ref field names an
existing KV entry: item is the KV path relative to the mount, and field
selects the field to read. field is required because KV entries are maps.
References are read-only in this provider.
[profiles.production]DATABASE_URL = { description = "DB", ref = { item = "myapp/config", field = "db_url" }, providers = ["vault://vault.example.com:8200/secret"] }The mount is not a ref coordinate: it comes from the provider URI (secret in
the example). To read one secret from a different mount, give that secret a
provider entry whose URI names the mount.
SecretSpec 0.16 can use AppRole to keep a user token out of the environment by
logging in from VAULT_ROLE_ID and VAULT_SECRET_ID:
$ export VAULT_ROLE_ID="$CI_VAULT_ROLE_ID"$ export VAULT_SECRET_ID="$CI_VAULT_SECRET_ID"$ secretspec export --format gha --provider "vault://vault.example.com:8200/secret?auth=approle"SecretSpec 0.17 adds a tokenless JWT/OIDC path. Under GitHub Actions or Forgejo
Actions with id-token: write, the provider mints the job’s OIDC token and logs
in with a role bound to the workflow’s claims:
$ secretspec export --format gha --provider "vault://vault.example.com:8200/secret?auth=jwt&role=ci"Advanced configuration
Section titled “Advanced configuration”KV version 1
Section titled “KV version 1”$ secretspec set DATABASE_URL --provider "vault://vault.example.com:8200/secret?kv=1"Vault namespaces
Section titled “Vault namespaces”$ secretspec check --provider vault://team-a@vault.example.com:8200/secret
$ export VAULT_NAMESPACE=team-a$ secretspec check --provider vault://vault.example.com:8200/secretDevelopment mode
Section titled “Development mode”$ vault server -dev$ export VAULT_TOKEN=hvs.dev-root-token$ secretspec check --provider "vault://127.0.0.1:8200/secret?tls=false"