OpenBao Secrets Management: A Production Tutorial (2026)
OpenBao secrets management gives platform teams a fully open-source way to store, rotate, and broker credentials without licence friction. It is the Linux Foundation fork of HashiCorp Vault, built after Vault relicensed to the Business Source Licence. This tutorial walks through a production-grade setup using the bao CLI. You will install and initialise a server, unseal it, wire up auth methods and policies, issue dynamic database credentials, inject secrets into Kubernetes, and harden the cluster for high availability. Every command is real and Vault-compatible, so existing Vault muscle memory carries over directly.
By the end you will have a working mental model and a copy-paste runbook. We assume a Linux host, basic familiarity with HCL, and a Kubernetes cluster for the injection section.
Why does this matter now? Hardcoded credentials remain one of the most common root causes in breach reports year after year. Consider the usual suspects. A static password sits in a config file. A database credential gets checked into a repository. An API key is shared over a chat thread. Each is a standing liability that an attacker can use the moment they find it. A secrets broker like OpenBao changes the economics. Secrets become short-lived, scoped, audited, and revocable in seconds. Even when a credential leaks, its blast radius and lifetime are small. That is the shift this tutorial is really about, and OpenBao is the open-source way to make it happen without a licensing question hanging over your platform.
Context: why OpenBao exists
In August 2023 HashiCorp moved Vault and most of its product line from the Mozilla Public Licence (MPL-2.0) to the Business Source Licence (BSL). The BSL is source-available, not open source. It restricts production use that competes with HashiCorp. That change unsettled vendors, managed-service providers, and enterprises with strict open-source-only policies.
The community response was OpenBao. It is a fork of the last MPL-2.0 Vault codebase, donated to the Linux Foundation and now governed as an LF project. OpenBao keeps the MPL-2.0 licence, an open governance model, and a public technical steering committee. You can read the project charter and roadmap on openbao.org. The source lives at github.com/openbao/openbao, and the project sits inside the broader cloud-native ecosystem alongside CNCF graduated projects.
The practical upshot: OpenBao is Vault without the licence asterisk. The CLI binary is bao, the API paths mirror Vault, and the concepts are identical. If your team already runs Vault, migration is mostly a binary swap plus a storage decision. We cover the gotchas later.
Why does the licence matter so much for a secrets tool specifically? Secrets management sits at the centre of every other system you run. It is the dependency your databases, your CI pipelines, your service mesh, and your applications all reach through. Committing that much critical infrastructure to a source-available licence creates a long-tail compliance risk. Auditors ask about it, procurement teams flag it, and some regulated industries forbid BSL software outright. An MPL-2.0 fork under neutral foundation governance removes that uncertainty. You can vendor the code, patch it, redistribute internally, and embed it in a product without negotiating a commercial exception.
There is also a supply-chain argument. A Linux Foundation project has a documented governance process, a public technical steering committee, and contribution rules that prevent any single vendor from unilaterally changing direction. For infrastructure you intend to run for a decade, that predictability is worth as much as any single feature. OpenBao inherited Vault’s mature codebase and battle-tested cryptographic barrier, then placed it under that governance umbrella. The result is a tool that feels familiar to every Vault operator while removing the licensing question that prompted the fork in the first place.
Architecture overview
OpenBao runs as a single server binary that brokers every secret request. Clients never touch storage directly. They authenticate, receive a token, and that token is checked against policies before any secrets engine responds.

