Skip to content

OpenBao Provider

The OpenBao provider integrates with OpenBao’s KV (Key-Value) secrets engine using OpenBao’s own provider identity and configuration conventions.

The current release’s practical form is:

Terminal window
$ export VAULT_TOKEN=hvs.your-token-here
$ secretspec check --provider openbao://bao.example.com:8200/secret

For a minimal Rust build, enable vault:

secretspec = { version = "0.16", default-features = false, features = ["vault"] }
Provideropenbao (0.17+)
URIopenbao://[namespace@]host[:port][/mount][?options]
AccessRead and write; secret references are read-only
Best forOpen-source, policy-controlled secret infrastructure
AuthenticationToken, AppRole, or JWT/OIDC
Build featureopenbao (0.17+)
Default storageKV path secretspec/{project}/{profile}/{key}, field value
Terminal window
$ export BAO_TOKEN=hvs.your-token-here
$ secretspec set DATABASE_URL --provider openbao://bao.example.com:8200
Enter value for DATABASE_URL: postgresql://localhost/mydb
Secret 'DATABASE_URL' saved to openbao (profile: default)
  • A running OpenBao server
  • Authentication credentials
  • KV secrets engine enabled (v1 or v2)
  • Build with --features openbao

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 is the default. SecretSpec checks these sources in order:

  1. The alias’s token provider credential
  2. BAO_TOKEN, then VAULT_TOKEN
  3. The file selected by BAO_TOKEN_PATH, then VAULT_TOKEN_PATH
  4. The OpenBao CLI’s default ~/.vault-token
Terminal window
$ export BAO_TOKEN=hvs.your-token-here

Select AppRole with ?auth=approle:

Terminal window
$ export BAO_ROLE_ID=your-role-id
$ export BAO_SECRET_ID=your-secret-id

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:

secretspec.toml
[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" } }

Select JWT with ?auth=jwt and a role. 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.

  • ?role=, BAO_JWT_ROLE, or VAULT_JWT_ROLE (required)
  • ?audience=, BAO_JWT_AUDIENCE, or VAULT_JWT_AUDIENCE
openbao://[namespace@]host[:port][/mount][?key=value&...]
  • host[:port]: OpenBao address (falls back through BAO_ADDR, VAULT_ADDR)
  • mount: KV engine mount path (default: secret)
  • namespace@: Optional namespace (falls back through BAO_NAMESPACE, VAULT_NAMESPACE)
  • ?auth=approle: Use AppRole authentication (default: token)
  • ?auth=jwt: Use JWT/OIDC authentication (requires a role)
  • ?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
openbao://bao.example.com:8200/secret
openbao://team-a@bao.example.com:8200/secret
openbao://bao.example.com:8200/secret?auth=approle
openbao://bao.example.com:8200/secret?auth=jwt&role=ci
secretspec.toml
[providers]
bao_prod = "openbao://bao.example.com:8200/secret"
[profiles.production]
DATABASE_URL = { description = "Database URL", providers = ["bao_prod"] }

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.

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"] }

AppRole avoids placing a user token in the CI environment:

Terminal window
$ 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"

With GitHub Actions or Forgejo Actions id-token: write, JWT/OIDC avoids a static authentication credential:

Terminal window
$ secretspec export --format gha --provider "openbao://bao.example.com:8200/secret?auth=jwt&role=ci"
Terminal window
$ secretspec set DATABASE_URL --provider "openbao://bao.example.com:8200/secret?kv=1"
Terminal window
$ secretspec check --provider openbao://team-a@bao.example.com:8200/secret
$ export BAO_NAMESPACE=team-a
$ secretspec check --provider openbao://bao.example.com:8200/secret
Terminal window
$ 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"