OpenBao
- OpenBao
Giving secrets one home with an audit trail
OpenBao is an open-source secrets management system. It stores secrets encrypted at rest, enforces access policies, issues time-limited dynamic credentials, and logs every access with a full audit trail.
OpenBao is the Linux Foundation fork of HashiCorp Vault, created in 2023 after Vault moved from the open-source MPL to the Business Source License (BSL). It is API- and CLI-compatible with Vault — the commands below are the same ones you would run against Vault, with bao in place of vault — so the concepts transfer directly. This course standardizes on OpenBao for the same reason it uses OpenTofu instead of Terraform: both are the OSI-licensed forks of tools HashiCorp relicensed.
The core problem OpenBao solves: secrets have to live somewhere. .env files get committed. Environment variables leak through process listings and crash logs. Shared credentials mean no accountability. OpenBao centralizes secrets with controlled access and an audit log that answers “who read the database password and when.”
ℹ️ OpenBao is only as strong as how you handle the unseal keys and root token.
bao operator initprints them once. In this lab a text file is acceptable; in production the key shares belong in separate custody (HSMs, distinct people) and the root token is revoked after initial setup. Anyone holding a threshold of unseal keys can decrypt everything.
OpenBao also shows up in the pipeline context — see CI/CD, secret management, and GitOps for how services pull secrets during a deploy.
How OpenBao differs from HashiCorp Vault
“API-compatible fork” undersells what has happened since 2023 — the projects are actively diverging, in both directions. Knowing the differences matters when you read Vault documentation (most of it still applies), interview at a shop running one or the other, or advise on a migration.
Origin and governance
OpenBao forked from Vault 1.14, the last MPL-2.0 release, after HashiCorp relicensed Vault under the Business Source License (BSL 1.1) in August 2023. The BSL is source-available but not open source: its “Additional Use Grant” forbids offering Vault as a competing hosted service. The fork now develops under the Linux Foundation (currently the OpenSSF, after starting in LF Edge). Meanwhile IBM closed its acquisition of HashiCorp in February 2025, and Vault 2.0 (April 2026) moved the product onto IBM’s versioning and support lifecycle — so today the comparison is really community-governed fork vs. IBM enterprise product.
What’s the same
- The security model: seal/unseal, Shamir key shares, policies, leases, audit devices.
- The API surface and CLI verbs —
bao kv putisvault kv put;hvacand most client libraries work against either. - Core secrets engines (KV, PKI, database, transit, SSH) and auth methods (token, AppRole, userpass, JWT/OIDC, Kubernetes, LDAP).
- Configuration format — the
openbao.hclabove is a validvault.hclapart from paths.
Where they diverge
| Area | OpenBao (MPL-2.0, Linux Foundation) | HashiCorp/IBM Vault (BSL 1.1) |
|---|---|---|
| Namespaces (multi-tenancy) | Free — GA since 2.3.1 (June 2025) | Enterprise-only |
| HSM auto-unseal (PKCS#11) | Free — since 2.2 | Enterprise-only |
| Standby read scaling | Free — HA standbys serve reads since 2.5 | Enterprise (“performance standbys”) |
| Cross-cluster replication (DR/performance) | Not available | Enterprise — the biggest gap |
| Advanced policy language | CEL (Common Expression Language) roles | Sentinel (Enterprise) |
| FIPS-validated builds, KMIP, Secrets Sync | Not available | Enterprise/HCP |
| Storage backends | Integrated Raft or PostgreSQL only — Consul, DynamoDB, S3, etcd, and the other legacy backends were deliberately removed | Long list retained |
| Cloud plugins (AWS/Azure/GCP auth & secrets) | External plugins, installed separately | Bundled |
| Managed service | None — self-host | HCP Vault Dedicated |
Two patterns worth noticing in that table. First, OpenBao’s headline additions — namespaces, HSM unseal, read scaling — are precisely features Vault reserves for Enterprise; the fork competes by open-sourcing the paywall. Second, OpenBao’s removals (legacy storage backends, the SSPL-licensed MongoDB plugin) are a security posture: fewer code paths, only OSI-licensed dependencies, Raft or Postgres so every deployment looks alike. Smaller attack surface is a feature.
⚠️ Migration is only supported one way and from one place: OpenBao documents in-place migration from Vault 1.14.x specifically. A Vault cluster that has been upgraded past that (or uses Enterprise features like replication) has no clean path across, and OpenBao data does not migrate back to Vault. Treat the two as compatible dialects that drift further apart every release — check both changelogs before assuming a Vault feature exists in OpenBao or vice versa.
For this course none of the gaps matter: everything in the labs (KV, policies, AppRole, audit devices) sits in the shared core. The skills transfer to a Vault shop unchanged.
Concepts
| Term | Definition |
|---|---|
| Secrets Engine | A plugin that stores or generates secrets (KV, database, PKI, AWS, etc.) |
| Auth Method | How clients authenticate to OpenBao (token, AppRole, TLS cert, etc.) |
| Policy | HCL rules that grant or deny capabilities on specific paths |
| Token | A bearer credential issued after authentication; scoped to policies |
| Lease | A time-to-live attached to dynamic secrets; must be renewed or the credential expires |
| Audit Device | A log sink (file, syslog) that records every OpenBao request |
Installation and Setup as a systemd Service
Install
OpenBao ships as a .deb release asset. The Ansible playbook installs it for you; to do it by hand:
# Download and install the OpenBao .deb (already done by Ansible)
VER=2.5.5 # current release — see github.com/openbao/openbao/releases
curl -fsSLO "https://github.com/openbao/openbao/releases/download/v${VER}/bao_${VER}_linux_amd64.deb"
sudo apt install "./bao_${VER}_linux_amd64.deb"
bao version
Configuration file
# /etc/openbao/openbao.hcl
ui = true
storage "file" {
path = "/opt/openbao/data"
}
listener "tcp" {
address = "127.0.0.1:8200"
tls_disable = true # TLS disabled for lab; enable in production
}
api_addr = "http://127.0.0.1:8200"
# Create storage directory
sudo mkdir -p /opt/openbao/data
sudo chown openbao:openbao /opt/openbao/data
# Enable and start the systemd unit (installed with the package)
sudo systemctl enable --now openbao
systemctl status openbao
Initialize and unseal
OpenBao starts in a sealed state — it holds encrypted data but cannot decrypt it until enough unseal keys are provided. Initialization generates the master key and splits it using Shamir’s Secret Sharing.
# The bao CLI reads BAO_ADDR (and still accepts the legacy VAULT_ADDR).
export BAO_ADDR='http://127.0.0.1:8200'
# Initialize: generates 5 key shares, requires 3 to unseal
bao operator init -key-shares=5 -key-threshold=3
# Output contains:
# Unseal Key 1: abc...
# Unseal Key 2: def...
# ...
# Initial Root Token: <root-token>
#
# Store these somewhere safe. In a lab, a text file is fine.
# In production, these go into separate HSMs or key management systems.
# Unseal with 3 of the 5 keys
bao operator unseal <key-1>
bao operator unseal <key-2>
bao operator unseal <key-3>
# Check status — Sealed: false means ready
bao status
Authenticate
export BAO_TOKEN='<root-token>' # root token from init output
# Verify
bao token lookup
KV Secrets Engine
The Key-Value (KV) engine stores arbitrary secrets as key-value pairs. Version 2 (KV-v2) keeps a history of every version.
# Enable KV-v2 at the path "secret/"
bao secrets enable -path=secret kv-v2
# Write a secret
bao kv put secret/dvwa/db \
username=dvwauser \
password=ChangeMeNow
# Read the secret
bao kv get secret/dvwa/db
# Read just one field
bao kv get -field=password secret/dvwa/db
# List all secrets at a path
bao kv list secret/dvwa
# Update a field without overwriting others
bao kv patch secret/dvwa/db password=NewPassword
# View version history
bao kv metadata get secret/dvwa/db
# Read a specific version
bao kv get -version=1 secret/dvwa/db
Policies
Policies grant the minimum capabilities needed — nothing more. A policy for a service that only reads its own secrets should not be able to list all secrets or write to other paths.
Capability reference
| Capability | What it allows |
|---|---|
create |
Create new secrets at a path |
read |
Read secret values |
update |
Overwrite existing secrets |
delete |
Delete secrets |
list |
List secret names (not values) |
patch |
Update individual fields |
Write a read-only policy
# policy/dvwa-read.hcl — allows reading DVWA's own secrets only
path "secret/data/dvwa/*" {
capabilities = ["read"]
}
path "secret/metadata/dvwa/*" {
capabilities = ["list"]
}
bao policy write dvwa-read policy/dvwa-read.hcl
# Verify
bao policy read dvwa-read
bao policy list
Create a token scoped to a policy
# Create a token valid for 24 hours, tied to dvwa-read policy
bao token create \
-policy=dvwa-read \
-ttl=24h \
-display-name=dvwa-app
# Output:
# token <scoped-token>
# token_policies dvwa-read
# token_ttl 24h
# Test the token — this should work
BAO_TOKEN=<scoped-token> bao kv get secret/dvwa/db
# This should fail (no write capability)
BAO_TOKEN=<scoped-token> bao kv put secret/dvwa/db password=hacked
AppRole Authentication
Token-based auth works for humans. Services that need to authenticate programmatically use AppRole: a role with a Role ID (like a username) and a Secret ID (like a one-time password).
# Enable AppRole auth method
bao auth enable approle
# Create a role tied to the dvwa-read policy
bao write auth/approle/role/dvwa-role \
token_policies=dvwa-read \
token_ttl=1h \
token_max_ttl=4h \
secret_id_ttl=24h
# Get the Role ID (static, safe to store in config)
bao read auth/approle/role/dvwa-role/role-id
# Generate a Secret ID (single-use, short-lived)
bao write -f auth/approle/role/dvwa-role/secret-id
# Authenticate using both
bao write auth/approle/login \
role_id=<role-id> \
secret_id=<secret-id>
# Returns a token scoped to dvwa-read
The “secret zero” problem — where does the Secret ID come from?
AppRole splits the credential in two on purpose: the Role ID is static and non-sensitive (bake it into the config, image, or an environment variable — alone it is useless), while the Secret ID is the sensitive half. That raises the obvious question: if the app needs a Secret ID to fetch its secrets, how does the Secret ID itself reach the app securely? Storing it in a file next to the code just moves the problem — that file is now the secret you have to protect. This is the secret zero (or secure introduction) problem: the first credential in the chain, the one that unlocks all the others.
The answer is that the Secret ID is delivered at deploy time by something the workload already trusts, and held only in memory long enough to log in — never committed, never parked at rest. Two standard patterns:
-
Response wrapping (the general-purpose answer). Whatever provisions the workload — your orchestrator, deploy job, or the Ansible run — asks OpenBao to wrap the Secret ID instead of returning it in the clear:
# Provisioner (trusted) mints a single-use wrapping token, valid 120s bao write -wrap-ttl=120s -f auth/approle/role/dvwa-role/secret-id # Output includes: # wrapping_token: <one-time-use token> # wrapping_token_ttl: 2m0s # Only the wrapping token is handed to the app. The app unwraps it once # to retrieve the real Secret ID, then logs in as usual: BAO_TOKEN=<wrapping-token> bao unwrapThe wrapping token is single-use and short-lived: the app unwraps it exactly once, and any interception is tamper-evident — if an attacker unwraps it first, the legitimate app’s unwrap fails and the deploy loudly breaks instead of silently leaking. The plaintext Secret ID never crosses the wire.
-
Platform identity (better still, when available). On Kubernetes, the pod’s service-account JWT is the credential — use the
kubernetesauth method and there is no Secret ID to ship at all. The same holds for cloud instance identity (aws/gcp/azureauth): the workload proves who it is with an identity the platform already issued, and OpenBao trusts that platform. This dissolves secret zero rather than protecting it.
⚠️ Never generate the Secret ID inside the service and store it long-term, and never commit one — that recreates the very problem AppRole exists to solve. The design goal running through the whole chain is that no long-lived, directly-usable credential sits at rest on the app host: the Role ID is useless alone, the Secret ID is single-use and short-lived, the resulting token expires in an hour, and (with the
databaseengine) even the fetched credential can be per-session.
Python Integration with hvac
hvac is the Vault HTTP API client for Python. Because OpenBao speaks the same API, hvac works against it unchanged — just point it at the OpenBao address. Installed by the Ansible playbook.
Read a static secret
import hvac
import os
client = hvac.Client(
url=os.environ.get('BAO_ADDR', 'http://127.0.0.1:8200'),
token=os.environ['BAO_TOKEN'],
)
assert client.is_authenticated(), "OpenBao authentication failed"
secret = client.secrets.kv.v2.read_secret_version(
path='dvwa/db',
mount_point='secret',
)
db_user = secret['data']['data']['username']
db_password = secret['data']['data']['password']
print(f"Connecting as {db_user}")
Authenticate with AppRole
import hvac
import os
client = hvac.Client(url=os.environ.get('BAO_ADDR', 'http://127.0.0.1:8200'))
resp = client.auth.approle.login(
role_id=os.environ['BAO_ROLE_ID'],
secret_id=os.environ['BAO_SECRET_ID'],
)
# Token is now set automatically
assert client.is_authenticated()
secret = client.secrets.kv.v2.read_secret_version(
path='dvwa/db',
mount_point='secret',
)
db_password = secret['data']['data']['password']
Complete get-secret script
#!/usr/bin/env python3
# scripts/get-secret.py
# Usage: BAO_ADDR=http://127.0.0.1:8200 BAO_TOKEN=<token> \
# python3 get-secret.py secret/dvwa/db password
import hvac
import os
import sys
def get_secret_field(path: str, field: str) -> str:
client = hvac.Client(
url=os.environ.get('BAO_ADDR', 'http://127.0.0.1:8200'),
token=os.environ['BAO_TOKEN'],
)
if not client.is_authenticated():
raise RuntimeError("OpenBao authentication failed")
# path is e.g. "secret/dvwa/db" — split mount from secret path
parts = path.split('/', 1)
mount, secret_path = parts[0], parts[1]
response = client.secrets.kv.v2.read_secret_version(
path=secret_path,
mount_point=mount,
)
return response['data']['data'][field]
if __name__ == '__main__':
if len(sys.argv) != 3:
print(f"Usage: {sys.argv[0]} <path> <field>", file=sys.stderr)
sys.exit(1)
value = get_secret_field(sys.argv[1], sys.argv[2])
print(value)
Audit Logging
OpenBao’s audit device logs every request and response. Enable it early — the audit log is what you query during an incident to answer “did anything read this secret?”
# Enable file audit (logs to /var/log/openbao/audit.log)
sudo mkdir -p /var/log/openbao
sudo chown openbao:openbao /var/log/openbao
bao audit enable file file_path=/var/log/openbao/audit.log
# Verify
bao audit list
# View recent entries (values are HMAC'd — not stored in plaintext)
tail -f /var/log/openbao/audit.log | jq .
Eliminating Plaintext Credentials
After setting up OpenBao, verify no credentials exist anywhere in plaintext on the system:
# Search for common patterns in config files
grep -rn --include="*.conf" --include="*.yml" --include="*.yaml" \
-E '(password|passwd|secret|api_key)\s*[:=]\s*\S+' /etc /opt 2>/dev/null \
| grep -v '^Binary' \
| grep -v '# ' # skip comments
# Search for credentials in environment variables
cat /proc/*/environ 2>/dev/null \
| tr '\0' '\n' \
| grep -iE '(password|secret|token|key)='
# Search git history for committed secrets
git log -p --all | grep -iE '(password|secret|token|api.key)\s*[:=]'
Any credential that appears in these searches needs to be rotated and removed.
Key takeaways
- OpenBao is the OSI-licensed (MPL-2.0) Linux Foundation fork of HashiCorp Vault — API/CLI compatible, so everything here applies to Vault too, with
baoin place ofvault. - The projects are diverging: OpenBao open-sources formerly Enterprise-only features (namespaces, HSM auto-unseal, standby reads) and removes legacy code paths; Vault (an IBM product since 2025) keeps replication, Sentinel, and FIPS builds behind the Enterprise license.
- It centralizes secrets with encryption at rest, per-path access policies, and an audit log that answers “who read this and when.”
- It boots sealed: initialization splits the master key with Shamir’s Secret Sharing, and a threshold of unseal keys is required before OpenBao can decrypt anything.
- Write policies for least privilege — a service that only reads its own secrets gets
read/liston its own paths and nothing else. - Humans use tokens; services use AppRole (a static Role ID plus a short-lived, single-use Secret ID) so credentials can rotate without redeploying.
- Enable a file audit device early and verify no plaintext credentials remain in configs,
/proc/*/environ, or git history.
References
- OpenBao Documentation — https://openbao.org/docs/
- OpenBao releases (
.debassets) — https://github.com/openbao/openbao/releases - OpenBao CHANGELOG — feature/version history for the fork — https://github.com/openbao/openbao/blob/main/CHANGELOG.md
- Jorijn Schrijvershof — HashiCorp Vault vs OpenBao: a thorough comparison for platform teams — https://jorijn.com/en/blog/hashicorp-vault-vs-openbao/
- InfoQ — HashiCorp Vault 2.0 Marks Shift to IBM Lifecycle — https://www.infoq.com/news/2026/04/vault-2-0-ibm-identity/
- hvac Python client — https://hvac.readthedocs.io/
- OpenBao AppRole auth method — https://openbao.org/docs/auth/approle/
- OpenBao KV secrets engine v2 — https://openbao.org/docs/secrets/kv/kv-v2/
- NIST SP 800-57 — Recommendation for Key Management — https://csrc.nist.gov/publications/detail/sp/800/57pt1/rev5/final
Related course pages: CI/CD & DevOps · Container security · DevSecOps fundamentals
🛠️ Maintenance note: OpenBao is the OpenBao Project (Linux Foundation) fork of HashiCorp Vault, adopted here because Vault moved to the BSL license in 2023 (the same reason this course uses OpenTofu over Terraform). The install pins a release tag (
openbao_versionin the Ansiblegroup_vars, currently 2.5.5) — bump it from the releases page each term. The CLI isbao; it readsBAO_ADDR/BAO_TOKENbut still honors the legacyVAULT_ADDR/VAULT_TOKEN. Because OpenBao tracks Vault’s API,hvacworks unchanged — re-check itsread_secret_versionargument names against the installed version.