OpenBao Provider
The OpenBao provider integrates with OpenBao’s KV (Key-Value) secrets engine using OpenBao’s own provider identity and configuration conventions.
At a glance
| Provider | openbao (0.17+) |
| URI | openbao://[namespace@]host[:port][/mount][?options] |
| Access | Read, write, and delete; secret references are read-only |
| Best for | Open-source, policy-controlled secret infrastructure |
| Authentication | Token, AppRole, or JWT/OIDC |
| Build feature | openbao (0.17+) |
| Default storage | KV path secretspec/{project}/{profile}/{key}, field value |
Quick start
$ export BAO_TOKEN=hvs.your-token-here
$ secretspec set DATABASE_URL --provider openbao://bao.example.com:8200Enter value for DATABASE_URL: postgresql://localhost/mydb✓ Secret 'DATABASE_URL' saved to openbao (profile: default)
Terminal window
Setup
Prerequisites
- A running OpenBao server
- Authentication credentials
- KV secrets engine enabled (v1 or v2)
- Build with
--features openbao
Environment compatibility
For the variables defined by the OpenBao CLI, the provider follows its
documented convention: BAO_ADDR, BAO_NAMESPACE, BAO_TOKEN, and
BAO_TOKEN_PATH take precedence over their VAULT_* counterparts.
SecretSpec additionally defines OpenBao-prefixed provider inputs for
AppRole and JWT authentication. These are consumed by SecretSpec, not by
the bao CLI, and retain the corresponding VAULT_* names as
compatibility fallbacks.
Token authentication
Token authentication is the default. SecretSpec checks these sources in order:
- The alias’s
tokenprovider credential BAO_TOKEN, thenVAULT_TOKEN- The file selected by
BAO_TOKEN_PATH, thenVAULT_TOKEN_PATH - The OpenBao CLI’s default
~/.vault-token
$ export BAO_TOKEN=hvs.your-token-here
Terminal window
AppRole authentication
Select AppRole with ?auth=approle. OpenBao roles bind a SecretID by
default, so the usual configuration provides both inputs:
$ export BAO_ROLE_ID=your-role-id
$ export BAO_SECRET_ID=your-secret-id
Terminal window
These are SecretSpec provider inputs, not OpenBao CLI variables.
VAULT_ROLE_ID and VAULT_SECRET_ID remain accepted as fallbacks.
Prefer semantic provider credentials when configuring an alias:
[providers.bao_approle]uri = "openbao://bao.example.com:8200/secret?auth=approle"
[providers.bao_approle.credentials]role_id = { provider = "onepassword", ref = { vault = "Infra", item = "bao-approle", field = "role_id" } }secret_id = { provider = "onepassword", ref = { vault = "Infra", item = "bao-approle", field = "secret_id" } }
secretspec.toml
Disabling SecretID binding removes AppRole’s usual second credential. Keep the server default unless the workload deliberately relies on another trust boundary, such as a tightly controlled Agent host and network constraints.
Custom authentication mounts
AppRole and JWT methods mounted somewhere other than their defaults can
be selected with ?auth_mount=. The value is relative to /v1/auth:
openbao://bao.example.com:8200/secret?auth=approle&auth_mount=platform-approleopenbao://bao.example.com:8200/secret?auth=jwt&auth_mount=ci-jwt&role=ci
The provider logs in at /v1/auth/platform-approle/login and
/v1/auth/ci-jwt/login, respectively. The KV mount remains the provider
URI path (secret in these examples).
JWT / OIDC authentication
Select JWT with ?auth=jwt. The provider performs the auth/jwt/login
exchange itself. The JWT comes from SecretSpec’s BAO_JWT input, then
the VAULT_JWT compatibility fallback. Otherwise, in a GitHub Actions
or Forgejo job with id-token: write, the provider mints one from the
runner’s OIDC identity.
Starting with SecretSpec 0.18, the role may be omitted when the JWT auth
mount has a default_role; OpenBao then selects that role during login.
An explicit SecretSpec role still takes precedence.
?role=,BAO_JWT_ROLE, orVAULT_JWT_ROLE; optional with a server-configureddefault_role(0.18+)?audience=,BAO_JWT_AUDIENCE, orVAULT_JWT_AUDIENCE
Provider credentials
| Credential | Environment fallback | Available since |
|---|---|---|
role_id | BAO_ROLE_ID → VAULT_ROLE_ID | 0.17+ |
secret_id | BAO_SECRET_ID → VAULT_SECRET_ID | 0.17+ |
token | BAO_TOKEN → VAULT_TOKEN | 0.17+ |
See the complete provider credential reference for all supported providers and environment fallbacks.
Configuration
URI format
openbao://[namespace@]host[:port][/mount][?key=value&...]
host[:port]: OpenBao address (falls back throughBAO_ADDR,VAULT_ADDR)mount: KV engine mount path (default:secret)namespace@: Optional namespace (falls back throughBAO_NAMESPACE,VAULT_NAMESPACE)?auth=approle: Use AppRole authentication (default:token)?auth=jwt: Use JWT/OIDC authentication; a server-configureddefault_rolecan supply the role when using SecretSpec 0.18+?auth_mount=(0.18+): Non-default AppRole or JWT mount beneath/v1/auth?role=: OpenBao role for JWT auth?audience=: Audience requested from the CI OIDC issuer?kv=1: Use KV v1 (default: v2)?tls=false: Disable TLS for development servers
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 OpenBao proxy tolerates more or less parallel load.
URI examples
openbao://bao.example.com:8200/secretopenbao://[email protected]:8200/secretopenbao://bao.example.com:8200/secret?auth=approle# SecretSpec 0.18+openbao://bao.example.com:8200/secret?auth=approle&auth_mount=platform-approleopenbao://bao.example.com:8200/secret?auth=jwt&role=ci# SecretSpec 0.18+, with default_role configured on the JWT auth mountopenbao://bao.example.com:8200/secret?auth=jwtProject configuration
[providers]bao_prod = "openbao://bao.example.com:8200/secret"
[profiles.production]DATABASE_URL = { description = "Database URL", providers = ["bao_prod"] }
secretspec.toml
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
A KV v2 mount can hold a cached provider
route’s entries. OpenBao 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.
This needs write access to the path’s metadata as well as its data, and
KV v1 is refused as a cache because it has no expiry.
Deleting — cache clear and
automatic invalidation — removes the KV path’s metadata and every
version, and is confined to entries SecretSpec owns: a secret reference
is never deleted, since the path it names is managed outside SecretSpec.
Use existing secrets
A secret’s ref field
names an existing KV entry: item is the path relative to the mount,
and field selects the field to read. References are read-only so a
single-field write cannot overwrite the entry’s other fields.
[profiles.production]DATABASE_URL = { description = "DB", ref = { item = "myapp/config", field = "db_url" }, providers = ["openbao://bao.example.com:8200/secret"] }CI/CD
AppRole avoids placing a user token in the CI environment:
$ export BAO_ROLE_ID="$CI_ROLE_ID"
$ export BAO_SECRET_ID="$CI_SECRET_ID"
$ secretspec export --format gha --provider "openbao://bao.example.com:8200/secret?auth=approle"
Terminal window
With GitHub Actions or Forgejo Actions id-token: write, JWT/OIDC
avoids a static authentication credential:
$ secretspec export --format gha --provider "openbao://bao.example.com:8200/secret?auth=jwt&role=ci"
Terminal window
Advanced configuration
KV version 1
$ secretspec set DATABASE_URL --provider "openbao://bao.example.com:8200/secret?kv=1"
Terminal window
OpenBao namespaces
$ secretspec check --provider openbao://[email protected]:8200/secret
$ export BAO_NAMESPACE=team-a
$ secretspec check --provider openbao://bao.example.com:8200/secret
Terminal window
Development mode
$ bao server -dev -dev-root-token-id="dev-only-token"
$ export BAO_TOKEN="dev-only-token"
$ secretspec check --provider "openbao://127.0.0.1:8200/secret?tls=false"
Terminal window
Last updated Oct 08, 2026