OpenBao — Secrets Management & Encryption
Security, SSH
Overview
OpenBao is an open-source secrets management and encryption-as-a-service platform. It originated as a community fork of HashiCorp Vault after its license changed from MPL 2.0 to the BUSL, making commercial use without a paid agreement restrictive.
OpenBao provides a unified interface for accessing secrets — passwords, OAuth tokens, API keys, certificates, and more — with tight access control. It encrypts sensitive data at rest and supports dynamic secrets, data leasing, renewal, and revocation.
Why Use It
Without a centralized system, secrets live scattered across config files, environment variables, and developer machines. OpenBao consolidates them into one encrypted store with audit logging and fine-grained access control.
The core benefit is eliminating long-lived distributed secrets:
- Dynamic credentials — databases, Kubernetes, and other services get short-lived credentials generated on demand that auto-expire, removing manual rotation
- SSH certificate signing — users generate local key pairs signed by a central CA, producing time-limited certificates instead of distributing static SSH keys
- Encryption-as-a-service — applications encrypt and decrypt data without managing keys themselves, useful for compliance without building PKI infrastructure
- Limited blast radius — short-lived, renewable tokens with parent-child relationships mean revoking a parent invalidates all children, containing compromises
Project Health
OpenBao is a genuinely well-governed open-source project, not a vanity fork1.
It operates under MPL 2.0 — an OSI-approved license with no hosting restrictions — and is governed by the Linux Foundation (moved from LF Edge to the OpenSSF sandbox in May 2025)2. A Technical Steering Committee with members from ControlPlane, SAP, Adfinis, Wallix, IOTech, and GitLab oversees development through working groups and a three-tier contributor structure with supply-chain security safeguards3.
The community has ~380 contributors, 6,200+ GitHub stars, and 26 releases since its November 2023 inception4. Commercial supporters including ControlPlane, SAP (via its EU-funded ApeiroRA initiative)5, Adfinis, and recently Nvidia6 back it with the same model that made Kubernetes successful: companies that profit from supporting the project contribute maintainers back to it.
Gaps relative to Vault Enterprise remain: no disaster recovery or performance replication, no Sentinel policies (CEL-based policies are planned), and no managed SaaS offering. Community channels replace vendor support SLAs. The ecosystem is younger with fewer production war stories, but the trajectory is clear.
Getting Started
Authenticate
Connect to an existing server, and authenticate using the root token or a configured auth method:
export BAO_ADDR='https://your-server:8200'
export BAO_CACERT='/path/to/ca.crt'| Variable | Purpose |
|---|---|
BAO_ADDR |
URL of the OpenBao server API endpoint; default is http://127.0.0.1:8200 |
BAO_CACERT |
Path to the CA certificate that signed the server’s TLS certificate; required for identity verification |
The following command performs OIDC (OpenID Connect) authentication and stores the resulting token in BAO_TOKEN for all subsequent CLI calls. The $(...) subshell captures the token from bao login7.
export BAO_TOKEN=$(bao login -method=oidc -path=oidc -token-only)| Flag | Purpose |
|---|---|
-method=oidc |
Use the OIDC authentication method |
-path=oidc |
Mount path of the OIDC auth backend on the server |
-token-only |
Suppress verbose output; print only the raw token |
Status
Verify the installation:
bao statusKey Value
--- -----
#…
Initialized true
Sealed false
#…
HA Mode active
#…| Field | Expected | Meaning |
|---|---|---|
Initialized |
true |
Server is ready; false means storage is not prepared |
Sealed |
false |
Server can serve requests; true means all commands will fail |
HA Mode |
active |
This node is serving; standby means point BAO_ADDR elsewhere |
If the server is sealed or uninitialized, an administrator must initialize and unseal it first.
Namespaces
Namespaces provide secure multi-tenancy within a single OpenBao instance. Each namespace is an isolated environment with its own secrets engines, authentication methods, policies, and identities. If your token has delegated permissions, you can create your own namespace and act as its administrator8.
Create a new namespace:
bao namespace create v.pensoTo operate inside it, set the namespace context so all subsequent commands target that tenant:
export BAO_NAMESPACE=v.penso
# or pass it per-command
bao -namespace=v.penso statusInside your namespace, you have full control to mount secrets engines, define policies, and configure AppRole authentication without affecting other tenants.
Access Model
Every operation in OpenBao passes through three layers. An authentication method first verifies identity — such as Kubernetes, AppRole, or OIDC — and consults a role to decide which policies to attach and how long the resulting token lives. The issued token carries those policies and must be presented with every subsequent request. Finally, policies enforce path-based access control, determining whether the request is allowed on the target path.
Roles
A role is a named configuration template that defines what an operation is allowed to produce. Roles exist in two contexts, and they are unrelated to each other9.
- Auth roles — map an authenticated identity to policies and token constraints. When a pod or machine logs in via Kubernetes, AppRole, or another auth method, OpenBao looks up the role to decide which policies to attach and how long the resulting token lives
- Secrets engine roles — define rules for secrets generation. The SSH engine uses roles to determine which users can be signed, which extensions are granted, and how long certificates remain valid
In both cases, the role acts as a gatekeeper: it constrains what can be issued before anything is created. Without a matching role, authentication succeeds but no token is returned, or a signing request is rejected outright.
Policies
OpenBao uses path-based access control lists (ACLs) written in HCL or JSON. Policies are deny-by-default, meaning an empty policy grants no permissions. Every action maps to a path and a capability.
| Capability | HTTP Verb | Action |
|---|---|---|
create |
POST/PUT | Create new data at a path |
read |
GET | Read data at a path |
update |
POST/PUT | Modify existing data |
patch |
PATCH | Partial updates |
delete |
DELETE | Remove data |
list |
LIST | List keys under a path |
sudo |
— | Access root-protected endpoints |
deny |
— | Explicitly block access; always takes precedence |
Two built-in policies exist10:
default— attached to every token by default; grants minimal system accessroot— unrestricted access; should never be used in production
To verify what your current token can do on a specific path without triggering a 403 error:
bao sys capabilities -path=secrets/database/Tokens
Tokens are the primary authentication mechanism in OpenBao. Every request carries a token, which is validated against attached policies to determine what actions are allowed11. Two parameters control token lifetime:
| Parameter | Effect |
|---|---|
| TTL | Time-to-live from creation. When it reaches zero, the token expires and requests fail |
| Max TTL | Upper bound. A renewable token can be extended via renewal, but never beyond this ceiling. Set both to 0 for a non-expiring token |
A token is either renewable or not. Renewable tokens must be actively renewed by the holder before expiry, typically using bao token renew. If renewal is missed, the token and all its children expire silently.
Beyond the standard parent-child model, three types exist for specific use cases:
| Type | Behavior |
|---|---|
| Orphan | Has no parent token. Revoking another token never affects it. Useful when independent lifecycle is needed |
| Periodic | Never truly expires as long as it is renewed before each TTL window. Commonly used for long-lived service accounts |
| Batch | Short-lived, non-renewable, cached server-side. Returned by auth methods like Kubernetes or AWS. Cannot create child tokens |
Creation
Create a token with token create12:
bao token create -ttl=24hKey Value
--- -----
token s.t01ozKccP69Q1PxmLReQ3ept.MIr2Kh
token_accessor 5jQ6WzRdP9auOVjB03uehg6s.MIr2Kh
token_duration 24h
token_renewable true
#…| Flag | Purpose |
|---|---|
-ttl |
Time-to-live; accepts durations like 15m, 24h, 720h |
Fields in the command output
| Field | Meaning |
|---|---|
token |
The credential to present on every request via BAO_TOKEN |
token_accessor |
Handle for auditing and management; can look up or revoke, cannot impersonate |
token_duration |
Remaining time-to-live before expiry |
token_renewable |
Whether the token can be renewed before it expires |
Lookup
Inspect a specific token to see its policies, remaining lifetime, and other attributes13:
bao token lookup s.t01ozKccP69Q1PxmLReQ3ept.MIr2KhKey Value
--- -----
#…
creation_ttl 24h
#…
num_uses 0
#…From a user’s perspective, five fields matter most:
| Field | Meaning |
|---|---|
creation_ttl |
Remaining time-to-live; decrements toward zero. When it reaches zero, the token expires and requests fail |
num_uses |
Remaining use count before invalidation; 0 means unlimited uses |
Renewal
Extend a renewable token’s life before it expires14:
bao token renew $tokenRenewal resets the remaining TTL but never exceeds the token’s max TTL. If the current remaining TTL plus the original TTL would surpass the max TTL, the result is capped at the max. Periodic tokens reset to their full TTL on each renewal.
Revocation
Revoke a token to immediately invalidate it and all of its children15:
bao token revoke $tokenUse the accessor instead of the token value when you want to revoke without possessing the credential itself:
bao token revoke-accessor $accessorRevoking a parent token cascades to all descendants. Orphan tokens have no parent, so they are only affected by direct revocation.
Authentication
Before OpenBao issues a token, it must verify who you are. Auth methods define how that verification happens16. Some methods delegate identity to an external system you already trust. Others keep everything self-contained within OpenBao.
Three categories cover the options:
- Platform-native — Infrastructure attests identity directly. No secrets to distribute.
- Kubernetes …pods present a service account token validated against the K8s API
- AWS / GCP / Azure …workloads use cloud IAM credentials from instance metadata
- External identity provider — OpenBao delegates to a system that already manages users.
- LDAP …binds against an LDAP server such as Active Directory or OpenLDAP
- RADIUS …challenges a RADIUS server for network infrastructure authentication
- Kerberos …validates a TGT against a Kerberos KDC for enterprise SSO
- JWT / OIDC …verifies a signed JWT from a trusted issuer, useful for CI/CD pipelines
- Self-contained — OpenBao handles identity itself.
- Certificate (TLS) …client certificate in mutual TLS, for environments with existing PKI
- Userpass …username and password managed within OpenBao, simplest for developer workstations
- AppRole …
role_idplussecret_idexchanged at login, for bare metal and VMs
Platform-native methods are preferred where available since they eliminate secret distribution entirely. External providers reuse identity you already manage. Self-contained methods work anywhere but require you to provision and protect credentials yourself.
Kubernetes
Kubernetes authentication eliminates secret distribution. A pod presents its service account token; OpenBao validates it against the Kubernetes API server and, if the token matches a configured role, issues a short-lived token17.
The service account token takes the form of a JWT18 (JSON Web Token): three Base64-encoded sections — header, payload, and signature — joined by dots. The header declares the signing algorithm. The payload carries claims, key-value pairs that identify the holder. The signature proves the issuer and guards against tampering.
OpenBao does not verify the JWT itself. Instead, it forwards the token to the Kubernetes TokenReview API19, which checks the signature and returns the pod’s identity — service account name, namespace, and groups. OpenBao then matches that identity against its configured roles. The exchange follows four steps:
- The pod sends its JWT to OpenBao
- OpenBao calls the TokenReview API using its own service account token
- The API server validates the JWT and returns the pod’s identity
- OpenBao matches the identity against a role and issues a token
OpenBao never trusts the pod. It defers to Kubernetes. Delete the service account or let the token expire, and authentication fails immediately.
Configure
Mount the auth method:
bao auth enable kubernetesPoint OpenBao at your Kubernetes cluster so it can verify tokens20:
bao write auth/kubernetes/config \
token_reviewer_jwt="$SERVICE_ACCOUNT_JWT" \
kubernetes_host="https://$KUBERNETES_API_HOST:443" \
kubernetes_ca_cert="@/path/to/ca.crt"| Parameter | Purpose |
|---|---|
token_reviewer_jwt |
Service account token OpenBao uses to call the K8s TokenReview API |
kubernetes_host |
HTTPS address of the Kubernetes API server |
kubernetes_ca_cert |
CA certificate that signed the API server’s TLS certificate |
The service account token belongs to a dedicated OpenBao service account with the token-reviewer ClusterRole bound to it. OpenBao needs this only to validate pod tokens, not to read secrets or manage resources.
Create Role
Define a role that maps Kubernetes identities to OpenBao policies21:
bao write auth/kubernetes/role/web-server \
bound_service_account_names=web-server \
bound_service_account_namespaces=production \
token_ttl=15m \
token_max_ttl=30m \
policies=read-secrets| Parameter | Effect |
|---|---|
bound_service_account_names |
Service account name the pod must use; * allows any |
bound_service_account_namespaces |
Namespace the pod must run in; * allows any |
token_ttl |
Lifetime of the token returned after a successful login |
token_max_ttl |
Upper bound; the token cannot be renewed beyond this |
policies |
Policies attached to the resulting token |
Tighten the binding by constraining both name and namespace. A pod in staging with the same service account name will be rejected if the role specifies production.
Login
Inside the pod, authenticate using the projected service account token22:
export BAO_TOKEN=$(\
bao write -format=json -field=token \
auth/kubernetes/login \
role=web-server \
jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token \
)| Argument | Purpose |
|---|---|
role=web-server |
Role name defined on the OpenBao side |
jwt=@... |
Path to the projected service account token inside the pod |
The token file at /var/run/secrets/kubernetes.io/serviceaccount/token is automatically mounted by Kubernetes when using projected service account tokens with a short TTL (e.g., 1 hour). OpenBao validates it against the API server. If the service account name and namespace match the role’s bounds, a token is returned.
No secrets are distributed to the pod. The platform proves who it is.
AppRole
AppRole splits identity into two halves and inserts a login step. One half is semi-public, the other is a constrained secret. Mount the auth method first23:
bao auth enable approleSuccess! Enabled approle auth method at: approle/Create
Create a role that defines what the resulting token can do:
bao write auth/approle/role/web-server \
secret_id_ttl=1h \
token_ttl=15m \
token_max_ttl=30m \
policies=read-secretsSuccess! Data written to: auth/approle/role/web-server| Parameter | Effect |
|---|---|
secret_id_ttl |
How long a secret_id remains valid before login is refused |
token_ttl |
Lifetime of the token returned after a successful login |
token_max_ttl |
Upper bound; the token cannot be renewed beyond this |
policies |
Policies attached to the resulting token |
List
List all configured AppRoles under the mount point24:
bao list auth/approle/role/Keys
----
web-serverThe command returns only role names, never credentials or token values. Your token must have the list capability on auth/approle/role/* — having read alone is not enough.
Read a specific role to inspect its constraints:
bao read auth/approle/role/web-serverKey Value
--- -----
bind_secret_id true
#…
policies [read-secrets~]
#…
secret_id_num_uses 0
secret_id_ttl 1h
#…
token_max_ttl 30m
#…
token_period 0s
#…
token_ttl 15m
#…| Field | Meaning |
|---|---|
bind_secret_id |
secret_id is required for login; disable for advanced workflows |
policies |
Policies attached at creation time (the ~ suffix indicates identity-derived policies) |
secret_id_num_uses |
How many times a secret_id can be used before expiring; 0 means unlimited |
secret_id_ttl |
Lifetime of a generated secret_id before it expires |
token_max_ttl |
Upper bound for the resulting token; renewal cannot extend beyond this |
token_period |
If non-zero, the token is periodic and resets to this value on each renewal |
token_ttl |
Initial lifetime of the token returned after login |
Role & Secret
Get the semi-public identifier — safe to embed in images or repositories:
bao read auth/approle/role/web-server/role-idKey Value
--- -----
role_id 3ef4698e-6875-5d2e-9f80-0c9d65f46a81Get the secret credential
bao write -f auth/approle/role/web-server/secret-idKey Value
--- -----
secret_id e39be171-63ba-cfd0-c444-043554a27b97
secret_id_accessor 02e07d94-ac16-0ba7-92b8-26d900920446
secret_id_num_uses 0
secret_id_ttl 1hThe secret_id is the actual credential and should be delivered separately — by your orchestrator, CI pipeline, or cloud-init — ideally response-wrapped so only the intended machine can unwrap it exactly once.
At boot, the machine exchanges both halves for a short-lived token:
export BAO_TOKEN=$(\
bao write -format=json -field=token \
auth/approle/login role_id=$role_id secret_id=$secret_id \
)Secrets Engines
A secrets engine is a pluggable module that defines how OpenBao manages data. Every action in OpenBao happens inside an engine, and each engine is mounted at its own path25.
Common engines26:
| Engine | Function |
|---|---|
| Cubbyhole | Per-token private secret storage; only the token that wrote it can read it |
| Databases | Dynamically generates short-lived credentials for supported databases |
| Identity | Internal store for entities, groups, and identity-based access control |
| Key/Value (KV) | Stores arbitrary static data such as passwords and API keys |
| Kubernetes | Generates Kubernetes service account tokens on demand |
| LDAP | Generates dynamic LDAP credentials |
| PKI | Acts as a certificate authority to issue and sign X.509 certificates |
| RabbitMQ | Generates dynamic RabbitMQ credentials |
| SSH | Signs or issues SSH certificates for dynamic host and user authentication |
| System | Internal endpoints for control, policy, and debugging |
| TOTP | Generates one-time passwords for multi-factor authentication |
| Transit | Provides encryption-as-a-service without storing your underlying data |
KV Store
A KV (Key-Value) store is a secrets engine for storing arbitrary static data such as passwords, API keys, or configuration values27. The current standard is KV v2, which tracks changes over time. Enable a KV v2 store at a custom path28:
bao secrets enable -path=secrets -version=2 kvSuccess! Enabled the kv secrets engine at: secrets/| Component | Purpose |
|---|---|
-path=secrets |
The mount point where the engine will be accessible |
-version=2 |
Explicitly requests KV version 2 |
kv |
The type of secrets engine being enabled |
Every write creates a new version, allowing you to inspect history or roll back to a previous state without losing data. Deletes are soft by default, meaning removed secrets can be recovered.
Write
In KV v2, every write creates a new version. You can inspect the version history or roll back to a previous version without losing data. Write a secret with kv put, paths follow the pattern <mount-path>/<key>:
bao kv put secrets/database username=admin password=s3cret==== Secret Path ====
secrets/data/database
======= Metadata =======
Key Value
--- -----
created_time 2026-07-07T06:43:44.146010062Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1| Component | Purpose |
|---|---|
secrets/database |
The mount point and key for the secret |
username=admin… |
Key-value pair to store; multiple pairs can be added space-separated |
Two things stand out in the output:
- Secret path …KV v2 stores actual data under a
/data/subpath (e.g.,secrets/data/database). Policies must explicitly grant access tosecrets/data/*, or reads and writes will fail with a403. - Metadata …OpenBao tracks the lifecycle of every secret. The
versionincrements on each write for rollbacks.deletion_timeshows when a soft delete occurred (n/ameans active). Ifdestroyedistrue, the version is permanently erased and cannot be recovered.
List
List secrets in a path29:
bao kv list secrets/----
databaseThe command returns only key names, never secret values or version information. It lists immediate children of the given path, so nested keys require an explicit prefix to traverse deeper. Your token must have the list capability in its policy — having read or create alone is not enough.
Read
Retrieve the current version of a secret with30:
bao kv get secrets/database#…
====== Data ======
Key Value
--- -----
password s3cret
username admin| Component | Purpose |
|---|---|
secrets/database |
The mount point and key to retrieve |
The output is divided into two sections. Data contains the actual key-value pairs you stored. A second Metadata section (omitted above for brevity) provides the version number, creation time, and deletion status.
# Read a specific previous version
bao kv get -version=1 secrets/database
# Output as JSON for programmatic parsing
bao kv get -format=json secrets/database
# Extract a single field directly
bao kv get -field=password secrets/databaseDelete
In KV v2, deleting a secret does not immediately erase it from storage. Instead, it performs a soft delete, which marks the latest version as deleted while preserving the history for potential recovery31.
# Soft delete the latest version (sets deletion_time, recoverable)
bao kv delete secrets/database
# Restore a specific deleted version back to active status
bao kv undelete -version=1 secrets/database
# Permanently erase versions 1 through N (sets destroyed to true)
bao kv destroy -versions=1-3 secrets/database
# Hard delete all versions at once (leaves no trace)
bao kv delete -delete-version=0 secrets/databaseSSH
The SSH secrets engine signs SSH public keys or generates one-time passwords for authenticating to remote machines32. Unlike static credentials, signed certificates expire automatically and can be revoked centrally.
Mount the SSH secrets engine at a custom path33:
bao secrets enable -path=ssh-signer sshSuccess! Enabled the ssh secrets engine at: ssh-signer/Separate mount paths are recommended for client and host key signing to isolate their CAs and roles.
Configure CA
Generate a signing key pair that OpenBao will use to sign certificates34:
bao write ssh-signer/config/ca generate_signing_key=trueKey Value
--- -----
#…
public_key ssh-rsa AAAAB3NzaC1yc2E...The public key must be trusted by target hosts so they accept signed certificates. Distribute it to each server’s SSH configuration:
curl -o /etc/ssh/trusted-user-ca-keys.pem \
$BAO_ADDR/v1/v.penso/ssh-signer/public_key| Argument | Purpose |
|---|---|
$BAO_ADDR |
OpenBao server address and port from your environment |
/v1/ |
API version prefix |
v.penso/ |
Namespace segment; required because raw HTTP calls don’t inherit BAO_NAMESPACE |
ssh-signer/ |
Mount path where the SSH secrets engine was enabled |
public_key |
Endpoint that returns the signing CA’s public key without authentication |
Then add the following to /etc/ssh/sshd_config on each host:
TrustedUserCAKeys /etc/ssh/trusted-user-ca-keys.pemRestart the SSH service after updating the configuration.
Create Role
Define a role that controls which users and extensions are allowed on signed certificates35:
bao write ssh-signer/roles/default -<<EOF
{
"allowed_users": "*",
"allow_user_certificates": true,
"default_extensions": {
"permit-pty": "",
"permit-port-forwarding": ""
},
"key_type": "ca",
"ttl": "30m"
}
EOFSuccess! Data written to: ssh-signer/roles/default| Field | Purpose |
|---|---|
allowed_users |
Usernames permitted in signed certificates; * allows any user |
ttl |
Certificate lifetime; shorter values reduce exposure if a key is compromised |
The default_extensions object controls capabilities granted to signed certificates:
| Extension | Effect |
|---|---|
permit-pty |
Interactive shell access |
permit-port-forwarding |
SSH tunneling and port forwarding |
permit-agent-forwarding |
SSH agent forwarding |
permit-X11-forwarding |
X11 display forwarding |
permit-user-rc |
Execution of ~/.ssh/rc on the server |
Sign Key
Generate a SSH key pair36:
ssh-keygen -t rsa -f /tmp/ssh-demo -N "" -C "demo@example.com"Generating public/private rsa key pair.
Your identification has been saved in /tmp/ssh-demo
Your public key has been saved in /tmp/ssh-demo.pub
#…| Flag | Purpose |
|---|---|
-t rsa |
Key algorithm; rsa is widely supported by older SSH servers |
-f /tmp/ssh-demo |
Output file path for the private key; public key gets a .pub suffix automatically |
-N "" |
Set an empty passphrase so SSH prompts are skipped during the demo |
-C "…" |
Free-form comment attached to the public key for identification |
Request OpenBao to sign the public key37:
# Redirect output to disk
bao write -field=signed_key ssh-signer/sign/default \
public_key=@/tmp/ssh-demo.pub > /tmp/ssh-demo-cert.pub| Argument | Purpose |
|---|---|
-field=signed_key |
Output only the certificate, discarding metadata like the serial number |
ssh-signer/sign/default |
Path to the signing endpoint; default is the role name |
public_key=@... |
The @ prefix tells OpenBao to read the key from a file instead of a raw string |
Verify an SSH certificate:
ssh-keygen -Lf /tmp/ssh-demo-cert.pub/tmp/ssh-demo-cert.pub:
#…
Valid: from 2026-07-07T10:27:48 to 2026-07-07T10:58:18
#…
Extensions:
permit-port-forwarding
permit-pty| Flag | Purpose |
|---|---|
-L |
Display detailed information about a public key or certificate |
-f |
Specify the input file path |
Relevant fields in the command output:
| Field | What to Check |
|---|---|
Valid |
Certificate time window; outside this range the host rejects it |
Extensions |
Granted capabilities; missing permit-pty means no interactive shell, missing permit-port-forwarding blocks tunnels |
Connect
SSH into the target host using both the signed certificate and the corresponding private key38:
ssh -i /tmp/ssh-demo-cert.pub -i /tmp/ssh-demo username@hostThe host validates the certificate signature against its trusted CA. If the principals match and the certificate has not expired, access is granted without password or static key exchange.
Appendix
Tokens vs AppRoles
When machines need to authenticate to OpenBao, two common approaches exist: a static token or the AppRole authentication method. Both result in the machine holding a token that reads from a KV store, but they differ fundamentally in how that token is obtained and what its lifecycle looks like.
| Dimension | Static Token | AppRole |
|---|---|---|
| Credential model | Single artifact; possession equals access | Two halves (role_id + secret_id) exchanged for a token at login |
| Rotation | Manual; requires revoking and redistributing everywhere | Automatic; rotate secret_ids through orchestrator without touching machines |
| Leakage risk | Stolen token works immediately from anywhere | secret_id can be constrained by TTL, use count, and CIDR; role_id alone is harmless |
| Token lifetime | Often drifts toward long-lived or non-expiring | Short-lived by design (e.g., 15m) with periodic re-login |
| Audit trail | Thin; no per-boot event, shared identity across deployments | Every boot produces a login event; per-machine granularity |
| Revocation | All-or-nothing; shared tokens affect all holders | Per-machine via secret_id accessor, or per-role to revoke everyone |
| Operational cost | Minimal setup, high maintenance over time | Higher initial setup, lower ongoing maintenance |
Rule of thumb: static token for throwaway or dev environments, AppRole when you have an orchestration layer capable of delivering constrained secret_ids, platform-native auth whenever the infrastructure provides it.
Static Token
An operator creates a token with the appropriate policies and distributes it to the machine via config file, environment variable, or systemd credential. The token is the credential. Whoever holds it has access — there is no login step, no second factor, nothing to constrain. It is the simplest approach and works fine for a lab or a single pet server. Treat it as a static API key.
Consider following drawbacks
- Rotation is manual and disruptive — Revoke the old token, create a new one, redistribute it everywhere. In practice these tokens drift toward long-lived or non-expiring.
- Leaks are immediately exploitable — A stolen token works from anywhere unless you explicitly set
token_bound_cidrsat creation. This flag restricts the token to requests originating from specific IP ranges (e.g.,-token-bound-cidr=10.0.0.0/24). If the token is used outside that range, OpenBao rejects it. Few people bother setting it because it requires stable client IPs and breaks if the machine’s address changes. - Audit trail is thin — You see requests from one token accessor, but every deployment of the same machine image reuses the identical identity with no per-boot login event.
- Renewal burden falls on the machine — The application must renew the token itself, or you accept expiry-and-outage, or you use a non-expiring token, which is worse.
AppRole
AppRole does not eliminate secret zero, it relocates it. Something trusted must still deliver the secret_id. If you take the lazy path — role_id and secret_id sitting in the same config file, secret_id_num_uses=0, generous TTLs — you have rebuilt a static token with more moving parts and gained nothing. The indirection only pays off if you actually enforce constraints and use a separate delivery path.
Why the indirection matters:
- Constrainable credentials — Set
secret_id_num_uses=1andsecret_id_bound_cidrson the role. A stolensecret_idmay already be consumed, expired, or unusable from the attacker’s network. A static token has none of these safety nets. - Short-lived tokens by design — A
15mtoken TTL with periodic re-login means a leaked token ages out fast. Compromise windows shrink from “until someone notices” to minutes. - Rotation without redistribution — Continuously rotate
secret_ids through your orchestrator. Therole_idnever changes, so machine images do not need rebuilding. - No single point of compromise — No single artifact at rest grants access. An attacker needs both the embedded
role_idand the delivery channel forsecret_id. With a static token, one file is the whole kingdom. - Per-machine audit and revocation — Every boot produces a login event. Revoke one machine’s
secret_idaccessor without affecting others. With shared static tokens, revocation is all-or-nothing.
When the environment offers native identity, skip AppRole entirely. Cloud providers and Kubernetes expose their own auth methods (aws, gcp, kubernetes, cert, jwt) where the platform attests identity directly and you distribute no secrets at all.
Footnotes
OpenBao GitHub Repository
https://github.com/openbao/openbao↩︎OpenBao Joins the OpenSSF
https://openbao.org/blog/openbao-joins-the-openssf/↩︎OpenBao Governance and Community Roles
https://github.com/openbao/openbao/blob/main/GOVERNANCE.md↩︎OpenBao GitHub Repository
https://github.com/openbao/openbao↩︎OpenBao GitHub Organization
https://github.com/openbao↩︎Nvidia Adopts OpenBao (TechTarget, Jun 2026)
https://www.techtarget.com/searchitoperations/news/366644831/Nvidia-adopts-OpenBao-open-source-fork-of-HashiCorps-Vault↩︎bao loginCommand Reference
https://openbao.org/docs/commands/login↩︎OpenBao Namespaces Documentation
https://openbao.org/docs/concepts/namespaces/↩︎OpenBao Roles Documentation
https://openbao.org/docs/concepts/roles/↩︎OpenBao Policies Documentation
https://openbao.org/docs/concepts/policies/↩︎OpenBao Tokens Documentation
https://openbao.org/docs/concepts/tokens/↩︎bao token createCommand Reference
https://openbao.org/docs/commands/token/create↩︎bao token lookupCommand Reference
https://openbao.org/docs/commands/token/lookup↩︎bao token renewCommand Reference
https://openbao.org/docs/commands/token/renew↩︎bao token revokeCommand Reference
https://openbao.org/docs/commands/token/revoke↩︎OpenBao Auth Methods Documentation
https://openbao.org/docs/auth/↩︎OpenBao Kubernetes Auth Documentation
https://openbao.org/docs/auth/kubernetes/↩︎JWT Specification (RFC 7519)
https://datatracker.ietf.org/doc/html/rfc7519↩︎Kubernetes TokenReview API Reference
https://kubernetes.io/docs/reference/access-authn-authz/authentication/#tokenreview↩︎bao writeCommand Reference
https://openbao.org/docs/commands/write/↩︎OpenBao Kubernetes Auth Roles
https://openbao.org/docs/auth/kubernetes/#roles↩︎OpenBao Kubernetes Auth Login
https://openbao.org/docs/auth/kubernetes/#login↩︎OpenBao AppRole Documentation
https://openbao.org/docs/auth/approle/↩︎bao listCommand Reference
https://openbao.org/docs/commands/list↩︎OpenBao Secrets Engines Documentation
https://openbao.org/docs/secrets/↩︎Full list of secrets engines
https://openbao.org/docs/secrets/↩︎OpenBao KV Secrets Engine Documentation
https://openbao.org/docs/secrets/kv/↩︎bao secrets enableCommand Reference
https://openbao.org/docs/commands/secrets/enable/↩︎bao kv listCommand Reference
https://openbao.org/docs/commands/kv/list↩︎bao kv getCommand Reference
https://openbao.org/docs/commands/kv/get↩︎bao kv deleteCommand Reference
https://openbao.org/docs/commands/kv/delete↩︎OpenBao SSH Secrets Engine Documentation
https://openbao.org/docs/secrets/ssh/↩︎bao secrets enableCommand Reference
https://openbao.org/docs/commands/secrets/enable/↩︎bao writeCommand Reference
https://openbao.org/docs/commands/write/↩︎Signed SSH Certificates Guide
https://openbao.org/docs/secrets/ssh/signed-ssh-certificates/↩︎ssh-keygenManual
https://man.openbsd.org/ssh-keygen↩︎bao sshSign Command Reference
https://openbao.org/docs/commands/ssh/sign↩︎OpenSSH Certificate Authentication
https://man.openbsd.org/ssh↩︎