Skip to content

feat(ibex): validate Ibex webhook sender IP against an allowlist (ISL-112)#411

Merged
islandbitcoin merged 1 commit into
mainfrom
jabariennis/isl-112-ibex-webhook-ip-validation
Jun 19, 2026
Merged

feat(ibex): validate Ibex webhook sender IP against an allowlist (ISL-112)#411
islandbitcoin merged 1 commit into
mainfrom
jabariennis/isl-112-ibex-webhook-ip-validation

Conversation

@islandbitcoin

Copy link
Copy Markdown
Contributor

Summary

Ibex recommends confirming that webhook requests originate from its published source IPs. This adds an IP-allowlist middleware as defense-in-depth alongside the existing shared webhookSecret check on the Ibex webhook server.

Changes

  • New validateIbexIp middleware (webhook-server/middleware/validate-ibex-ip.ts):
    • Extracts the client IP via request-ip (consistent with graphql-admin-server.ts).
    • Matches it against a configured allowlist of IPs and CIDR ranges using ipaddr.js, via a pure, unit-tested isIpInAllowlist. IPv4-mapped IPv6 (::ffff:1.2.3.4) is normalized to IPv4 before matching.
    • Returns 403 for non-allowlisted IPs.
  • Fail-open when unconfigured: if ibex.webhook.allowedIps is empty the check is skipped, so payment webhooks are never blocked before an operator populates Ibex's IPs (no risk of a self-inflicted webhook outage).
  • Scoped to authenticated webhook routes only. Applied (before authenticate) to the onReceive routes (invoice/lnurlp/zap/cashout/onchain) and the authenticated onPay POSTs (invoice/onchain). The public GET /pay/lnurl/:username LNURL-pay endpoint — which legitimately receives traffic from arbitrary payers — is deliberately left unrestricted. /health is also unaffected.
  • Config: adds ibex.webhook.allowedIps to the JSON schema + TS type and the dev base config (empty default, documented).

Activation (operator action)

This ships the mechanism; the allowlist is off by default. To enforce it, populate ibex.webhook.allowedIps with Ibex's published webhook source IPs/CIDRs in the production config overrides. I did not hardcode IPs — they're deployment config and must come from Ibex's docs.

Validation

  • New unit tests: 10/10 pass — exact IPv4, CIDR in/out, IPv6 range, IPv4-mapped-IPv6, invalid/missing IP, and empty-allowlist cases.
  • Full unit suite: 53 suites / 400 tests green.
  • tsc --noEmit: zero errors in the touched files.

Notes

  • Client-IP extraction uses request-ip (matching the admin server). If this server sits behind a proxy/CDN that can be bypassed, X-Forwarded-For spoofing is a theoretical concern — the webhookSecret check remains the primary auth, with the IP allowlist as an additional layer. Worth confirming the deployment topology (ingress / trusted proxy) when the IPs are configured.
  • No CI on lnflash/flash PRs yet, so checks above are local (unit + typecheck).

Closes ISL-112.

🤖 Generated with Claude Code

…-112)

Ibex recommends confirming that webhook requests originate from its
published IPs. This adds an IP-allowlist middleware as defense-in-depth
alongside the existing shared `webhookSecret` check.

- New `validateIbexIp` middleware: extracts the client IP via request-ip
  (consistent with the GraphQL admin server) and matches it against a
  configured allowlist of IPs / CIDR ranges using ipaddr.js (with a pure,
  unit-tested `isIpInAllowlist`). IPv4-mapped IPv6 is normalized.
- Fail-open when `ibex.webhook.allowedIps` is empty, so payment webhooks are
  never blocked before an operator populates Ibex's published IPs.
- Applied only to the authenticated Ibex-webhook routes (the ones already
  using `authenticate`). The public `GET /pay/lnurl/:username` LNURL-pay
  endpoint, which legitimately receives traffic from arbitrary payers, is
  deliberately left unrestricted.
- Adds `ibex.webhook.allowedIps` to the config schema/type and the dev base
  config (empty default, documented).
- Unit tests cover exact-IP, CIDR, IPv6, IPv4-mapped-IPv6, invalid-IP, and
  empty-allowlist cases.

Activation is operator config: populate `ibex.webhook.allowedIps` with Ibex's
published webhook source IPs in the production overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@linear

