CRUMB a card from devarno-cloud

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 JSON

The 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