---
id: w4/m6/t002
title: "Core SecretStore seam + OpenBao KV client (BEX_OPENBAO_URL, k8s auth login)"
worker: worker4
status: done
estimate: 40m
depends_on: [w4/m6/t001]
---

## Objective

Add a `SecretStore` interface to the bex-api Core (get/put/delete env maps per service) with an OpenBao KV v2 implementation: login via the Kubernetes auth method (m5 t004's role), read/write `tenants/<tenant>/services/<app>/env` (single default tenant until w1/m2, same convention as m4). Configured by `BEX_OPENBAO_URL`; when unset, secrets operations return "unavailable" (surfaced as 503 by adapters) — the same pattern as `BEX_PROM_URL`, everything else untouched.

## Context

- Core/adapters split: `docs/ADR006-bex-api.md` — one Core, thin adapters; follow how the metrics Core handles the optional `BEX_PROM_URL` dependency.
- SA token for login is mounted in the pod; token renewal per OpenBao lease semantics.
- Nil/unset behavior must leave the rest of the API byte-for-byte unchanged (mirror m4's nil-checker discipline).

## Steps

1. Define the `SecretStore` interface in the Core + a fake for tests.
2. Implement the OpenBao client (k8s auth login, KV v2 data/metadata paths, token renewal).
3. Wire construction in `cmd/api/main.go` from `BEX_OPENBAO_URL`; nil ⇒ unavailable.
4. Unit tests against the fake + an httptest OpenBao stub for login/read/write.

## Files

- `operator/internal/api/` (Core seam + openbao client; follow existing package layout)
- `operator/cmd/api/main.go`

## Acceptance criteria

- [ ] Interface + OpenBao impl + fake; unit tests cover login, get/put/delete, unavailable path.
- [ ] `BEX_OPENBAO_URL` unset ⇒ store is nil and nothing else changes; `make test` green.

## Out of scope

- HTTP endpoints (t003); k8s Secret materialization (t004).
