Skip to content

fix(webhooks): set trust proxy and resolve client IP via req.ip#427

Merged
islandbitcoin merged 1 commit into
mainfrom
fix/webhook-trust-proxy
Jul 3, 2026
Merged

fix(webhooks): set trust proxy and resolve client IP via req.ip#427
islandbitcoin merged 1 commit into
mainfrom
fix/webhook-trust-proxy

Conversation

@islandbitcoin

Copy link
Copy Markdown
Contributor

Summary

Launch-hardening follow-up from the #413 merge (tracked as a pre-cutover deploy item).

Both the Bridge and IBEX webhook servers run behind exactly one XFF-writing hop (nginx ingress; the DO load balancer is L4). Without trust proxy:

  • express-rate-limit keyed every sender into one shared bucket on the LB/ingress socket address, so per-sender limits didn't exist.
  • The IP allowlists (ibex.webhook.allowedIps, Bridge replay BRIDGE_WEBHOOK_REPLAY_ALLOWED_IPS) resolved the client with request-ip, which prefers the leftmost X-Forwarded-For entry — any caller can forge that to an allowlisted IP once an allowlist is enabled.

Changes

  • app.set("trust proxy", 1) on both webhook servers (trust exactly the one ingress hop; client-forged XFF entries stay untrusted).
  • Allowlist checks now use Express's req.ip (rightmost untrusted XFF entry) with a socket-address fallback, replacing request-ip in validateIbexIp and replayIngressMiddleware.
  • Loopback replay exemption unchanged — still granted only from the socket address.
  • Kept validate: { xForwardedForHeader: false } on the limiters as a safety net: a future trust-proxy misconfiguration degrades to a shared bucket instead of a webhook outage.

Testing

  • TEST=webhook-server npm run test:unit — 8 suites, 85 tests pass (replay ingress specs updated to the req.ip semantics, plus a new socket-fallback case).
  • npx tsc --noEmit clean.

Deploy notes

Pairs with the remaining pre-cutover config item: populate ibex.webhook.allowedIps with IBEX's published webhook IPs (allowlist is intentionally fail-open while empty).

🤖 Generated with Claude Code

Both webhook servers sit behind one XFF-writing hop (nginx ingress; the
DO LB is L4). Without trust proxy, express-rate-limit keyed every sender
into one bucket on the LB socket, and the IP allowlists resolved the
client via request-ip, which prefers the leftmost X-Forwarded-For entry
— forgeable by any caller once an allowlist is enabled.

- app.set("trust proxy", 1) on the Bridge and IBEX webhook servers
- ibex.webhook.allowedIps and the Bridge replay allowlist now match on
  req.ip (rightmost untrusted XFF hop) with socket-address fallback
- loopback replay exemption unchanged: socket address only

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@islandbitcoin islandbitcoin merged commit 9e966dc into main Jul 3, 2026
14 checks passed
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