feat(ibex): validate Ibex webhook sender IP against an allowlist (ISL-112)#411
Merged
islandbitcoin merged 1 commit intoJun 19, 2026
Merged
Conversation
…-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>
bobodread876
approved these changes
Jun 19, 2026
bobodread876
left a comment
Collaborator
There was a problem hiding this comment.
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.
- Correct middleware ordering — validateIbexIp runs before authenticate, so unauthorized IPs get rejected before any secret-checking logic. Efficient.
- Fail-open design — empty allowlist = no check. Smart for a rollout: ship the mechanism, configure later.
- Proper route scoping — the public GET /pay/lnurl/:username endpoint (arbitrary payers) is deliberately left unrestricted. Only authenticated webhook POSTs get the
IP check. - Good IP handling — uses ipaddr.js with ipaddr.process() to normalize IPv4-mapped IPv6 (::ffff:1.2.3.4). CIDR ranges supported.
- 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. - Config is well-documented — schema, types, and base config all updated with clear comments.
Minor observations (non-blocking)
- 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. - 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. - 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.
- 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. - 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
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>
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
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
webhookSecretcheck on the Ibex webhook server.Changes
validateIbexIpmiddleware (webhook-server/middleware/validate-ibex-ip.ts):request-ip(consistent withgraphql-admin-server.ts).ipaddr.js, via a pure, unit-testedisIpInAllowlist. IPv4-mapped IPv6 (::ffff:1.2.3.4) is normalized to IPv4 before matching.403for non-allowlisted IPs.ibex.webhook.allowedIpsis 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).authenticate) to theonReceiveroutes (invoice/lnurlp/zap/cashout/onchain) and the authenticatedonPayPOSTs (invoice/onchain). The publicGET /pay/lnurl/:usernameLNURL-pay endpoint — which legitimately receives traffic from arbitrary payers — is deliberately left unrestricted./healthis also unaffected.ibex.webhook.allowedIpsto 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.allowedIpswith 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
tsc --noEmit: zero errors in the touched files.Notes
request-ip(matching the admin server). If this server sits behind a proxy/CDN that can be bypassed,X-Forwarded-Forspoofing is a theoretical concern — thewebhookSecretcheck 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.lnflash/flashPRs yet, so checks above are local (unit + typecheck).Closes ISL-112.
🤖 Generated with Claude Code