SC-6 Encrypted secrets table, ${SECRET:NAME} substitution, access log, secrets UI #628

Closed
opened 2026-05-01 10:34:20 +00:00 by claude-desktop · 0 comments
Collaborator

User story

As an operator, I want a dedicated encrypted-secrets store where MCP / config artifacts reference values by ${SECRET:NAME} placeholders, so that forge tokens and API keys never sit in plain text inside artifact bodies and every read leaves an audit trail.

Acceptance criteria

Master key + crypto

  • CLAUDE_HOOKS_SECRET_KEY env var (32 b64 bytes). Service refuses to start if missing or malformed length.
  • Document in docs/credentials.md and the systemd unit template how to generate (openssl rand -base64 32) and where to paste it.
  • apps/server/src/infrastructure/secrets.ts exports:
    • encrypt(plain: string): { ciphertext: Buffer; iv: Buffer; auth_tag: Buffer } — AES-256-GCM, fresh 12-byte IV per call, 16-byte auth tag.
    • decrypt(row, accessor: string, reason: string): Promise<string> — always logs into secret_access_log before returning the plaintext.

${SECRET:NAME} substitution

  • Helper that walks an artifact body / config object, substitutes every ${SECRET:NAME} placeholder with the decrypted value, and reports the access. Used by renderForInstance (SC-2 / SC-5).
  • A missing-secret reference raises MissingSecretError(name) and never silently produces empty placeholder text.

HTTP routes

  • GET /secrets — list of names + descriptions + last-rotated. Bodies never returned.
  • POST /secrets body { name, value, description? } — encrypts + inserts.
  • PUT /secrets/{name} body { value, description? } — replaces ciphertext, bumps rotated_at.
  • DELETE /secrets/{name} — hard delete.
  • GET /secrets/{name}/access-log — paginated audit log for that secret.
  • POST /secrets/{name}/rotate — re-encrypt under the current master key (bumps rotated_at; useful as a scheduled hygiene move).
  • All routes operator-auth-gated via the existing guardMutating rail.

Dashboard UI

  • /settings/secrets (or a tab in the new agent-config page): list, add, rotate, delete, access-log viewer.
  • Add / edit forms always treat value as write-only — read shows ••••, only POST/PUT carry it.
  • Reuse <Drawer>, <Button>, <Tabs> primitives per apps/web/CLAUDE.md.

Tests

  • Round-trip: encrypt(plain)decrypt(row, accessor, reason) returns plain, logs one access row with the supplied accessor + reason.
  • Tampering: mutating the ciphertext fails decryption with a clear error (auth-tag mismatch).
  • Missing secret: render fails with MissingSecretError("FOO") when the placeholder names an unknown secret.
  • HTTP: GET /secrets returns names only, never bodies. Mutating endpoints require operator session.

Out of scope

  • Master-key rotation (re-encrypt every row under a new key) — separate ticket.
  • Hardware-backed key storage (HSM / TPM).
  • Per-secret access-control beyond the operator role.

References

  • specs/agent-config-customization.md §Encryption and §Story SC-6
  • apps/server/src/infrastructure/database/migrations/ — SC-1 lands the secret + secret_access_log tables
  • docs/credentials.md — current credentials documentation
  • Depends on SC-1 (tables); SC-2 consumes the substitution helper.
## User story As an operator, I want a dedicated encrypted-secrets store where MCP / config artifacts reference values by `${SECRET:NAME}` placeholders, so that forge tokens and API keys never sit in plain text inside artifact bodies and every read leaves an audit trail. ## Acceptance criteria ### Master key + crypto - [ ] `CLAUDE_HOOKS_SECRET_KEY` env var (32 b64 bytes). Service refuses to start if missing or malformed length. - [ ] Document in `docs/credentials.md` and the systemd unit template how to generate (`openssl rand -base64 32`) and where to paste it. - [ ] `apps/server/src/infrastructure/secrets.ts` exports: - `encrypt(plain: string): { ciphertext: Buffer; iv: Buffer; auth_tag: Buffer }` — AES-256-GCM, fresh 12-byte IV per call, 16-byte auth tag. - `decrypt(row, accessor: string, reason: string): Promise<string>` — always logs into `secret_access_log` before returning the plaintext. ### `${SECRET:NAME}` substitution - [ ] Helper that walks an artifact body / config object, substitutes every `${SECRET:NAME}` placeholder with the decrypted value, and reports the access. Used by `renderForInstance` (SC-2 / SC-5). - [ ] A missing-secret reference raises `MissingSecretError(name)` and never silently produces empty placeholder text. ### HTTP routes - [ ] `GET /secrets` — list of names + descriptions + last-rotated. **Bodies never returned.** - [ ] `POST /secrets` body `{ name, value, description? }` — encrypts + inserts. - [ ] `PUT /secrets/{name}` body `{ value, description? }` — replaces ciphertext, bumps `rotated_at`. - [ ] `DELETE /secrets/{name}` — hard delete. - [ ] `GET /secrets/{name}/access-log` — paginated audit log for that secret. - [ ] `POST /secrets/{name}/rotate` — re-encrypt under the current master key (bumps `rotated_at`; useful as a scheduled hygiene move). - [ ] All routes operator-auth-gated via the existing `guardMutating` rail. ### Dashboard UI - [ ] `/settings/secrets` (or a tab in the new agent-config page): list, add, rotate, delete, access-log viewer. - [ ] Add / edit forms always treat `value` as write-only — read shows `••••`, only POST/PUT carry it. - [ ] Reuse `<Drawer>`, `<Button>`, `<Tabs>` primitives per `apps/web/CLAUDE.md`. ### Tests - [ ] Round-trip: `encrypt(plain)` → `decrypt(row, accessor, reason)` returns `plain`, logs one access row with the supplied accessor + reason. - [ ] Tampering: mutating the ciphertext fails decryption with a clear error (auth-tag mismatch). - [ ] Missing secret: render fails with `MissingSecretError("FOO")` when the placeholder names an unknown secret. - [ ] HTTP: `GET /secrets` returns names only, never bodies. Mutating endpoints require operator session. ## Out of scope - Master-key rotation (re-encrypt every row under a new key) — separate ticket. - Hardware-backed key storage (HSM / TPM). - Per-secret access-control beyond the operator role. ## References - `specs/agent-config-customization.md` §Encryption and §Story SC-6 - `apps/server/src/infrastructure/database/migrations/` — SC-1 lands the `secret` + `secret_access_log` tables - `docs/credentials.md` — current credentials documentation - Depends on **SC-1** (tables); **SC-2** consumes the substitution helper.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#628
No description provided.