The core components, shown in figure 1, are these.
- Storage backend. Encrypted data at rest. The recommended choice is Raft integrated storage, which replicates state across server nodes with no external dependency. Older backends like Consul still work but add operational surface.
- The seal. OpenBao encrypts everything behind a barrier. The barrier key is itself encrypted by a root key. At rest the root key is sealed. On startup the server is sealed and cannot serve requests until unsealed.
- Auth methods. Pluggable login paths. Token, AppRole, Kubernetes, and JWT/OIDC are common. Each method maps an identity to a set of policies.
- Secrets engines. Mounted at paths. KV v2 stores static secrets. The database engine mints dynamic credentials. PKI issues certificates. Transit does encryption-as-a-service without storing data.
- Policy engine. HCL policies grant capabilities on paths. Default-deny applies everywhere.
Two secrets engines deserve special mention because they often get overlooked by teams who arrive only for database credentials. The transit engine turns OpenBao into encryption-as-a-service. Your application sends plaintext, OpenBao returns ciphertext, and the key never leaves the server. This solves application-layer encryption without scattering key material across services, and it supports key rotation and re-wrapping so you can roll keys without re-encrypting every record by hand. The PKI engine issues short-lived X.509 certificates on demand, which is the natural complement to a zero-trust network where every service needs a rotating identity certificate. Between KV v2 for static config, database for dynamic credentials, transit for encryption, and PKI for certificates, a single OpenBao deployment can replace four separate point solutions.
This separation is the heart of OpenBao secrets management. Identity, authorisation, and secret generation are distinct layers you compose per workload.
It helps to trace a single request through these layers. A pod starts and presents its ServiceAccount token to the Kubernetes auth method. The auth method validates that token against the Kubernetes API and, if the binding matches, issues a client token carrying one or more policies. The pod then asks for a secret at database/creds/app-role. Before the database engine runs, the policy engine checks whether the client token holds the read capability on that exact path. Only if the check passes does the engine generate a credential. The storage backend never appears in this path from the client’s perspective; it only persists the engine’s internal state and the lease metadata. Every layer is independently auditable, which is what makes the design defensible to a security review.
The barrier deserves a closer look because it underpins everything else. All data written to storage passes through an encryption barrier using AES-256-GCM. The barrier key is wrapped by the root key, and the root key is what the seal protects. This is why a stolen storage volume or a leaked Raft snapshot is useless on its own: without the root key, the ciphertext is opaque. The seal is therefore the single most important control in the entire system, and the rest of this tutorial treats it accordingly.
Getting started: install, init, and unseal
Download the binary from the releases page, or use your distribution’s package. The commands below assume bao is on your PATH.
# Verify the install
bao version
# OpenBao v2.x
# Create a minimal server config (Raft storage, single node to start)
cat > /etc/openbao/config.hcl <<'EOF'
storage "raft" {
path = "/opt/openbao/data"
node_id = "node-1"
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = false
tls_cert_file = "/etc/openbao/tls/tls.crt"
tls_key_file = "/etc/openbao/tls/tls.key"
}
api_addr = "https://10.0.0.11:8200"
cluster_addr = "https://10.0.0.11:8201"
ui = true
EOF
# Start the server (run under systemd in production)
bao server -config=/etc/openbao/config.hcl
In a second shell, point the CLI at the server and initialise it. Initialisation generates the root key, splits it into Shamir key shares, and returns an initial root token.
export BAO_ADDR='https://10.0.0.11:8200'
export BAO_CACERT='/etc/openbao/tls/ca.crt'
# Initialise with 5 key shares, 3 required to unseal
bao operator init -key-shares=5 -key-threshold=3
The output prints five unseal keys and one root token. Distribute the unseal keys to separate custodians. Never store them together. The server starts sealed, so unseal it by supplying the threshold number of shares.
# Run three times with three different shares
bao operator unseal # paste share 1
bao operator unseal # paste share 2
bao operator unseal # paste share 3
# Authenticate with the root token (temporarily)
export BAO_TOKEN='<initial-root-token>'
bao status
bao status should now report Sealed: false. The Shamir scheme means no single custodian can unseal the cluster alone. For production we replace manual unseal with auto-unseal, covered later.
A few details matter here. The key-shares and key-threshold values are a trust decision, not a default to accept blindly. Five shares with a threshold of three is a common balance. It tolerates losing two shares and still requires collusion of three custodians to unseal. Pick numbers that fit your custody model, not the demo defaults. Initialisation happens exactly once per cluster, and the unseal keys it prints are never shown again. If you lose them before configuring auto-unseal, you lose the cluster.
Note also that unsealing is separate from authenticating. Unsealing makes the server able to decrypt its own storage. Authenticating proves who you are so policies can apply. A freshly unsealed server still refuses every request until you present a valid token. That two-step model trips up newcomers who expect unsealing to grant access. It does not. Keep the distinction clear in your runbooks, because operators under pressure during an incident frequently confuse a sealed server with an authentication failure, and the remediation for each is completely different.
A short troubleshooting checklist saves time during the first install. If bao status reports a connection error, confirm BAO_ADDR uses https and that BAO_CACERT points at the CA that signed the listener certificate. A common mistake is leaving BAO_ADDR at the default http://127.0.0.1:8200 while the server listens on TLS. If bao operator init returns “already initialized”, the storage path already holds a cluster; you cannot reinitialise without destroying that data. If unseal progress resets unexpectedly, you are likely pasting shares that belong to a different initialisation. And if the server starts but immediately seals again after a restart, you have not yet configured auto-unseal, which is the expected behaviour for a Shamir-sealed cluster.
For day-to-day operation, set up a non-root admin path early. Enable the userpass or OIDC auth method for human operators, attach an admin policy scoped to the mounts they manage, and stop using the root token entirely. Human access through OIDC also gives you single sign-on and a clean audit trail tied to real identities rather than a shared secret. The discipline of never typing the root token outside break-glass is one of the highest-leverage habits a team can build.
Auth methods and policies
A root token can do anything, which is exactly why you stop using it after setup. Real workloads authenticate through scoped auth methods and receive tokens bound to least-privilege policies.

