Skip to content

fix(webhooks,triggers): encrypt signing secrets at rest - #793

Merged
arantespp merged 1 commit into
mainfrom
claude/soat-secret-encryption-2pczil
Jul 31, 2026
Merged

fix(webhooks,triggers): encrypt signing secrets at rest#793
arantespp merged 1 commit into
mainfrom
claude/soat-secret-encryption-2pczil

Conversation

@arantespp

Copy link
Copy Markdown
Member

Summary

  • 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. This encrypts both using the existing encryptValue/decryptValue from lib/secrets.ts, keyed by SECRETS_ENCRYPTION_KEY.
  • Decryption falls back to treating the stored value as plaintext when it isn't valid ciphertext, so existing rows keep working with no migration/backfill step; they get re-encrypted the next time the secret is created or rotated.
  • No REST/OpenAPI/MCP surface change — same routes, same response shapes, same IAM actions (webhooks:GetWebhookSecret, triggers:GetTriggerSecret, etc.). GET .../secret was 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.
  • Docs updated: modules/webhooks.md, modules/triggers.md, and self-hosting/configuration.md now note that SECRETS_ENCRYPTION_KEY loss 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

  • Added lib-level assertions (via db.Webhook/db.Trigger raw reads) in webhooks.test.ts and triggers.test.ts proving the stored column differs from the plaintext value returned by the API, for both create and rotate.
  • Added tests proving a legacy plaintext row (simulating pre-migration data) is still read back correctly through GET .../secret.
  • pnpm --filter @soat/server test --testPathPatterns="webhooks.test.ts" — 46 passed
  • pnpm --filter @soat/server test --testPathPatterns="triggers.test.ts" — 86 passed
  • pnpm --filter @soat/server test --testPathPatterns="mcp.test.ts" — 140 passed (covers get-trigger-secret/rotate-trigger-secret MCP tools)
  • pnpm --filter @soat/server test --testPathPatterns="formationsApply.test.ts" — 15 passed (covers the webhook formation module's getAttributes().secret path)
  • pnpm --filter @soat/server typecheck — clean
  • pnpm --filter @soat/server eslint --fix on changed files — clean

Generated by Claude Code

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
arantespp enabled auto-merge (squash) July 31, 2026 17:18
@github-actions

Copy link
Copy Markdown

Deploy Outputs

Package Stack Output Key Output Value
@soat/website SoatWebsite-claude-soat-secret-encryption-2pczil BucketWebsiteURL http://soatwebsite-claude-soat-secret-encryp-staticbucket-iu9pjrrc6qx8.s3-website-us-east-1.amazonaws.com

@arantespp
arantespp merged commit b937d2e into main Jul 31, 2026
6 checks passed
@arantespp
arantespp deleted the claude/soat-secret-encryption-2pczil branch July 31, 2026 17:30
arantespp added a commit that referenced this pull request Jul 31, 2026
Patch release 0.18.0 -> 0.18.1.

- fix(webhooks,triggers): encrypt signing secrets at rest (#793)
- fix(workflows): reject an automation transition with no recorded cause (#792)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants