fix(webhooks,triggers): encrypt signing secrets at rest - #793
Merged
Conversation
Webhook and trigger signing secrets were the last plaintext secret columns in the schema; every other secret (API keys, AI provider keys, project secrets) is already hashed or AES-256-GCM encrypted. Reuse the existing encryptValue/decryptValue from lib/secrets.ts, keyed by SECRETS_ENCRYPTION_KEY. Decryption falls back to the raw value when it isn't valid ciphertext, so existing rows keep working and get re-encrypted on next rotate/create with no migration window where deliveries or inbound signature checks break. No REST/OpenAPI/MCP surface change: same routes, same response shapes.
arantespp
enabled auto-merge (squash)
July 31, 2026 17:18
Deploy Outputs
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
encryptValue/decryptValuefromlib/secrets.ts, keyed bySECRETS_ENCRYPTION_KEY.webhooks:GetWebhookSecret,triggers:GetTriggerSecret, etc.).GET .../secretwas intentionally kept: it's already gated behind its own IAM action, and both the dispatcher and the inbound signature verifier need the plaintext secret to sign/verify, so removing it wouldn't follow from encrypting the column.modules/webhooks.md,modules/triggers.md, andself-hosting/configuration.mdnow note thatSECRETS_ENCRYPTION_KEYloss also affects webhook delivery signing and inbound trigger signature verification (recoverable via rotate).Open questions resolved this session
Q: Should GET /webhooks/{id}/secret be removed if the column is encrypted? A: No — resolved by long-term (durability ladder). Retrievability is already enforced deterministically by a distinct IAM action, so operators who want show-once can withhold it; removing the route hard-codes one policy for everyone and deletes the only non-disruptive recovery path (rotation invalidates deliveries in flight until subscribers reconfigure). Checked this session: permissions/webhooks.json defines webhooks:GetWebhookSecret separately from webhooks:GetWebhook; webhookDispatcher.ts and rest/hooks.ts both require the plaintext secret to sign/verify, so hashing (the API-key pattern) isn't reachable either way.Test plan
lib-level assertions (viadb.Webhook/db.Triggerraw reads) inwebhooks.test.tsandtriggers.test.tsproving the stored column differs from the plaintext value returned by the API, for both create and rotate.GET .../secret.pnpm --filter @soat/server test --testPathPatterns="webhooks.test.ts"— 46 passedpnpm --filter @soat/server test --testPathPatterns="triggers.test.ts"— 86 passedpnpm --filter @soat/server test --testPathPatterns="mcp.test.ts"— 140 passed (coversget-trigger-secret/rotate-trigger-secretMCP tools)pnpm --filter @soat/server test --testPathPatterns="formationsApply.test.ts"— 15 passed (covers the webhook formation module'sgetAttributes().secretpath)pnpm --filter @soat/server typecheck— cleanpnpm --filter @soat/server eslint --fixon changed files — cleanGenerated by Claude Code