SC-6 Encrypted secrets table, ${SECRET:NAME} substitution, access log, secrets UI #628
Labels
No labels
area:agents
area:dashboard
area:database
area:design
area:design-review
area:flows
area:infra
area:meta
area:security
area:sessions
area:webhook
area:workdir
security
type:bug
type:chore
type:meta
type:user-story
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
charles/claude-hooks#628
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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_KEYenv var (32 b64 bytes). Service refuses to start if missing or malformed length.docs/credentials.mdand the systemd unit template how to generate (openssl rand -base64 32) and where to paste it.apps/server/src/infrastructure/secrets.tsexports: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 intosecret_access_logbefore returning the plaintext.${SECRET:NAME}substitution${SECRET:NAME}placeholder with the decrypted value, and reports the access. Used byrenderForInstance(SC-2 / SC-5).MissingSecretError(name)and never silently produces empty placeholder text.HTTP routes
GET /secrets— list of names + descriptions + last-rotated. Bodies never returned.POST /secretsbody{ name, value, description? }— encrypts + inserts.PUT /secrets/{name}body{ value, description? }— replaces ciphertext, bumpsrotated_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 (bumpsrotated_at; useful as a scheduled hygiene move).guardMutatingrail.Dashboard UI
/settings/secrets(or a tab in the new agent-config page): list, add, rotate, delete, access-log viewer.valueas write-only — read shows••••, only POST/PUT carry it.<Drawer>,<Button>,<Tabs>primitives perapps/web/CLAUDE.md.Tests
encrypt(plain)→decrypt(row, accessor, reason)returnsplain, logs one access row with the supplied accessor + reason.MissingSecretError("FOO")when the placeholder names an unknown secret.GET /secretsreturns names only, never bodies. Mutating endpoints require operator session.Out of scope
References
specs/agent-config-customization.md§Encryption and §Story SC-6apps/server/src/infrastructure/database/migrations/— SC-1 lands thesecret+secret_access_logtablesdocs/credentials.md— current credentials documentation