Skip to content

fix(coolify): URL-token auth + test connection UI + IP allowlist for webhook ingest#44

Merged
finedesignz merged 1 commit into
mainfrom
fix/coolify-webhook-url-token
May 26, 2026
Merged

fix(coolify): URL-token auth + test connection UI + IP allowlist for webhook ingest#44
finedesignz merged 1 commit into
mainfrom
fix/coolify-webhook-url-token

Conversation

@finedesignz
Copy link
Copy Markdown
Owner

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)

  • New primary route: POST /api/coolify/webhook/:user_id/:token. token IS the existing users.coolify_webhook_secret UUID. Constant-time compared. Enumeration-safe (identical 401 shape + timing for unknown user vs. wrong token).
  • Legacy POST /api/coolify/webhook/:user_id (HMAC headers) kept for a 30-day grace period. Returns Deprecation: true + Sunset: + Link: rel=deprecation headers 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

  • New table 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).
  • Settings → Supervisor → Coolify Webhook card now renders the last 10 attempts inline with status pill (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)

  • 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). PUT validates each entry server-side; returns 400 with the offending entry on bad CIDR.
  • Zero-dep CIDR helper at hub/src/lib/cidr.ts (IPv4 + IPv6 + CIDR via plain bigint math; matches the existing cf-connecting-ip → x-real-ip → x-forwarded-for source-IP precedence used elsewhere in the hub).
  • 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 some docs imply. The Zod schema now accepts both at the wire and normalizes internally via EVENT_ALIAS to 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, all REMO_E2E_DB_URL-gated)
  • bun run build:web — green
  • 34 new/updated tests cover: URL-token success + wrong-token + no-secret, underscore event aliasing, IP allowlist (exact / CIDR / x-forwarded-for fallback / mismatch → 403), legacy HMAC path (success + Deprecation header + missing sig + stale ts + wrong secret), CIDR helper unit tests
  • After deploy: user re-rotates via Settings, pastes new URL into Coolify, hits "Send Test Notification", confirms a success row appears in the attempts log

User action post-deploy

  1. Go to Settings → Supervisor → Coolify Webhook.
  2. Click Rotate URL (or Generate URL if not configured). Copy the new URL.
  3. In Coolify: Project → Notifications → Webhook. Paste the URL. Save.
  4. Send a test notification from Coolify — the attempts log should show a fresh success row within seconds.
  5. (Optional) Add 46.224.61.233 (Coolify server IP) to Allowed source IPs for defense in depth.

🤖 Generated with Claude Code

…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>
@finedesignz finedesignz merged commit aa7ef81 into main May 26, 2026
1 check passed
@finedesignz finedesignz deleted the fix/coolify-webhook-url-token branch May 26, 2026 14:35
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.

1 participant