Add OpenBao Transit-engine CA key provider#89
Open
bootc wants to merge 6 commits into
Open
Conversation
bootc
force-pushed
the
feature/openbao-transit-signer
branch
from
July 4, 2026 19:25
49c3809 to
86f3145
Compare
Adds --ca-key-provider openbao, delegating the CA private key to an OpenBao Transit secrets engine key instead of a local PEM file: the key never exists inside any openvox-ca process. Supports AppRole, static token file, and Kubernetes auth (native, no sidecar), with proactive token renewal and reactive re-authentication on a 403 so a revoked or expired token self-heals rather than requiring a restart. internal/ca gains a KeyProvider interface alongside the existing ExternalSigner, wired into bootstrapCA/loadCA; the frontend process and signer RPC protocol are unchanged. Every existing storage backend keeps working unmodified -- OpenBao only ever supplants key custody. The OpenBao integration lives under internal/signer/openbao, alongside the existing local-key isolation signer; all OpenBao-specific config is grouped under its own top-level "openbao" YAML key / --openbao-* flags rather than a flat prefix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds compose-backends-openbao.yml + mage test:backendsOpenBao, mirroring the existing Postgres/MySQL/Redis-Go backend targets: brings up a throwaway OpenBao dev server (2.5.5 in CI), configures its transit engine and a least-privilege-scoped AppRole via the same SDK internal/signer/openbao uses, runs the new openbao_integration Go suite, then tears it down. Wired into CI as backends-openbao alongside the other backend jobs. The suite (internal/signer/openbao/openbao_integration_test.go) signs a digest through a real Transit key and verifies it, and exercises the create-key-on-first-boot convenience path -- complementing the existing httptest-based unit tests, which fake the wire protocol. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
openvox-ca already refused to start if the CA certificate's public key didn't match the loaded key (internal/ca's loadCA, covering local keys and OpenBao alike); this adds the live counterpart for OpenBao: a CA.KeyVerifier interface, implemented by internal/signer/openbao's Signer, re-fetches the Transit key's current public component and compares it against what was loaded at startup before every certificate issuance. A key rotated directly at OpenBao (bao write -f transit/keys/<name>/rotate) while openvox-ca is already running is caught at the next issuance -- refusing the request -- instead of silently producing a certificate that fails verification later. The isolated-signer RPC protocol (internal/signer) gains a matching VerifyCurrentKey method so this works whether or not key isolation is in use: RemoteSigner forwards the check to the signer process, which is the one actually holding the OpenBao-backed key. Keys that can't rotate independently of the process (a local PEM file) don't implement the interface, so the check is a no-op for them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The `token` auth method authenticated by returning the raw lookup-self secret to the LifetimeWatcher. A lookup-self response carries the token's lifetime in its Data map, not in the auth/lease envelope the watcher renews on, so the watcher saw an empty LeaseID and no Auth block, treated the token as un-renewable, and fired DoneCh the instant it started. run() then re-authenticated (another lookup-self) and started a fresh watcher that again ended immediately -- a tight loop of lookup-self calls and "renewal ended" warnings hammering OpenBao, with no actual renewal. Only the token method was affected; AppRole/Kubernetes return a proper auth secret from their /login exchange. Re-shape the lookup-self result into an auth secret carrying the real TTL and renewable flag, so the watcher renews a renewable token and, for a non-renewable one, waits until near expiry before re-reading the file. Add a minReauthInterval floor in run() as a backstop so any watcher that still ends immediately (e.g. a non-renewable token with no expiry, TTL 0) settles into a steady re-auth cadence rather than a busy loop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…back ca_key_provider was only ever compared against "openbao" (UsesOpenBao); any other value, including a typo like "openba" or the anticipated-but- unimplemented "pkcs11", silently fell through to local-file key custody. For a setting whose whole purpose is to keep the CA private key off local disk, that failure mode writes the key to disk exactly when the operator asked for it to live elsewhere. Add CAKeyProviderConfig.Validate, accepting only "", "file", and "openbao", and call it during server startup validation so an unrecognised provider is a hard error. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the pre-issuance KeyVerifier check — and all of its plumbing —
with the verification x509.CreateCertificate already performs. Before every
issuance the frontend used to call VerifyCurrentKey, which under key
isolation was an extra RPC to the signer that re-fetched the Transit key's
public component from OpenBao and compared it to the value cached at load:
two sequential OpenBao round trips per issuance (verify then sign), held
under the CA mutex, plus a redundant RPC on every local-file issuance since
RemoteSigner unconditionally advertised the capability.
CreateCertificate already guards against exactly this, unconditionally and
in-process: it refuses if the signer's public key doesn't equal the parent
CA certificate's, and it re-verifies the signature the signer returned
against that public key before returning it. loadCA has pinned
c.CAKey.Public() to the CA certificate, so a Transit key rotated at OpenBao
out from under a running CA — cached Public() still matches the CA cert,
but OpenBao now signs with the new key — produces a signature that fails
that re-verification, and issuance errors instead of emitting a certificate
no verifier could validate. No extra round trip: the one Sign that issuance
already makes is the only OpenBao call.
Removes ca.KeyVerifier and its call site; signer.{VerifyKeyRequest,
VerifyKeyResponse,KeyVerifier}, Service.VerifyCurrentKey and
RemoteSigner.VerifyCurrentKey; and openbao.Signer.VerifyCurrentKey, along
with their tests. On the way past, openbao.Signer's inline 403 self-heal for
Sign (token revoked out-of-band → re-authenticate and retry once) is
factored into a small withReauth helper.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bootc
force-pushed
the
feature/openbao-transit-signer
branch
from
July 18, 2026 00:27
5ebb935 to
9d02e8d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an alternative CA key-custody mode backed by OpenBao's Transit secrets engine, so the CA private key never has to exist on disk (or in process memory) at all — openvox-ca only ever sends a digest to Bao for signing.
internal/signer/openbaopackage: OpenBao API v2 client, AppRole/static-token-file/Kubernetes auth methods, background token lease renewal (TokenManager, built onNewLifetimeWatcher) with reactive re-authentication on 403, and acrypto.Signer/ca.KeyProviderimplementation backed by Transitsign/keys.secret_id, projected SA JWT) are re-read fresh on every login rather than cached at startup.openvox-ca [signer]child process, same trust boundary as a local private key today; the frontend keeps talking to it over the existing RPC with no changes.ca.KeyProviderinterface generalises key custody (local PEM blob vs. Bao-backed), and a new optionalca.KeyVerifierinterface lets a key implementation assert it still matches its source of truth:RemoteSigner→ signer process), not just in single-process mode.--openbao-*flags /openbao.*config keys /PUPPET_CA_OPENBAO_*env vars,docs/openbao-transit.md, README updates, and acompose-backends-openbao.yml+mage test:backendsOpenBaoCI job running the full flow against a real OpenBao container.Explicitly out of scope for this PR (see
docs/openbao-transit.mdand prior discussion): BYOK/wrapped import of an existing CA key into Transit,openvox-ca-ctl setupflags for offline Bao-targeted bootstrap, and automated Kubernetes-auth CI testing (no kind/k3d infra in this repo yet) — Kubernetes auth has full unit coverage but is only verified live manually.Test plan
go build ./...,go vet ./...(incl.-tags mage),gofmt -l .cleango test ./...— full suite passing across all packagesmage test:backendsOpenBao— live integration run against a real OpenBao 2.5.5 container (AppRole auth, sign/verify, key generate-then-load, rotation detection)backends-openbaojob🤖 Generated with Claude Code