Figure 2 shows the flow. A client logs in, receives a token, and every subsequent request is checked against the attached policy before a secrets engine responds.
OpenBao supports many auth methods because different identities need different proofs. A human operator might log in through OIDC against your identity provider. A CI pipeline might use a JWT issued by the CI system. A pod uses its Kubernetes ServiceAccount. A standalone VM uses AppRole or cloud IAM. Each method’s job is the same: take an external proof of identity and exchange it for an OpenBao token carrying the right policies. Choosing the right method per workload is the foundation of the whole model, because once you trust an identity you can stop distributing static credentials to it entirely.
Start by writing a policy. Policies are HCL that grant capabilities on paths.
# app-policy.hcl
# Read-only access to one KV path and database creds
path "secret/data/app/*" {
capabilities = ["read"]
}
path "database/creds/app-role" {
capabilities = ["read"]
}
Load the policy, then enable AppRole for machine-to-machine logins.
# Write the policy
bao policy write app-policy app-policy.hcl
# Enable and configure AppRole
bao auth enable approle
bao write auth/approle/role/app \
token_policies="app-policy" \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=24h
# Fetch the RoleID (stable) and a SecretID (sensitive, short-lived)
bao read auth/approle/role/app/role-id
bao write -f auth/approle/role/app/secret-id
The workload logs in with the RoleID and SecretID pair and gets a token scoped to app-policy.
bao write auth/approle/login \
role_id="<role-id>" \
secret_id="<secret-id>"
The split between a stable RoleID and a short-lived SecretID is deliberate. The RoleID is not sensitive; it merely names the role. The SecretID is the credential, and it should be delivered just-in-time. The recommended pattern is response wrapping: an orchestrator requests a SecretID with bao write -wrap-ttl=120s -f auth/approle/role/app/secret-id, which returns a single-use wrapping token instead of the SecretID itself. The orchestrator hands that wrapping token to the workload, which unwraps it exactly once to retrieve the real SecretID. If anyone intercepts and unwraps the token first, the legitimate workload’s unwrap fails and you get an immediate, loud signal of compromise. This is how you deliver secret zero without ever exposing it in plaintext logs or environment dumps.
For Kubernetes workloads, enable the Kubernetes auth method so pods authenticate with their ServiceAccount token instead of a SecretID.
bao auth enable kubernetes
bao write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
bao write auth/kubernetes/role/app \
bound_service_account_names="app-sa" \
bound_service_account_namespaces="prod" \
token_policies="app-policy" \
token_ttl=1h
Now a pod running as app-sa in prod can log in with no static credential. That is the secret-zero problem solved by the platform’s own identity.
Policies repay careful design. The capabilities you grant are create, read, update, delete, list, sudo, and deny. Default-deny means a token can do nothing until a policy explicitly allows a path. Prefer many small policies composed onto a role over one broad policy. A reporting service that only reads should never hold create on a secrets path. When you find yourself reaching for a wildcard at the top of a path, stop and ask whether you are encoding a real requirement or papering over unclear ownership.
Two patterns prevent most policy mistakes. First, scope paths to the narrowest prefix that works, so secret/data/app/* rather than secret/data/*. Second, separate read paths from write paths into distinct policies and attach them deliberately. AppRole then becomes a clean composition point: one role gets the read policy, another gets read plus rotate. The same discipline applies to the Kubernetes role bindings. Bind to specific ServiceAccount names and namespaces, never to a wildcard, so a compromised pod in one namespace cannot assume another team’s identity. This least-privilege posture is the difference between an audit that passes and one that generates a page of findings.
Dynamic secrets with the database engine
Static database passwords are a liability. They leak, they get shared, and rotating them breaks things. Dynamic secrets fix this. OpenBao generates a unique, short-lived database user on demand, then deletes it when the lease expires.
This is the feature that most justifies running a secrets broker at all. With static credentials, every service holds the same long-lived password, rotation is a coordinated outage, and an audit cannot tell you which service used a credential at a given moment. With dynamic secrets, every service instance gets its own unique credential, the audit log attributes every database connection to a specific lease, and rotation is automatic because credentials expire by default. The phrase “dynamic secrets 2026” shows up in so many platform roadmaps precisely because the pattern has matured from a nice-to-have into an expected baseline for any team serious about credential hygiene.

Figure 3 traces the lifecycle. Enable the database engine and configure a connection to PostgreSQL.
bao secrets enable database
# Configure the connection (use a dedicated rotation user)
bao write database/config/app-postgres \
plugin_name="postgresql-database-plugin" \
allowed_roles="app-role" \
connection_url="postgresql://{{username}}:{{password}}@db.prod:5432/appdb?sslmode=require" \
username="bao_admin" \
password="<initial-admin-password>"
# Immediately rotate the admin password so no human knows it
bao write -f database/rotate-root/app-postgres
Rotating the root credential is important. After that command, only OpenBao knows the admin password. Next, define a role describing the ephemeral user.
bao write database/roles/app-role \
db_name="app-postgres" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
Now any client with the policy can request fresh credentials.
“`bash
bao read database/creds/app-role
