fix(coolify): URL-token auth + test connection UI + IP allowlist for webhook ingest#44
Merged
Merged
Conversation
…webhook ingest
Coolify's webhook UI only exposes a single URL field (no headers, no signing),
so the original HMAC-header design was unusable against real deployments.
Replace it with a per-user token embedded in the URL path, while keeping the
legacy HMAC route mounted for a 30-day grace period.
Part 1 — URL-token auth
- New primary route: POST /api/coolify/webhook/:user_id/:token
token IS users.coolify_webhook_secret, constant-time compared.
- Legacy POST /api/coolify/webhook/:user_id kept; returns Deprecation +
Sunset headers and logs a warning per hit.
- GET/POST /api/account/coolify-webhook-secret now returns the full
URL with the token embedded.
Part 2 — Test-connection UI + audit log
- New table coolify_webhook_attempts (capped 100/user, app-side trim).
- Every hit logged: success, auth_failed, ip_rejected, bad_payload,
legacy_hmac.
- GET /api/account/coolify-webhook-attempts?limit=N (JWT).
- SettingsPage Coolify Webhook card renders the last 10 attempts inline
with status pill + IP + reason + relative timestamp + refresh button.
Part 3 — IP allowlist (defense in depth)
- Optional users.coolify_webhook_allowed_ips (CSV of IPv4/IPv6/CIDR).
NULL/empty = allow all (back-compat).
- GET/PUT /api/account/coolify-webhook-allowed-ips (JWT, validates CIDR).
- Zero-dep CIDR helper at hub/src/lib/cidr.ts (IPv4 + IPv6 + CIDR).
- Rejected requests get 403 + audit row with reason
'source_ip_not_in_allowlist'.
Coolify event-name fix (discovered mid-flight from PR #42 investigation)
- Coolify's SendWebhookJob emits underscore event names
(deployment_success, deployment_failed), not the dotted form.
- Zod schema now accepts both and normalizes internally via EVENT_ALIAS.
Tests
- 34 new/updated tests in hub/test/{coolify-webhook,cidr}.test.ts cover
URL-token success + wrong-token, no-secret enumeration safety,
underscore event aliasing, IP allowlist match/mismatch/CIDR/x-forwarded-for,
legacy HMAC success + Deprecation header + every failure path.
- Full hub suite: 132 pass / 0 fail.
- web/bun run build: green.
Docs
- docs/coolify-webhook-migration.md: new "2026-05-25 update" section
documents URL-token auth, Coolify event-name shape, IP allowlist,
legacy grace period, audit log.
- CLAUDE.md: file-map entries updated to reflect new ingress shape +
new endpoints + cidr helper.
User action post-deploy: re-rotate via Settings → Supervisor → Coolify
Webhook (the previously-pasted URL will keep working via the legacy HMAC
route, but Coolify can't actually send HMAC headers, so re-rotating to get
the new URL format is required for Coolify-originated webhooks to succeed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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
Coolify's Notifications → Webhook UI only exposes a single URL field — no header configuration, no signing-secret support. Phase 06's original HMAC-header design was therefore unusable against real Coolify deployments (every test notification rejected at the signature check). This PR replaces it with a per-user token embedded in the URL path, and folds in two parallel-discovered fixes.
Part 1 — URL-token auth (primary)
POST /api/coolify/webhook/:user_id/:token.tokenIS the existingusers.coolify_webhook_secretUUID. Constant-time compared. Enumeration-safe (identical 401 shape + timing for unknown user vs. wrong token).POST /api/coolify/webhook/:user_id(HMAC headers) kept for a 30-day grace period. ReturnsDeprecation: true+Sunset:+Link: rel=deprecationheaders and logs a warning per hit.GET/POST /api/account/coolify-webhook-secret[/rotate]now return the full URL with the token embedded — that's the string the user pastes into Coolify.Part 2 — Test-connection UI + audit log
coolify_webhook_attempts(capped 100 rows/user via app-side delete-oldest). Every hit logged regardless of outcome.GET /api/account/coolify-webhook-attempts?limit=N(JWT).success/auth_failed/ip_rejected/legacy_hmac/bad_payload), source IP, reason, relative timestamp, refresh button. Empty state explains the next step.Part 3 — IP allowlist (defense in depth)
users.coolify_webhook_allowed_ips(CSV of IPv4 / IPv6 / CIDR). NULL/empty = allow all (back-compat).GET/PUT /api/account/coolify-webhook-allowed-ips(JWT). PUT validates each entry server-side; returns 400 with the offending entry on bad CIDR.hub/src/lib/cidr.ts(IPv4 + IPv6 + CIDR via plain bigint math; matches the existingcf-connecting-ip → x-real-ip → x-forwarded-forsource-IP precedence used elsewhere in the hub).source_ip_not_in_allowlist.Coolify event-name fix (discovered mid-flight from PR #42 investigation)
Coolify's
SendWebhookJobemits underscore event names (deployment_success,deployment_failed), not the dotted form some docs imply. The Zod schema now accepts both at the wire and normalizes internally viaEVENT_ALIASto the dotted canonical (deployment.succeeded/deployment.failed) so the rest of the pipeline stays on one shape.Test plan
bun test hub/— 132 pass, 0 fail (45 skipped, allREMO_E2E_DB_URL-gated)bun run build:web— greensuccessrow appears in the attempts logUser action post-deploy
successrow within seconds.46.224.61.233(Coolify server IP) to Allowed source IPs for defense in depth.🤖 Generated with Claude Code