
Security
OpenBao Migration: Three Gates Before You Move a Secret
The documented Vault-to-OpenBao path stops at Vault 1.14.x, Raft storage and Shamir unseal. Three gates decide which route your cluster is actually on.
The licence argument was settled years ago; the operational half is missing. Most write-ups repeat the guide's happy path — swap the binary, carry on — without reading the constraints at the top of that guide. Read as gates, they classify a cluster in an afternoon.
The Gates That Decide Whether You Can Migrate At All
Vault's LICENSE file names the Licensed Work as "Vault Version 1.15.0 or later" under the Business Source Licence, with IBM as Licensor since its acquisition of HashiCorp closed on 27 February 2025. The Additional Use Grant permits production use unless you offer the work to third parties, hosted or embedded, competing with IBM's paid versions. The OpenTofu migration made that case: the reason to move is governance, not compliance.
The triage is what nobody writes. OpenBao's migration guide opens with a constraints list — Vault 1.14.1 (OSS), OpenBao 2.2.0, Raft storage, Shamir unseal, no auto-unseal — that every summary treats as a footnote. It is a filter, splitting clusters three ways: the documented in-place upgrade, an API rebuild, and a prerequisites-first detour whose legs return you to the gate you failed.
Mechanical enough to script, before anyone estimates:
#!/usr/bin/env bash
# openbao-triage.sh — needs jq.
set -euo pipefail
read -r version storage seal < <(vault status -format=json |
jq -r '[.version, (.storage_type // "x"), (.type // "x")] | @tsv')
ver_lt() { # sort -V puts the lower first; equality excluded
[ "$1" != "$2" ] && [ "$1" = "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n1)" ]
}
# Gate one: three outcomes, not two.
if ver_lt "$version" "1.14.1"; then g1=prereq # upgrade Vault to 1.14.1
elif ver_lt "$version" "1.15.0"; then g1=pass
else g1=out # 1.15.0+ leaves the guide
fi
# Gate two: implemented at all (A), and in the guide (B: raft only).
case "$storage" in
raft) g2=pass ;; # ready, in the guide
postgresql) g2=prereq-not-guide ;; # ready, NOT in guide
file|inmem) g2=prereq-not-prod ;; # not production recommended
*) g2=prereq-not-impl ;; # no OpenBao implementation
esac # prereq-* = offline migration to Raft
# Gate three: anything but shamir owes a seal migration, cluster down.
if [ "$seal" = shamir ]; then g3=pass; else g3=prereq; fi
printf '%s=%s %s=%s %s=%s\n' "$version" "$g1" "$storage" "$g2" "$seal" "$g3"
if [ "$g1$g2$g3" = passpasspass ]; then
echo 'ROUTE: documented in-place rolling upgrade'
elif [ "$g1" = out ]; then
echo 'ROUTE: API replay rebuild'
else
echo 'ROUTE: prerequisites first, then re-triage'
fi
# Plugin-backed mounts surface here, not at cutover.
vault secrets list -format=json | jq -r 'to_entries[] | "\(.key)\t\(.value.type)"'Gate One: The Licence Line and the Support Line Are the Same Version
Every summary quotes the ceiling; none quotes the floor. The Limitations section carries both: "If your Vault version is lower than 1.14.1, upgrade to v1.14.1" and "This guide will not work on Vault versions 1.15.0 and newer; use at your own risk." Two-sided window, three outcomes.
- Below 1.14.1 — upgrade to 1.14.1 first, then re-enter the gates — a Vault-side project with its own change window.
- 1.14.1 through 1.14.x — continue to gate two. 1.14.0 sits below the floor.
- 1.15.0 or newer — the guide says it will not work, at your own risk. Outside the documented path, not a tested block.
1.15.0 is exactly where the BSL took effect. Staying MPL-clean and staying inside the documented migration were one decision: a team that upgraded past 1.15.0 for a feature accepted the BSL and closed the supported exit in the same change.
The guide was tested with OpenBao 2.2.0 and community-edition Vault only, and says other versions "will probably work fine." The current release is v2.6.1, published July 2026 — four minors ahead, so rehearse on a clone. It also warns that a Vault bootstrapped before 1.3 may need a rekey: OpenBao removed the older Shamir seal implementation.
Gate Two: “OpenBao Supports My Backend” Is Not the Same Question As “The Guide Covers It”
Check A — does OpenBao implement this backend at all? Counting implementation directories on 4 August 2026: OpenBao's internal/physical holds postgresql and raft plus a crosstest harness that is not a backend; its sdk/physical holds file and inmem. Four. Vault's physical holds twenty-one, its sdk/physical the same two. Twenty-three, four overlapping. Nineteen Vault backends — consul, dynamodb, etcd, gcs, s3, azure, mysql, mssql, cassandra, spanner, zookeeper and eight more — have none.
Of the four, only two are production options. OpenBao's storage documentation marks Integrated Storage and PostgreSQL "Production Ready: Yes". The filesystem backend is not recommended for production and has no high availability; the in-memory backend is "highly discouraged in production" because data is lost on restart.
Check B — does the documented in-place path cover it? Raft only; the guide describes no in-place procedure for anything else. The routing distinction: a PostgreSQL-backed Vault heads for a fully supported OpenBao backend and is still outside the in-place procedure. Everything non-Raft goes through a Vault-side storage migration to Raft, or replay.
Vault 1.14.x's operator migrate copies data between backends at the storage level with no decryption involved, and OpenBao documents the same command with storage_source and storage_destination stanzas. Its preconditions belong in the config file itself.
# vault operator migrate -config=vault-migrate.hcl
#
# Documented preconditions: offline (Vault will not start a server during a
# migration); destination NOT pre-initialised; destination keys OVERWRITTEN;
# source unmodified apart from a lock key. Snapshot both ends.
storage_source "consul" {
address = "127.0.0.1:8500"
path = "vault/"
}
storage_destination "raft" {
path = "/var/lib/vault/raft"
node_id = "vault-01"
}
cluster_addr = "https://vault-01.internal.example.com:8201"Gate Three: Shamir or Nothing — What Auto-Unseal Costs You
Read your seal stanza. awskms, azurekeyvault, gcpckms, transit and pkcs11 all sit outside the guide's Shamir-only constraint, and the route back is a documented seal migration, not a config edit.
OpenBao's seal concepts page is blunt: "The seal migration process cannot be performed without downtime … the process requires that you briefly take the whole cluster down." Both seals must be available throughout: the KMS behind auto-unseal stays reachable while you leave it. Vault's 1.14.x seal documentation states the same, and this runs Vault-side, before OpenBao is installed.
The sequence is per-node: add disabled = "true" to the old seal block on a standby, bring it up, and unseal with -migrate, supplying the recovery keys, not the barrier shares, because the old seal is an auto seal. Repeat standby by standby, step the active down so a migrated standby takes over, convert the old active last. The stanza below is the disabled old-seal half only.
# Auto-unsealed cluster returning to Shamir: the OLD seal block stays, marked
# disabled, and the KMS must stay reachable throughout.
seal "awskms" {
disabled = "true"
region = "eu-central-1"
kms_key_id = "REPLACE_WITH_KEY_ID"
}The trade is a security one. Auto-unseal exists because Shamir shares are a custody problem: five officers, five envelopes, a quorum at 3 a.m. Returning to Shamir re-establishes that custody — a control change with an owner and an audit trail. Shamir is a constraint of the crossing, not a posture: once OpenBao runs, move back to auto-unseal.
The In-Place Path, Step by Step — and What It Silently Assumes
For the one qualifying combination — 1.14.1–1.14.x, Raft, Shamir — the guide gives a genuine rolling procedure, and the order is what summaries lose. Convert every follower first, one at a time, while the Vault leader keeps serving; then step the leader down and convert it last, joining the newly elected leader. Reversing that loses quorum on a half-converted cluster.
#!/usr/bin/env bash
# Followers first, leader LAST — both DERIVED from raft, never hardcoded.
# Per node beforehand: /etc/openbao/openbao.hcl with a listener and a raft
# stanza on a FRESH path, its own new node_id, no disable_mlock.
# Export VAULT_TOKEN and BAO_TOKEN — an ssh shell carries neither.
set -euo pipefail
D=internal.example.com
peers="$(vault operator raft list-peers -format=json)"
leader_id="$(jq -r '.data.config.servers[]|select(.state=="leader")|.node_id' <<<"$peers")"
mapfile -t followers < <(jq -r '.data.config.servers[]|select(.state!="leader")|.node_id' <<<"$peers")
leader_addr="$(vault status -format=json | jq -r '.leader_address')"
# Raft counts PEERS, not processes: a new id leaves the old entry still voting.
# Evict it once the replacement is a voter, or quorum climbs until you lose it.
convert_node() {
local old_id="$1" new_id="$2" join_addr="$3" host="$1.$D"
ssh "$host" 'sudo systemctl stop vault'
ssh "$host" 'sudo chown openbao:openbao /var/log/vault/audit.log 2>/dev/null || true'
ssh "$host" 'sudo systemctl start openbao'
ssh "$host" "BAO_ADDR=https://127.0.0.1:8200 bao operator raft join ${join_addr}"
# Shamir is multi-share: one unseal takes ONE share, exits 0 at 1/N. Loop
# until sealed=false — a sealed node never becomes a voter. A TTY per share.
until ssh "$host" 'BAO_ADDR=https://127.0.0.1:8200 bao status -format=json' |
jq -e '.sealed == false' >/dev/null; do
ssh -t "$host" 'BAO_ADDR=https://127.0.0.1:8200 bao operator unseal'
done
# Poll from here, authenticated — not over ssh. Voter before the next node.
for _ in $(seq 60); do
if BAO_ADDR="${join_addr}" bao operator raft list-peers -format=json |
jq -e --arg id "$new_id" \
'.data.config.servers[]|select(.node_id==$id)|.voter==true' >/dev/null; then
BAO_ADDR="${join_addr}" bao operator raft remove-peer "$old_id"
BAO_ADDR="${join_addr}" bao operator raft list-peers # == live
return 0
fi
sleep 5
done
echo "STOP: ${new_id} not a voter after 5m — do not convert the next node" >&2
exit 1
}
for old_id in "${followers[@]}"; do
convert_node "$old_id" "bao-${old_id#vault-}" "$leader_addr"
done
# Step down the ACTIVE node by address — never a VIP, never a standby.
VAULT_ADDR="$leader_addr" vault operator step-down
new_leader="" # wait out the election
until [ -n "$new_leader" ] && [ "$new_leader" != "$leader_addr" ]; do
sleep 5
new_leader="$(BAO_ADDR="https://${followers[0]}.$D:8200" \
bao status -format=json | jq -r '.leader_address')"
done
convert_node "$leader_id" "bao-${leader_id#vault-}" "$new_leader" # leader lastThe Voter=true poll is not an Autopilot task; the guide polls list-peers. OpenBao's integrated storage documentation explains why: "Recently joined nodes are accepted as non-voters initially until they are in sync with matching Raft index and only after reaching a stability threshold are they then full voting members." Do not tune the threshold down to shorten the wait — a documented route to cluster instability.
Five things sit below the procedure:
- Token format changes, and it reaches every consumer. OpenBao issues
[sbr].<random>; Vault issued{hvs,hvb,hvr}.<long_random>. Old tokens last their TTLs, new ones take the new shape, so prefix or length validation breaks. mlockis conditional. Removedisable_mlock*if you have it*: OpenBao has not usedmlocksince 2.0.0, per an accepted design RFC. A config without the key needs nothing.- The audit file needs a new owner. With the
audit filebackend in use, the log must become writable by the OpenBao user after Vault stops on that node. - Use a different storage path. Joining nodes pull data from the cluster, so a fresh path costs nothing and eases rollback.
- Plugins are the ambush. "OpenBao comes without many plugins by default," and the guide assumes none are in use. A mount backed by a plugin OpenBao lacks needs its workaround at triage, not on the night.
The consumer-side payoff is the strongest thing the fork has: OpenBao states its API "should be compatible with Vault to the extent that existing clients should not even register a difference," some needing a restart. For a Kubernetes estate on External Secrets Operator over Vault, the migration arrives as a hostname and a CA.
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore # cluster-scoped: no metadata.namespace
metadata:
name: sovereign-secrets
spec:
provider:
# Still the vault provider — OpenBao speaks the same API.
vault:
server: "https://bao.internal.example.com:8200" # was vault.internal…
path: "kv"
version: "v2"
caProvider:
type: ConfigMap
name: openbao-ca # was vault-ca
key: ca.crt
# caProvider.namespace and serviceAccountRef.namespace are honoured
# ONLY on a ClusterSecretStore; on a namespaced SecretStore, drop both.
namespace: external-secrets
auth:
kubernetes:
mountPath: "kubernetes"
role: "platform-reader"
serviceAccountRef:
name: external-secrets
namespace: external-secretsWhen In-Place Is Off the Table: The Replay Migration
Clusters that fail a gate and will not pay for the prerequisite legs have one other option, undocumented because neither project owns it: rebuild the secret plane through the API. An engineering pattern, not a supported runbook — say so to whoever signs it off; its failure modes are yours.
The shape is a dual-write window. Consumers write through one abstraction targeting both clusters; a one-way replay job enumerates policies, auth mounts and KV data and re-creates them in OpenBao; a verification pass reads every path back before the write path widens. Only reads move at cutover. Without dual-write, rollback means replaying backwards under incident pressure.
#!/usr/bin/env bash
# EXCERPT — enumerate-and-recreate skeleton only. Per-auth-method
# re-configuration is elided: each method takes its own arguments.
set -euo pipefail
: "${VAULT_ADDR:?}" "${BAO_ADDR:?}"
# 1. Policies port cleanly: HCL text, compatible policy API.
for p in $(vault policy list | grep -vE '^(root|default)$'); do
vault policy read "$p" | bao policy write "$p" -
done
# 2. KV mounts: same path, same version. A type OpenBao lacks stops here.
vault secrets list -format=json |
jq -r 'to_entries[] | select(.value.type == "kv")
| "\(.key)\t\(.value.options.version // "1")"' |
while IFS=$'\t' read -r path version; do
bao secrets enable -path="${path%/}" -version="$version" kv
done
# 3. KV data: current values only.
# ... (excerpt: per auth method, its own re-configuration; per dynamic
# engine, its own credentials)Name the non-portables first; they turn a replay into an outage. Leases and dynamic credentials do not cross: every database, PKI and cloud credential is bound to Vault's lease store, so let them expire and re-issue against OpenBao. KV version history does not cross unless you replay every version; if compliance depends on it, answer that before cutover. Plugin-backed mounts have no counterpart where OpenBao lacks the plugin. The rest is re-creatable: the same workload-identity plumbing authenticates clients to either.
What Each Side Charges For: Replication Versus Namespaces
Price the trade in both directions or the business case is dishonest. Vault Enterprise's replication is the real loss: DR secondaries share the primary's configuration, policy and secrets, and DR "is designed to be a mechanism to protect against catastrophic failure of entire clusters." OpenBao documents no comparable first-party feature — an absence we assert from reading its docs. Two things get mistaken for it: `retry_join_as_non_voter` adds a node outside the quorum that still receives the replication stream; snapshots are backup. Neither is a promotable cross-region secondary, so replacing DR — snapshot shipping, a tested restore, a documented recovery time — is engineering work, not licence saving.
The inverse is real. Vault's namespaces and secure multi-tenancy require "appropriate Vault Enterprise license or HCP Vault Dedicated cluster." OpenBao documents namespaces supporting "secure multi-tenancy (SMT) within a single OpenBao instance with tenant isolation and administration delegation" as a core feature, with no licence gate stated — price and availability, not bit-for-bit parity. Read scalability lands the same way: added in v2.5.0, standbys serving reads, Raft only.
Failure Modes and the Rollback You Rehearse Before You Need It
Name the specific ways this breaks, then rehearse against them:
- A consumer that parses token shape. Silent at cutover, loud at the next issuance. Grep every client for
hvs.prefix and length assumptions. - A plugin discovered at cutover. Triage lists every mount type; one with no OpenBao counterpart does not migrate here.
- A node that never reaches
Voter=true. Stop; do not convert the next one. Converting faster than nodes rejoin is one way to lose the cluster; the poll is the throttle. - A stale peer left in the raft configuration. The quieter way to lose it. Joining under a new id does not evict the old entry, so quorum climbs against you: two corpses make five peers needing three votes; stopping the third leaves two alive. Run
remove-peerafter eachVoter=true, then checklist-peersmatches your live nodes. - An audit device OpenBao cannot write. A blocked audit path blocks requests. Fix ownership right after stopping Vault.
Rollback is cheap if you built for it. Give OpenBao its own storage path and Vault's directory is untouched: the old cluster stays sealed but intact. Snapshot Raft first and verify the restore into a scratch cluster; an unrestored snapshot is hope, not a backup. Keep it through a soak window — same discipline as running your own internal PKI: an exit ramp only counts once driven.
Writing Exit Criteria So the Next Relicensing Is a Scheduled Task
The lesson is not about secrets managers. A licence change found clusters already ineligible for the escape route — not through the licence, but ordinary drift in version, storage backend and seal type. Write the eligibility criteria down as standing controls and test them on a schedule, the way you test a restore.
- A version ceiling with an owner. "We do not cross Vault 1.15.0 without a documented decision" is enforceable by anyone; crossing it accepts a licence and closes an exit at once.
- A storage backend chosen for portability, and a seal type reviewed against the exit path, with key custody rehearsed often enough that returning to Shamir is procedure, not archaeology.
- A rehearsed replay job, run quarterly against a scratch OpenBao instance. The only honest answer to "how long would it take us?"
- An annual re-read of the governance documents. Licence parameters and Licensor identity change quietly — same discipline as identity providers and every dependency you cannot rewrite in a quarter.
None of that is expensive: a quarterly hour and a policy line, against a migration costing a maintenance window per prerequisite leg and a custody ceremony you may no longer have. The cost is never in the exit — it is in discovering you had already disqualified yourself from taking it.
§FAQ/Common questions
Frequently asked
Can I migrate from Vault to OpenBao if I am running Vault 1.16 or newer?
Not on the documented in-place path. OpenBao's migration guide states in its Limitations section that the guide will not work on Vault versions 1.15.0 and newer and that using it there is at your own risk. It describes no supported alternative there, so treat this as outside the documented path, not a tested hard block. The same version line is where HashiCorp's Business Source Licence takes effect — the LICENSE file names the Licensed Work as Vault version 1.15.0 or later — so the licence boundary and the supported-migration boundary sit on the same number. A cluster on 1.16 or newer has two realistic routes: rebuild the secret plane through the API against a fresh OpenBao cluster, or take the in-place path on a separate cluster still inside the window — at least 1.14.1 and below 1.15.0. No supported downgrade puts a running cluster back inside that window, so do not plan for one.
My Vault cluster uses PostgreSQL storage. Does OpenBao support it?
Yes, and that still does not put you on the in-place path — two different questions, and conflating them is the most common triage error. OpenBao's storage documentation lists PostgreSQL alongside Integrated Storage as production ready, so a PostgreSQL cluster heads for a fully supported OpenBao backend. But the migration guide's constraint list specifies Raft, and documents no in-place procedure for any other backend: a PostgreSQL-backed Vault passes the implementation check and fails the coverage check. Its options are to run vault operator migrate to Raft on the Vault side first — offline, with a destination that must not be pre-initialised and whose existing keys are overwritten — then re-enter the gates, or to rebuild through the API. Nineteen other Vault backends, including Consul, DynamoDB, etcd, S3 and GCS, have no OpenBao implementation at all.
Do my applications break when I switch from Vault to OpenBao?
Mostly no, with one specific exception the guide names. OpenBao states its API should be compatible with Vault to the extent that existing clients should not even register a difference, though some clients or services may have to be restarted. In practice a Kubernetes estate using External Secrets Operator sees a hostname change and a CA change; the provider block stays vault. The exception is token format. OpenBao issues tokens shaped [sbr].<random> where Vault issues much longer {hvs,hvb,hvr}.<long_random> tokens. Existing tokens continue to be accepted for their TTLs, but every newly issued token takes the new form. Anything that validates a token by prefix, by length, or with a regular expression written against the Vault format will fail after cutover rather than during it — which is why the warning to consumers belongs before the first node is touched, not in the post-migration notes.
How much downtime does a Vault-to-OpenBao migration need?
It depends on which gates you pass, which is the point of triaging first. The documented in-place path — Vault at 1.14.1 or above and below 1.15.0, Raft, Shamir — is a rolling upgrade: followers convert one at a time while the Vault leader keeps serving, and only once every follower runs OpenBao is the leader stepped down and converted last. That is a rolling change, not an outage, provided each converted node shows Voter true in bao operator raft list-peers before you touch the next. The prerequisite legs are different. A storage migration with operator migrate is explicitly offline — Vault will not start a server while one runs. A seal migration from auto-unseal back to Shamir is documented as impossible without downtime, briefly taking the whole cluster down with both the old and new seals reachable throughout. A cluster owing two prerequisite legs owes two maintenance windows before the rolling part begins.
What do I lose by moving off Vault Enterprise to OpenBao?
The clearest loss is replication. Vault Enterprise disaster-recovery and performance replication are licensed features, with DR secondaries sharing the primary's configuration, policy and supporting secrets and designed to protect against catastrophic failure of entire clusters. OpenBao's documentation contains no comparable first-party feature — an absence read out of its docs, not something OpenBao states. Two capabilities get mistaken for it: Raft non-voters, whose documented purpose is to receive the data replication stream and add read scalability without joining the quorum, and snapshots, which are backup. If DR replication is load-bearing in your resilience story, budget the replacement as engineering work. The trade runs the other way too: namespaces and secure multi-tenancy require a Vault Enterprise licence or HCP Vault Dedicated cluster, while OpenBao documents both as core, with no licence gate stated in its docs — a difference in price and availability, not bit-for-bit parity.
Further reading
- Kubernetes secrets management is still broken: ESO over Vault
- OpenTofu migration: the registry is the hard part
- Sovereign internal PKI with step-ca and cert-manager
- Zero-trust workload identity with SPIFFE/SPIRE
- Keycloak, Authentik or Zitadel: the self-hosted Okta exit
- Digital sovereignty: from policy slogan to testable architecture
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.