Files
MOPAC/docs/PORTING-NOTES-secrets.md
T
mrcharles c6da05fac5 docs: rewrite README to the docs standard; polish docs/
README: verified quickstart (Docker builder, dry-run, live demo), mermaid
architecture, CLI/config/routing reference tables, status table from
REPORT.md, correct ukrrs repo URLs. docs/: Status front-matter lines on
all PORTING-NOTES (secrets notes now tracked). DESIGN: table of contents
with anchor links; sections untouched.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-08-28 21:10:04 -05:00

6.5 KiB

Porting notes: KNELSecretsManager (reference/KNELSecretsManager)

Scope for MOPAC harness: Bitwarden secret retrieval for bw: key refs (reserved in internal/config/keys.go:34-35, errors until "phase 3"). DESIGN.md: Go bitwarden-go wrapper replaces KNELSecretsManager; secrets never in logs. Line refs from study 2026-08-28, drift possible.

Status: 2026-08-28 — complete; open questions for Charles pending (section 3), feeds ukrrs/mopac-bitwarden-go.

1. Current surface

Two generations ship in the repo; ADR-002 (docs/ADR-002-ContainerBW.md) supersedes the standalone script with a containerized bw wrapper.

Legacy secrets-manager.sh (v2.0) — single bash script, CLI only: install | get <name> | list | test, -c/--config FILE. Every invocation redoes the full lifecycle: source plaintext config (BW_SERVER_URL, BW_CLIENTID, BW_CLIENTSECRET, BW_PASSWORD) -> bw config server -> bw logout -> bw login --apikey -> bw unlock --raw -> bw get password <name> (secrets-manager.sh:119-159). Exit codes 10/20/30/40/50/60/70 (config/bw-missing/install/server/session/secret/login). Logs to /tmp/secrets-manager.sh.log (names, not values). Designed to be vendored into shell frameworks; Makefile lint/test/vendor-test targets.

Current container wrapper (production since 2026-08-13, ADR-002):

  • bin/bw-install.sh — downloads pre-compiled bw 2026.7.0 (inside an alpine container; no host Node/wget), builds reachableceo-bw-native: 2026.7.0 (debian:bookworm-slim + ca-certificates, docker/bw-native/ Dockerfile), installs wrapper to ~/.local/bin/bw + entrypoint to ~/.local/share/bw/entrypoint.sh.
  • bin/bw-cli.sh (host) — loads ~/.config/bw/env (single-quoted values, mode 600; the only secrets-on-disk allowed org-wide), then docker run with named volume tsys-bw-cli-state mounted at /root/.config/Bitwarden CLI so login state persists across calls.
  • bin/bw-entrypoint.sh (container) — idempotent: bw config server -> bw login --apikey (no TOTP; API key minted from an authenticated web session) -> bw unlock --passwordfile (password briefly in /tmp) -> bw sync -> exec bw "$@" with BW_SESSION.
  • Interface is transparent bw: status | list items/collections | get password|totp|item <name> | generate. Lookup is by item name; the default secret is the item's password field (e.g. APIKEY-pushover).
  • Backend: Vaultwarden at https://pwvault.turnsys.com, machine account coo@turnsys.com, API-key auth + master-password unlock.

Consumers: nothing in the MOPAC harness consumes it yet — sole planned integration is the bw: key-ref prefix (internal/config/keys.go, asserted by config_test.go:126,130; harness.toml.example:5). First real secrets needed: Redmine API key + LiteLLM key ([redmine] key_ref, [litellm] key_ref). Siblings still on Bitwarden: KNELCredsManager (~/.creds/*.env feeder) and mcp-bitwarden-wrapper.sh in KNEL-AIMiddleware.

Weaknesses to fix in the port: plaintext master password + API secret on disk; full login/unlock/sync per call (slow, racy logout in legacy); secret values echoed to stdout and captured by callers into env; docker required per host; "native" bw binary is secretly a Node SEA (ADR-002 caveat); legacy script bw logouts any concurrent session.

2. Replacement design sketch (Go)

Shape: internal/secrets package + thin MOPAC/tools/bitwarden-go CLI (per DESIGN.md:88) so both harness and humans share one resolver. Config gains ResolveKeyRef case for bw:REF (keys.go already reserves it; keep redact() semantics).

bw execution: exec bw from PATH (the ADR-002 wrapper makes it transparent whether bw is native or containerized). Treat as opaque subprocess: bw get password <name> --session <tok> --nointeraction, JSON mode where useful (bw get item <name>), 30s timeout, capture stdout into a []byte not a logged string.

Machine credentials (bootstrapping only): prefer env BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD; fallback ~/.config/bw/env parsed by hand (single-quoted values, require mode 0600). Never in harness.toml, never in logs. Server URL from env BW_SERVER with Vaultwarden default.

Unlock strategy: once per process — config server (idempotent), bw login --apikey (skip if bw status says logged in), bw unlock --passwordenv BW_PASSWORD --raw -> session key held in memory only; single bw sync after login. On session-expired error mid-run: re-unlock once, then fail. Never bw logout (breaks concurrent clients).

Lookup by key: bw:<item-name> -> password field (matches current usage). Optional bw:<name>#<field> and bw:<name>#totp if non-password fields are needed. Not-found => typed ErrSecretNotFound; auth failures => ErrUnlockFailed (mirrors exit codes 60/50/70 for callers).

Caching: per-run in-memory map[ref]string + mutex (multi-vertical workers may resolve concurrently); negative results cached too. Session token cached for process lifetime, zeroed on exit. No disk cache of values; optional disk persistence of BW_SESSION is an open question.

Never-log rules (extends keys.go:70-78): log refs only as bw:<name>****; error strings never embed values; subprocess stderr from bw is scrubbed/dropped (bw can echo fragments); secrets never written to REPORT.md, spans, or crash dumps; go test uses fake bw stub, never the real vault.

3. Open questions for Charles

  1. Substrate: keep shelling out to bw (containerized per ADR-002, Docker on every harness host) or implement the Vaultwarden REST API directly in Go (truly zero Node/bw, but reimplements crypto + sync)? DESIGN.md:84 says "replaces Node bw CLI dependency" — which side of that line do you want?
  2. Machine account: dedicated service account for the harness instead of coo@turnsys.com, with a vault collection scoped to only the secrets the harness needs?
  3. Ref syntax: is bw:<item-name> (password field) sufficient for v1, or do you want #field / #totp / collection-scoped refs now?
  4. Session lifetime: unlock per harness once run only, or persist BW_SESSION (0600, TTL) across runs to avoid unlock cost each start?
  5. Secret inventory + names: confirm exact vault item names for Redmine and LiteLLM keys so key_ref values can be pinned in harness.toml.example.
  6. Scope of port: absorb KNELCredsManager's ~/.creds/*.env contract too, or strictly the bw: refs used by the harness?
  7. Subprocess injection: any consumers needing secrets as env vars for child processes (LiteLLM?), vs HTTP-header-only as today?