Product documentation
OpenBao raft HA + OTC KMS auto-unseal (production, gated)
The gated, default-off chart scaffold for the managed-OTC production OpenBao posture — integrated raft storage plus OTC/T-Cloud KMS auto-unseal via the tcloudpublic seal plugin — and the careful ops steps it deliberately leaves for a separately staging-validated cutover: bundling the plugin into the OpenBao image, the seal/unseal/raft-standby-join/seal-migration validation gate, and the Transit seal-OpenBao fallback.
OpenBao raft HA + OTC KMS auto-unseal (production, gated)
This page documents the gated, default-off chart scaffold added under SSO-2182
for Thoryn's own managed-OTC production high-availability posture, and — just as
importantly — the careful operational steps the scaffold deliberately does not
perform. Per the accepted design ADRs (adrs/2026-07-25-managed-otc-production-ha.md
and adrs/2026-07-24-production-high-availability.md), OpenBao is the one stateful
tier that stays in-cluster even on managed OTC CCE — there is no managed-Transit
equivalent — running integrated raft for its own HA and delegating only the
unseal to OTC / T Cloud KMS via the first-party tcloudpublic seal plugin.
Security-critical — this is the unseal mechanism. Flipping any flag below changes how the platform's cryptographic custody layer boots. The scaffold ships inert: with
vault.raft.enabledandvault.seal.tcloudpublic.enabledbothfalse(the defaults) the rendered OpenBao config is byte-identical to the current SSO-1466 file-backend + Shamir / K8s-Secret unseal path. The live cutover is a separately staging-validated ops step, not a chart flip. Do not enable these in the staging or self-managed defaults.
What the scaffold renders (and what it does not)
Rendered by the chart today (gated, default off)
Flag (values.yaml) | Default | On effect |
|---|---|---|
vault.raft.enabled | false | storage "file" becomes storage "raft" (integrated storage) with a retry_join peer list derived from vault.raft.replicas. Config stanza only. |
vault.seal.tcloudpublic.enabled | false | Renders the seal "tcloudpublic" stanza (non-secret fields only) + injects the KMS credentials into the OpenBao pod as env vars from a pre-created Secret + renders the allow-egress-openbao-kms NetworkPolicy. |
vault.seal.tcloudpublic.credentialsSecret | "" | Name of a pre-created K8s Secret with keys key_id / access_key / secret_key. required (fail-loud) when the seal is enabled. |
vault.seal.tcloudpublic.region | eu-de | Non-secret, rendered into the seal stanza. |
vault.raft.replicas | 3 | Raft quorum size — drives the retry_join peer list today; becomes the StatefulSet replica count at the validated cutover. |
The credential values never live in a values file or the config.hcl ConfigMap.
The Secret is created out-of-band (--set-string from a protected environment, or a
sealed-secret) and the three fields are injected into the OpenBao pod as env vars
only. A missing Secret name aborts helm template / helm upgrade at render time —
there is no operator-readable unseal key on the KMS path, so a missing credential
means a pod that can never auto-unseal, and failing loud at render is the correct
posture (the CLAUDE.md fail-loud-secrets rule).
Deliberately NOT performed by this scaffold
These are the careful parts — each lands under the separately staging-validated cutover, not this PR:
- The OpenBao image is not rebuilt. The
tcloudpublicplugin must be bundled + registered in the image before the seal works (see below). The chart still references the stock digest-pinnedopenbao/openbaoimage. - The Deployment is not converted to a StatefulSet. Raft HA needs stable
per-pod identity, a headless peer Service, and per-pod PVCs. The scaffold renders
only the
storage "raft"stanza; the workload stays a single-replica Deployment (a 3-replica Deployment on oneReadWriteOncePVC would not schedule, which is whyvault.raft.replicasdoes not drive the Deployment replica count here). - No seal-migration or raft-migration is run. Migrating a live file-backend + Shamir store to raft + KMS auto-unseal is an ordered, forward-only, rehearsed operation validated live as the real service identity — never a chart flip.
- The unseal orchestration is unchanged. On the KMS path the per-pod
openbao-unsealsidecar and theoperator init-generated unseal key inopenbao-init-job.yamlare replaced by KMS auto-unseal (recovery keys, not an unseal key). That rewrite is part of the validated cutover.
(i) Bundle the kms-tcloudpublic plugin into the OpenBao image
The tcloudpublic seal is an external plugin (kms-tcloudpublic, shipped in
openbao/openbao-plugins); the stock openbao/openbao binary does not carry it.
Two version facts from the ADR gate this:
- The
tcloudpublicseal requires OpenBao >= v2.6.0 (auto-unseal became pluggable in that release). The chart is pinned toopenbao/openbao:2.5.4today, so the version bump is part of this work. - In v2.7.0 the built-in cloud KMS seals (
awskms/azurekeyvault/gcpckms/ocikms/alicloudkms/pkcs11) become external-plugin-only, whiletransitstays built-in — relevant to the fallback below.
Build a thin image FROM openbao/openbao:<version >= 2.6.0> that adds the plugin
binary into the plugin directory and registers it, then digest-pin it the same way
the base image is pinned (scripts/check-infra-image-digests.sh enforces the
@sha256: suffix). Sketch:
FROM openbao/openbao:2.6.x@sha256:<pin>
# Copy the tcloudpublic seal plugin into the plugin directory and register it in
# the image's plugin catalog. The exact plugin filename, the plugin_directory,
# and the registration command are confirmed against openbao/openbao-plugins
# during the staging validation (the plugin is v0.0.1).
COPY kms-tcloudpublic /openbao/plugins/kms-tcloudpublic
# plugin_directory = "/openbao/plugins" in config.hcl, or `bao plugin register`.Maturity caveat. The plugin is v0.0.1 (upstream marks it experimental). Treat every mechanism here — the env-var names the plugin reads for
key_id/access_key/secret_key, the exact registration command, the config field names — as confirmed by the staging validation gate, not assumed. The env-var names the chart injects (BAO_SEAL_TCLOUDPUBLIC_KEY_ID/_ACCESS_KEY/_SECRET_KEY) are a documented placeholder shape; adjust them to whatever the plugin build actually reads.
(ii) The staging validation gate (the gating deliverable)
Before any production use, run this validation on OTC CCE as the real service identity (the CLAUDE.md validate-live-before-cutover rule). This is the deliverable that gates the launch — not "the chart renders" and not "CI is green".
- Seal reachability. Deploy the plugin-bundled image with
vault.seal.tcloudpublic.enabled=trueand a realcredentialsSecret. Confirm the OpenBao pod's egress reaches OTC KMS (theallow-egress-openbao-kmsNetworkPolicy allows it when the egress baseline is on). - Fresh-init auto-unseal. On a fresh store,
bao operator initmust auto-unseal via KMS and return recovery keys (not an unseal key). Confirm the pod comes up unsealed with no per-pod unseal sidecar. - Restart auto-unseal. Delete the pod; confirm it restarts and auto-unseals without manual intervention — this is the mechanism that removes the SSO-1724 sealed-cascade class of outage.
- Raft standby join. Bring up the raft replicas (StatefulSet), confirm a
follower joins quorum via
retry_join, and confirm abao operator raft snapshotsucceeds (re-enabling the native snapshot backup the file backend could not take —vault-snapshot-cronjob.yaml). - Seal migration. Rehearse the forward-only migration from the current
file-backend + Shamir store to raft + KMS auto-unseal (
bao operator unseal -migrate/ the seal-rewrap flow), keeping the file PVC as a rollback anchor. - Exercise the real Transit operations as each consuming service identity
(
kubectl create token <sa>thenbao loginthen a real sign / verify / encrypt round-trip) — proving the key material survived the migration, per the ADR.
Only when steps 1-6 pass against the real deployed system is the production cutover authorized.
(iii) Fallback — Transit seal-OpenBao (mature, always available)
If the v0.0.1 tcloudpublic plugin proves unready at the launch date, fall back to
Transit auto-unseal via a small dedicated "seal-OpenBao" that holds the cluster's
unseal key. The transit seal stays built-in through OpenBao v2.7.0, so this
path needs no external plugin. It still removes the operator-readable-unseal-key
custody risk and the SSO-2129 restore-ordering exposure (the AES-256-GCM at-rest key
ordering constraint is unchanged). A PKCS#11 seal against an OTC Dedicated HSM
(Data Encryption Workshop) is a possible future hardware-backed compliance path, but
its GA on T Cloud Public is unconfirmed, so it is not the launch choice.
What this posture buys (per the ADRs)
- No SPOF / no key loss — raft replicates the Transit key material across quorum members, so a single pod/node loss neither seals the platform nor loses keys, and it re-enables the native raft-snapshot backup that the file backend could not take.
- No sealed-cascade — KMS auto-unseal removes the manual-unseal failure mode that kept the store sealed for 6h in the SSO-1724 staging outage; a restarted pod auto-unseals.
- No operator-readable unseal key — the unseal is delegated to OTC KMS, so there
is no
thoryn-openbao-keys-style unseal Secret to custody or to restore in the right order (the SSO-2129 restore-ordering risk is reduced; the four AES-256-GCM at-rest keys still have their ordering constraint).
Related
adrs/2026-07-25-managed-otc-production-ha.md— the ratified managed-OTC posture and the "Resolved — OTC KMS auto-unseal compatibility" spike result.adrs/2026-07-24-production-high-availability.md— the self-managed in-cluster HA design (raft 3-node + KMS/HSM or transit auto-unseal) this scaffold shares.adrs/2026-06-07-vault-to-openbao.md— the OpenBao seal/unseal + Transit custody boundary this extends.docs/modules/ROOT/pages/operator/openbao-cutover.adoc— the current file-backend cutover + unseal-key recover/rotate runbook (the posture this replaces in production).