Webhook Dispatcher — Security Model
Status: populated
Owner: Platform Engineering / Security
Last updated: 2026-04-18
Companion: API_CONTRACTS · DEPLOYMENT_TOPOLOGY
1. Threat Surface
| Surface | Exposure |
|---|
REST API /v1/webhooks | External (via Kong); JWT-authenticated |
| Outbound HTTP to customer URLs | External egress; adversarial responses possible |
| NATS consumer | Internal cluster only |
/health, /ready, /metrics | Internal cluster only |
2. Authentication & Authorisation
REST API (Kong)
- JWT Bearer token validated by Kong before request reaches service.
accountId injected as X-Account-Id header by Kong after JWT validation.
- Service trusts
X-Account-Id only from Kong (NetworkPolicy restricts direct access).
- All webhook CRUD operations verified against
accountId — no cross-account access.
NATS
- mTLS leaf node credentials; user
webhook-dispatcher.
- Scoped permissions:
subscribe webhook.dispatch, publish webhook.dispatch.deadletter.
PostgreSQL
- Dedicated service account
hook_svc.
- Grants: full CRUD on
hook.webhook_configs, hook.delivery_attempts.
- No access to
dlr.*, orch.*, or other schemas.
- TLS enforced.
3. Webhook Secret Security
At Rest
secret_enc column stores AES-256-GCM ciphertext.
- Encryption key: KMS-managed CMK; application derives data key per-tenant using envelope encryption.
- Plaintext secret never written to disk or logs.
In Transit (HMAC Signing)
- Plaintext secret loaded into memory only during signing.
- Memory not shared across concurrent goroutines.
- Secret not returned in any API response after initial registration.
- Customers may rotate by issuing
PUT /v1/webhooks/:id with new secret.
Secret Rotation Impact
- After secret rotation, any in-flight deliveries using the old secret will be rejected by the customer endpoint.
- New
delivery_attempts rows (including retries scheduled after the rotation) use the new secret automatically.
4. Outbound HTTP Security
| Control | Implementation |
|---|
| TLS verification | System CA bundle; custom CA not supported |
| Redirect prevention | redirect: 'manual'; 3xx treated as failure |
| Timeout | 5 s hard timeout via AbortSignal.timeout |
| SSRF prevention | Blocked at NetworkPolicy level; internal cluster IPs not routable via egress |
| Response body | Only first 512 chars stored; never executed or parsed as code |
SSRF Mitigation
DNS resolution occurs at delivery time. NetworkPolicy egress rules block access to:
10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 (private ranges)
169.254.169.254/32 (cloud metadata endpoint)
Customer URLs pointing to these ranges will fail at the network layer, not application layer.
5. Rate Limiting
| Surface | Limit |
|---|
| REST API per account | 60 requests/minute (Kong rate-limit plugin) |
| Max webhooks per account | 10 (database-enforced trigger) |
| Outbound delivery concurrency | 20 per pod (NATS max_ack_pending) |
6. Data Handling
| Data | Handling |
|---|
Phone numbers (to field) | Stored only in delivery_attempts.payload_snapshot (JSONB); not logged |
| Webhook secrets | AES-256-GCM encrypted at rest; not in logs; not in API responses |
response_body_preview | First 512 chars of customer endpoint response; may contain arbitrary data; stored as text, never interpreted |
accountId | Internal UUID; safe to log |
7. Dependency Security
npm audit in CI; CRITICAL/HIGH block merge.
- Base image:
node:20-alpine.
- Trivy scan in CI.
- Dependabot for automated patch PRs.