[feature] OpenBao transit engine + broker + KV-sourced master key #27

Open
opened 2026-08-12 08:12:19 +00:00 by james.oates · 3 comments
Owner

Execution tracker for the OpenBao integration. Design + plan live in Obsidian (source of truth):

  • Spec: edge-router/specs/2026-08-11-openbao-transit-broker-design.md
  • Plan: edge-router/plans/2026-08-11-openbao-transit-broker.md

Branch: feat/openbao-transit-broker. Supersedes the transit portion of #20; unblocked by #18.

What: opt-in OpenBao as the at-rest encryption engine (transit) and as a source for the master API key (KV), with all OpenBao credentials obtained from a local broker over a Unix socket — key material never enters the ER process. Local AES-GCM stays default; two independent toggles share one broker + one OpenBao.

Tasks (TDD, each independently testable):

  • 1. Broker + transit client (broker.rs) — token over UDS, 90%-TTL cache, 403-refresh (mock-UDS-broker tests)
  • 2. Async CryptoEngine enum (Local | Transit); prefix-routed decrypt; .await refactor of call sites
  • 3. Config + OpenBao env + file>inline resolution flip + highest-security-wins-and-rejects
  • 4. Master key from OpenBao KV (kv_read + v1/v2 field extraction)
  • 5. One-shot AES→transit migration (injectable re-encrypt seam; idempotent/resumable)
  • 6. Startup wiring + fail-closed (health-check fail-fast, migrate, inject)
  • 7. Live integration test (gated on BAO_ADDR, real transit round-trip) + docs

Pre-flight: dev homelab runs a pre-per-service broker (one shared socket /run/broker/broker.sock, broker itself registered) → no ER enrollment needed; Task 7 verifies against it. Confirm the actual transit key name + KV path/field at Task 7.

Notes: migration is one-way (rollback needs a pre-migration DB snapshot); ENCRYPTION_KEY resolution flips to file>inline (one non-additive behavior change). Verify the Alpine/musl Docker build after adding hyperlocal.

Execution tracker for the OpenBao integration. Design + plan live in Obsidian (source of truth): - Spec: `edge-router/specs/2026-08-11-openbao-transit-broker-design.md` - Plan: `edge-router/plans/2026-08-11-openbao-transit-broker.md` Branch: `feat/openbao-transit-broker`. Supersedes the transit portion of #20; unblocked by #18. **What:** opt-in OpenBao as the at-rest encryption engine (transit) and as a source for the master API key (KV), with all OpenBao credentials obtained from a local broker over a Unix socket — key material never enters the ER process. Local AES-GCM stays default; two independent toggles share one broker + one OpenBao. **Tasks (TDD, each independently testable):** - [ ] 1. Broker + transit client (`broker.rs`) — token over UDS, 90%-TTL cache, 403-refresh (mock-UDS-broker tests) - [ ] 2. Async `CryptoEngine` enum (Local | Transit); prefix-routed decrypt; `.await` refactor of call sites - [ ] 3. Config + OpenBao env + `file>inline` resolution flip + highest-security-wins-and-rejects - [ ] 4. Master key from OpenBao KV (`kv_read` + v1/v2 field extraction) - [ ] 5. One-shot AES→transit migration (injectable re-encrypt seam; idempotent/resumable) - [ ] 6. Startup wiring + fail-closed (health-check fail-fast, migrate, inject) - [ ] 7. Live integration test (gated on `BAO_ADDR`, real transit round-trip) + docs **Pre-flight:** dev homelab runs a pre-per-service broker (one shared socket `/run/broker/broker.sock`, broker itself registered) → **no ER enrollment needed**; Task 7 verifies against it. Confirm the actual transit key name + KV path/field at Task 7. **Notes:** migration is one-way (rollback needs a pre-migration DB snapshot); `ENCRYPTION_KEY` resolution flips to file>inline (one non-additive behavior change). Verify the Alpine/musl Docker build after adding `hyperlocal`.
Author
Owner

Task 1 donee6ad026 on feat/openbao-transit-broker.

broker.rs: TransitClient (token over UDS, ~90%-TTL cache, 403-refresh, HTTPS-only, Debug-redacted token) + is_transit_ciphertext. Offline tests via a mock UDS broker (cache-vs-refetch, https-only rejection, fail-closed-without-broker); real transit round-trip deferred to the gated live test (task 7). Deps added: hyperlocal, bytes. Full suite 202 green, clippy + fmt clean.

Tracked debt: mod broker is #[allow(dead_code)] until CryptoEngine (task 2) + startup wiring (task 6) construct a TransitClient. Remove the allow then.

Tasks 2–7 remain. Task 2 is the broad async refactor of every encrypt/decrypt call site (crypto.rs, health.rs, db.rs, middleware.rs, proxy.rs) — the security-sensitive core.

**Task 1 done** — `e6ad026` on `feat/openbao-transit-broker`. `broker.rs`: `TransitClient` (token over UDS, ~90%-TTL cache, 403-refresh, HTTPS-only, `Debug`-redacted token) + `is_transit_ciphertext`. Offline tests via a mock UDS broker (cache-vs-refetch, https-only rejection, fail-closed-without-broker); real transit round-trip deferred to the gated live test (task 7). Deps added: `hyperlocal`, `bytes`. Full suite 202 green, clippy + fmt clean. **Tracked debt:** `mod broker` is `#[allow(dead_code)]` until `CryptoEngine` (task 2) + startup wiring (task 6) construct a `TransitClient`. Remove the allow then. Tasks 2–7 remain. Task 2 is the broad async refactor of every `encrypt`/`decrypt` call site (`crypto.rs`, `health.rs`, `db.rs`, `middleware.rs`, `proxy.rs`) — the security-sensitive core.
Author
Owner