linear Bot commented Jun 18, 2026

Copy link
Copy Markdown

ISL-112

@islandbitcoin islandbitcoin self-assigned this Jun 18, 2026

@bobodread876 bobodread876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds an IP allowlist middleware for Ibex webhook routes. Fails open (skips check) when ibex.webhook.allowedIps is empty, so there's zero risk of blocking webhooks
until an operator deliberately configures the IPs.

  1. Correct middleware ordering — validateIbexIp runs before authenticate, so unauthorized IPs get rejected before any secret-checking logic. Efficient.
  2. Fail-open design — empty allowlist = no check. Smart for a rollout: ship the mechanism, configure later.
  3. Proper route scoping — the public GET /pay/lnurl/:username endpoint (arbitrary payers) is deliberately left unrestricted. Only authenticated webhook POSTs get the
    IP check.
  4. Good IP handling — uses ipaddr.js with ipaddr.process() to normalize IPv4-mapped IPv6 (::ffff:1.2.3.4). CIDR ranges supported.
  5. Solid tests — 10 unit tests covering exact IP, CIDR in/out, IPv6, IPv4-mapped IPv6, invalid IP, and empty allowlist. Pure functions (parseAllowlist,
    isIpInAllowlist) are cleanly testable.
  6. Config is well-documented — schema, types, and base config all updated with clear comments.

Minor observations (non-blocking)

  1. Module-load parsing — the allowlist is parsed once at module load (const allowlist = parseAllowlist(...)). If the config is hot-reloaded without a process
    restart, the new IPs won't take effect. This is fine for this use case (IP allowlists rarely change), but worth noting.
  2. requestIp.getClientIp behind a proxy — the PR description already calls this out: if the webhook server sits behind a proxy/CDN, X-Forwarded-For could be spoofed.
    The webhookSecret remains the primary auth. Good that this is documented — make sure to confirm the ingress topology when configuring production IPs.
  3. Missing newline at EOF in middleware/index.ts — pre-existing (the original file had no trailing newline, the PR keeps it that way). Cosmetic, not worth blocking.
  4. No integration test — only unit tests on the pure functions. An integration test that fires a request through the Express router with a mocked IP would be nice
    for confidence, but the unit coverage is adequate for the logic.
  5. 403 response body — returns "Forbidden" as plain text. Consistent with the existing authenticate middleware's pattern? Worth a quick check for consistency, but
    not blocking.

Security review

  • ✅ No secrets in code
  • ✅ Fail-open prevents self-inflicted outages
  • ✅ Defense-in-depth (doesn't replace the shared secret)
  • ✅ Public routes correctly excluded
  • ✅ Proper input validation on config entries (invalid entries logged and skipped, not crashed)

ACK

@islandbitcoin islandbitcoin merged commit d190f03 into main Jun 19, 2026
8 of 14 checks passed
heyolaniran pushed a commit to heyolaniran/flash that referenced this pull request Jun 20, 2026
…-112) (lnflash#411)

Ibex recommends confirming that webhook requests originate from its
published IPs. This adds an IP-allowlist middleware as defense-in-depth
alongside the existing shared `webhookSecret` check.

- New `validateIbexIp` middleware: extracts the client IP via request-ip
  (consistent with the GraphQL admin server) and matches it against a
  configured allowlist of IPs / CIDR ranges using ipaddr.js (with a pure,
  unit-tested `isIpInAllowlist`). IPv4-mapped IPv6 is normalized.
- Fail-open when `ibex.webhook.allowedIps` is empty, so payment webhooks are
  never blocked before an operator populates Ibex's published IPs.
- Applied only to the authenticated Ibex-webhook routes (the ones already
  using `authenticate`). The public `GET /pay/lnurl/:username` LNURL-pay
  endpoint, which legitimately receives traffic from arbitrary payers, is
  deliberately left unrestricted.
- Adds `ibex.webhook.allowedIps` to the config schema/type and the dev base
  config (empty default, documented).
- Unit tests cover exact-IP, CIDR, IPv6, IPv4-mapped-IPv6, invalid-IP, and
  empty-allowlist cases.

Activation is operator config: populate `ibex.webhook.allowedIps` with Ibex's
published webhook source IPs in the production overrides.

Co-authored-by: Dread <bobodread@bobodread.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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