jackdaw-dev observation (2026-08-12): the new-namespace warning is non-blocking + stderr, so a scripted put suppressing stderr can still mis-namespace silently. Deliberately not blocking (domain-scoped names would misfire on auto-prefix); revisit with an opt-in --strict flag only if scripted callers appear.
9.9 KiB
title, kind, status, owner, created, depends_on
| title | kind | status | owner | created | depends_on | ||
|---|---|---|---|---|---|---|---|
| secrets-broker | module-contract | draft | infra-ops | 2026-08-11 |
|
secrets-broker — fleet-wide credential registry over Vaultwarden
Purpose
A single place to store and look up any fleet secret — env files, API tokens,
TLS certs/keys, SSH keys, DB creds, WireGuard keys — that should not live in a git
repo and is today single-copy on a host. Vaultwarden is the durable central store;
the broker is the thin programmatic surface (secret put|get|list|backfill) that
agents, scripts, and the operator use. Solves two problems at once: durability
(secrets currently backed up nowhere) and lookup (no canonical place to find a
credential).
Scope (corrected 2026-08-11): this serves the CC sessions on THIS dev box
(nh3-dev) — a per-box credential store + backup, NOT a fleet service. New dev
boxes get this same stack copied and duplicated one at a time (each box: its own
bw + a per-box bootstrap.env, backing up its own local secrets, hostname-
namespaced). No daemon / no central broker — the subprocess CLI is the final
shape. Local .env/env.sh files stay in place; the vault is the authoritative
registry + backup. Deploy-time source-of-truth (pull-at-deploy) stays out of scope.
Architecture
- Store: the operator-provisioned service account (
infra-ops@phasefinal.com) in theinfra-opsorg (idd30c6b58…), Default collection (idb829376a…). Items land in that org collection so the operator's primary account (shared into the org) sees them too; organised by folder (per box, named for the hostname) + a<host>/<path>name convention. - Client:
bw(official Bitwarden CLI, installed to~/.localon nh3-dev). Switched fromrbwafter rbw'sregisterreturned an undebuggable 400 against this Vaultwarden despite valid creds (a directclient_credentialsgrant + both prelogin paths return 200; rbw emits no HTTP logs). bw gives a clean unattended flow (login --apikeyviaBW_CLIENTID/BW_CLIENTSECRETenv,unlock --passwordenv --raw→BW_SESSION) and full write support — org collections + attachments included, which reopens the org-vs-personal-vault choice rbw had foreclosed. - Runs as the session user (
lkraven) on nh3-dev, invoked directly by CC sessions on this box. No ssh, no daemon; each dev box runs its own copy against its own local secrets. - Code home:
eshpfi-management/services/secrets-broker/(thesecretCLI is copied to each dev box; the per-boxbootstrap.envlives in~/.config/secrets-broker/,0600, never committed).
Data model / taxonomy (lookup-first)
- One item per secret. Item name encodes the path so lookup works even if
folders are unavailable:
<host>/<stack>/<file>(e.g.ana-docker/gitea/.env), or<domain>/<name>for non-host creds (gitea/claude-bot-token,wireguard/irv-ml1,certs/phasefinal-wildcard). - Folders group by class (one per box, named for the hostname); the item name carries the real path so lookup works regardless.
- Item type: everything is a secure note (type 2) — the file/secret content in
the note body (arbitrary
KEY=VALUEenv content isn't a clean login shape). - Binary secrets (
*.pem,*.key,*.pfx) → base64 into a hidden field (content_b64); portable and rbw-agnostic. bw could use attachments, but base64 keeps a single fetch path. Cap ~64 KB/item. - Metadata fields on every item:
source_host,source_path,synced_at,sha256(of the plaintext, for drift detection without decrypting to compare).
CLI surface (services/secrets-broker/secret)
secret put <name> [--file PATH | --stdin] [--type note|login] [--field k=v]…— idempotent upsert (create if absent, else update in place, keyed by name). Never prints the secret value. Refuses.env.example/ template inputs.secret get <name> [--field FIELD] [--file OUT]— fetch to stdout or a0600file; base64 fields decode with--file.secret list [--prefix P]— names + metadata only (never values).secret backfill --host H [--dry-run]— ssh to host H, enumerate real secret files (.env,env.sh, and the cert/key set), upsert each.--dry-runprints the plan (names, sizes, would-create/would-update) and writes nothing.
Bootstrap secret handling
- The service account's email + master password + personal API key
(client_id/secret) live in a
0600file at~/.config/secrets-broker/bootstrap.envon nh3-dev (session-user-owned) — mirrors the~/.config/<tool>/<token>pattern used across the fleet. The operator provisions the account and drops these creds (they never transit the chat; a0600template is pre-staged atbootstrap.env.template). This is the one secret that cannot live in the vault (secrets-zero); the crown jewel. - bw unattended flow:
bw login --apikey(readsBW_CLIENTID/BW_CLIENTSECRETfrom env, sourced from the bootstrap file) →bw unlock --passwordenv BW_PASSWORD --raw→ a session token exported asBW_SESSIONfor subsequent calls. No pinentry/daemon (the abandoned rbw shim +.pinentry-keymarker can be removed).
Invariants
- Idempotent upserts — re-running
put/backfillconverges, never duplicates (name is the key). - Never log or echo secret values — not to stdout, stderr, logs, or the board.
listand--dry-runshow names/metadata only. .env.exampleand templates are never stored — real secrets only.- Round-trip verified — backfill re-fetches and compares
sha256against the source before reporting a file "stored". - Bootstrap file is
0600, owned by the broker identity; the broker host is the trust boundary (the service account can read everything it can write — Bitwarden has no write-only). - Vault durability is a precondition — the Vaultwarden DB is in the pg_dump set (confirmed); v1 adds a periodic independent encrypted vault export → restic as belt-and-suspenders before it becomes load-bearing.
Phases
- Phase 0 — verify (read-only): DONE. Vault located + DB-backed-up; creds valid (direct grant 200); rbw abandoned (register 400); bw selected.
- Phase 1 — stand up: DONE (2026-08-11). Operator provisioned the service account
(
infra-ops@phasefinal.com); bw installed to~/.local;bw config server+login --apikey+unlock --passwordenvall succeed; create→read→delete round-trip verified against the live vault. Remaining Phase-1 polish: decide the folder-vs-org taxonomy (bw reopened org) and lay it out. - Phase 2 — backfill.
secret backfill --host <h> --dry-runfor every host, review, then real run; round-trip verify. Start with.env/env.sh, then the cert/key set. - Phase 3 — CLI + keep-fresh. Harden the
secretCLI; a periodic re-sync (host→vault) so rotated secrets don't drift; the encrypted export→restic job.
The rbw capability gate — RESOLVED (rbw 1.15.0, 2026-08-11)
Verified against the installed rbw 1.15.0 write surface on nh3-extdev:
add/editsupport--folder,--uri, username, and password + multi-line note. Folders work → taxonomy is folder + name-encoded path.- No
--organization/--collectiononadd→ rbw cannot create items in an org collection. Resolution: v1 uses the service account's own vault + folders (pure-rbw, honors the rust-client choice; nobwwrite-path needed). Org + Collections stays deferred — it would forcebwfor writes for marginal multi-user-ACL benefit the fleet store doesn't need. addis editor-driven (first line = password, the rest = note). Thesecret putwrapper drives it non-interactively by setting$EDITOR/$VISUALto a content-supplying shim (standard rbw scripting pattern) — no human editor in the automated path. Env-file blobs live in the note; single tokens as the password; metadata (sha256/source) as trailing note lines read back withget --field.
Consequence for human lookup: the registry is the service account's shared vault
— the operator looks things up by logging in as secrets-broker (or pointing rbw
at it), not via a shared collection in his personal account. Acceptable for a fleet
infra store; the org path remains the escape hatch if per-user ACLs are ever needed.
Out of scope (v1)
- Deploy-time source-of-truth (deploys pulling secrets from the vault into
.env). This is the powerful evolution; it changes every deploy path and makes a vault outage deploy-blocking. Revisit after the registry is proven. - Organization + Collections with granular multi-user ACLs (rbw-write-limited; future).
- Bitwarden Secrets Manager — Vaultwarden does not implement it; not an option.
Known limitations (deferred)
secret putnamespace warning is non-blocking + stderr — an interactive caller sees the new-top-level-namespace heads-up, but a scripted caller that suppresses stderr can still store an item under a typo'd/missing prefix silently (jackdaw-dev, 2026-08-12). Deliberately not blocking (some names are legitimately domain-scoped, e.g.gitea/…,certs/…, so auto-prefixing would misfire). Revisit with an opt-in--strictflag (nonzero exit on a new namespace) only if scripted callers appear.
Failure modes / rollback
- Broker/vault down → lookups fail, but host
.envare untouched and authoritative (backup semantics), so nothing stops running or deploying. - Bad backfill write → idempotent upsert + round-trip verify catch it; items are versioned in Vaultwarden (restore prior).
- Bootstrap file compromise = full registry read → tight perms + broker-host trust are the control; rotate the service account's master password + API key on suspicion.