Credentials Brokering & Secrets Boundary
so1 advanced 5 min read
ELI5
The browser is a guest holding a paper guest pass. The kitchen has the master keyring. When the guest needs a locked cupboard opened, the kitchen opens it on their behalf — the guest never touches a key, ever.
Technical Deep Dive
Invariant (ADR-001 & ADR-002)
Third-party tokens (GitHub PATs, n8n API keys, MCP credentials) are never exposed to the browser.
If a code path could place a secret in any response body, header, or client-side log, that path is a defect regardless of authentication state.
Brokered Call Flow
sequenceDiagram participant B as Browser (so1-rover) participant R as Next.js proxy (/api/bff) participant H as BFF (so1-control-plane-api) participant S as SecretsManager (lib/secrets) participant U as Upstream (GitHub) B->>R: GET /api/bff/catalog (cookie) R->>H: GET /api/catalog (Authorization: Bearer session) H->>H: auth verify → ctx { userId, orgId } H->>H: RBAC check (org A allowed?) H->>S: get(orgId, "github.pat") S-->>H: token H->>U: GET /repos (Authorization: token …) U-->>H: repos JSON H-->>R: repos JSON (no token field) R-->>B: repos JSONThe secret crosses exactly two hops: S → H and H → U. It never crosses H → R or R → B.
lib/secrets.ts
ADR-001 names a lib/secrets.ts abstraction; the concrete backend (AWS KMS, HashiCorp Vault, Google Secret Manager) is deferred to TASKSET 5 so route and adapter code can be written today without coupling to a vendor. Interface (inferred from ADRs):
interface SecretsManager { get(orgId: string, key: string): Promise<string>;}RBAC Layer
Per ADR-001, even with a valid Clerk/BetterAuth session, the BFF must verify the user is a member of orgId before resolving its secret. Skipping this check turns secrets brokering into a privilege-escalation vector.
Token Rotation
Rotation is server-side and invisible to the browser: replacing the value in the secrets manager is sufficient. No client cache invalidation is needed because clients never held the token.
Logging Boundary
Per ADR-003 and ADR-004, log redaction strips Bearer … and sk-… patterns. redactInputs removes pat, token-like fields from Job.metadata.inputs before persistence (so1-007).
Key Terms
- Brokering → server-side dereference of a secret on behalf of an authenticated principal.
- RBAC → role-based access control, here scoped per-org.
- Secrets manager → external system (KMS/Vault/etc.) holding the actual values.
Q&A
Q: Why not store the GitHub PAT in a Clerk custom JWT claim and let the browser send it? A: That breaks the invariant — any XSS would exfiltrate the token. Brokering keeps blast radius bounded to the BFF.
Q: Where does the auth userId come from on the secrets call?
A: From the auth middleware’s context (so1-004), set after BetterAuth/Clerk session verification.
Q: What’s the smallest change that would compromise the model?
A: Echoing details.upstream payloads that include an Authorization header back to the client without redaction.
Examples
Adding a Linear integration: write lib/adapters/linear.ts, fetch the org’s Linear key via secrets.get(orgId, "linear.api_key"), never thread the key through the route handler’s response. The browser sees only the normalised IssueList.
neighbors on the map
- Airlock JWT Handoff & Session Cookies debugging login loops or session expiry
- Link Protection implementing password-protected links
- LORE RBAC & Airlock Auth Flow implementing authentication in a new LORE page
- Airlock Bearer Auth diagnosing a 401 from /v1/ingest/*