Ir al contenido
Español

IP allowlist

Esta página aún no está disponible en tu idioma.

IpAllowlist restricts a route subtree to clients whose IP falls inside one of the configured CIDRs — defence-in-depth on top of BearerTokenAuth: even a leaked token is useless off an allowlisted network.

import { IpAllowlist, IpAllowlistOptions, withMiddleware } from 'actor-ts/http';
const ipAllowlistOptions = IpAllowlistOptions.create().withAllow('10.0.0.0/8', '127.0.0.1/32');
const allowlist = IpAllowlist(ipAllowlistOptions);
const managementRoutes = withMiddleware(allowlist, clusterRoutes);

IPv4 and IPv6 CIDRs are both supported ('::1/128', 'fd00::/8'), and IPv4-mapped IPv6 peers (::ffff:a.b.c.d) on a dual-stack socket are normalised so a plain IPv4 CIDR still matches. A non-matching — or missing — client IP fails closed with 403.

IpAllowlistOptions — builder or plain object:

FieldPurpose
allowNon-empty list of CIDR strings; at least one must contain the client IP, else 403. Invalid CIDR syntax throws at construction.
trustedProxiesCIDRs of the reverse proxies in front of this app. Unset (the default) means no forwarding header is read at all. See below.
forwardedHeaderHeader carrying the forwarded chain. Default 'x-forwarded-for'; only read when trustedProxies is set.
getClientIpReplace IP extraction wholesale — the escape hatch. Default reads request.remoteAddress (the socket peer). Returning null/undefined denies the request (fail-closed). Mutually exclusive with trustedProxies.

The default reads the socket peer, which behind a proxy is the proxy. To get the client’s own address, name the proxies — that is the whole configuration:

const ipAllowlistOptions = IpAllowlistOptions.create()
.withAllow('10.0.0.0/8')
.withTrustedProxies('10.9.9.0/24');
const allowlist = IpAllowlist(ipAllowlistOptions);

The middleware then reads the chain in wire order — [...x-forwarded-for entries, socket peer], client first — and walks it from the right, taking the first address that is not one of your proxies.

That direction is the entire point, because every common proxy appends to an inbound X-Forwarded-For rather than replacing it:

ProxyWhat it does to X-Forwarded-For
NGINX$proxy_add_x_forwarded_for is the inbound header, comma-appended with $remote_addr
AWS ALBappends the connecting peer (xff_header_processing defaults to append)
Cloudflareappends, and additionally sets CF-Connecting-IP

So the leftmost entry is whatever the client typed, and only the entries added from the right were added by infrastructure. Three things follow, and each closes a hole a hand-written extractor leaves open:

  • A client that reaches the app directly is the socket peer, and the peer is not one of your proxies — so the walk stops there and the header is never read. A forged x-forwarded-for decides nothing.
  • Junk a client prepends sits to the left of its real address and is never reached.
  • An entry that is not a valid address matches no CIDR, counts as untrusted, ends the walk — and then fails allow too.

Where your proxy sets a single-value header instead of appending, point the same walk at it. On Cloudflare that is CF-Connecting-IP:

const ipAllowlistOptions = IpAllowlistOptions.create()
.withAllow('10.0.0.0/8')
.withTrustedProxies('173.245.48.0/20', '103.21.244.0/22') // …the published Cloudflare ranges
.withForwardedHeader('cf-connecting-ip');