All 7 tasks implemented on feat/openbao-transit-broker (commits e6ad026, 9da37d5, e4590ae, a5bc56a, d76a025):

  • 1. Broker + transit client (broker.rs) — token/UDS/cache/403-refresh, mock-broker tests
  • 2. Async CryptoEngine (Local|Transit) + .await refactor of every call site
  • 3. Config + OpenBao env + file>inline flip + highest-security-wins-and-rejects (pure, race-free tests)
  • 4. Master key from OpenBao KV (kv_read + v1/v2 field extraction)
  • 5. One-shot AES→transit migration (injected re-encrypt seam; selective + idempotent tests)
  • 6. Startup wiring: one shared client, fail-fast health check, engine selection, migrate — all WIP dead-code allows removed
  • 7. Gated live integration test + docs (README/example.toml/CLAUDE.md)

Full suite 213 green (live test skips without BAO_ADDR), clippy + fmt clean, zero dead-code allows. Alpine/musl Docker build (validates hyperlocal) in progress. Then: final review + merge to main, and a live run against the dev broker to prove the real transit round-trip.

**All 7 tasks implemented** on `feat/openbao-transit-broker` (commits `e6ad026`, `9da37d5`, `e4590ae`, `a5bc56a`, `d76a025`): - ✅ 1. Broker + transit client (`broker.rs`) — token/UDS/cache/403-refresh, mock-broker tests - ✅ 2. Async `CryptoEngine` (Local|Transit) + `.await` refactor of every call site - ✅ 3. Config + OpenBao env + `file>inline` flip + highest-security-wins-and-rejects (pure, race-free tests) - ✅ 4. Master key from OpenBao KV (`kv_read` + v1/v2 field extraction) - ✅ 5. One-shot AES→transit migration (injected re-encrypt seam; selective + idempotent tests) - ✅ 6. Startup wiring: one shared client, fail-fast health check, engine selection, migrate — all WIP dead-code allows removed - ✅ 7. Gated live integration test + docs (README/example.toml/CLAUDE.md) Full suite **213 green** (live test skips without `BAO_ADDR`), clippy + fmt clean, zero dead-code allows. Alpine/musl Docker build (validates `hyperlocal`) in progress. Then: final review + merge to `main`, and a live run against the dev broker to prove the real transit round-trip.
Author
Owner

Status: code-complete, blocked on live verification — NOT merged.

All offline gates pass on feat/openbao-transit-broker: 213 unit tests, Alpine/musl Docker build (confirms hyperlocal compiles), clippy + fmt clean, zero dead-code allows.

But the feature has never run against real OpenBao — every passing test uses a mock UDS broker (canned token) or an injected vault:v1:{pt} fake. Per our rule (verify external services against the real contract, don't assume documented behaviour), this must be proven before merge. Only a live run catches: the real transit response shape (.data.ciphertext/.data.plaintext + base64), the actual vault:/bao: prefix (which migration idempotency + decrypt-routing depend on), and the broker's real GET /token JSON.

Can't run it from the build host: playground-broker-1's socket is not mounted on the host (/run/broker absent), and devinfra-openbao-1 is only on 127.0.0.1:8200 with an in-network cert the HTTPS-only client correctly rejects. The live test needs to run inside the dev environment where the broker socket is mounted and bao's cert is trusted:

BAO_ADDR=https://bao.dev.oates.ws \
BAO_BROKER_SOCKET=/run/broker/broker.sock \
BAO_TRANSIT_KEY=<key the shared broker's policy grants> \
BAO_MASTER_KEY_PATH=<optional kv path> \
cargo test openbao_live -- --nocapture

Prereqs to confirm there: the shared broker's policy grants a transit key ER can use (encrypt/decrypt), and (if testing KV) read on the master-key path. Merge to main is gated on this coming back green.

**Status: code-complete, blocked on live verification — NOT merged.** All offline gates pass on `feat/openbao-transit-broker`: 213 unit tests, Alpine/musl Docker build (confirms `hyperlocal` compiles), clippy + fmt clean, zero dead-code allows. **But the feature has never run against real OpenBao** — every passing test uses a mock UDS broker (canned token) or an injected `vault:v1:{pt}` fake. Per our rule (verify external services against the real contract, don't assume documented behaviour), this must be proven before merge. Only a live run catches: the real transit response shape (`.data.ciphertext`/`.data.plaintext` + base64), the actual `vault:`/`bao:` prefix (which migration idempotency + decrypt-routing depend on), and the broker's real `GET /token` JSON. **Can't run it from the build host:** `playground-broker-1`'s socket is not mounted on the host (`/run/broker` absent), and `devinfra-openbao-1` is only on `127.0.0.1:8200` with an in-network cert the HTTPS-only client correctly rejects. The live test needs to run **inside the dev environment** where the broker socket is mounted and bao's cert is trusted: ``` BAO_ADDR=https://bao.dev.oates.ws \ BAO_BROKER_SOCKET=/run/broker/broker.sock \ BAO_TRANSIT_KEY=<key the shared broker's policy grants> \ BAO_MASTER_KEY_PATH=<optional kv path> \ cargo test openbao_live -- --nocapture ``` Prereqs to confirm there: the shared broker's policy grants a transit key ER can use (encrypt/decrypt), and (if testing KV) read on the master-key path. **Merge to `main` is gated on this coming back green.**
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#27
No description provided.