Zum Inhalt springen
Deutsch

IP-Allowlist

IpAllowlist beschränkt einen Route-Teilbaum auf Clients, deren IP in einem der konfigurierten CIDRs liegt — Defence-in-depth zusätzlich zu BearerTokenAuth: selbst ein geleaktes Token ist außerhalb eines allowgelisteten Netzes nutzlos.

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- und IPv6-CIDRs werden beide unterstützt ('::1/128', 'fd00::/8'), und IPv4-gemappte IPv6-Peers (::ffff:a.b.c.d) auf einem Dual-Stack-Socket werden normalisiert, sodass ein einfaches IPv4-CIDR trotzdem matcht. Eine nicht passende — oder fehlende — Client-IP schlägt fehl-geschlossen mit 403.

IpAllowlistOptions — Builder oder einfaches Objekt:

FeldZweck
allowNicht-leere Liste von CIDR-Strings; mindestens eines muss die Client-IP enthalten, sonst 403. Ungültige CIDR-Syntax wirft bei Konstruktion.
trustedProxiesCIDRs der Reverse-Proxys vor dieser App. Ungesetzt (der Default) heißt: es wird überhaupt kein Forwarding-Header gelesen. Siehe unten.
forwardedHeaderHeader, der die Forwarded-Kette trägt. Default 'x-forwarded-for'; wird nur gelesen, wenn trustedProxies gesetzt ist.
getClientIpErsetzt die IP-Extraktion komplett — die Notluke. Default liest request.remoteAddress (den Socket-Peer). null/undefined lehnt die Anfrage ab (fail-closed). Schließt sich mit trustedProxies gegenseitig aus.

Der Default liest den Socket-Peer — und der ist hinter einem Proxy der Proxy. Um die eigene Adresse des Clients zu bekommen, benennst du die Proxys; das ist die ganze Konfiguration:

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

Die Middleware liest die Kette dann in Wire-Reihenfolge — [...x-forwarded-for-Einträge, Socket-Peer], Client zuerst — und läuft sie von rechts ab; sie nimmt die erste Adresse, die keiner deiner Proxys ist.

Diese Richtung ist der ganze Punkt, denn jeder verbreitete Proxy hängt an einen eingehenden X-Forwarded-For an, statt ihn zu ersetzen:

ProxyWas er mit X-Forwarded-For macht
NGINX$proxy_add_x_forwarded_for ist der eingehende Header, per Komma um $remote_addr erweitert
AWS ALBhängt den verbindenden Peer an (xff_header_processing steht per Default auf append)
Cloudflarehängt an und setzt zusätzlich CF-Connecting-IP

Der linkeste Eintrag ist also das, was der Client geschrieben hat, und nur die von rechts hinzugefügten Einträge kommen von der Infrastruktur. Daraus folgen drei Dinge, und jedes schließt eine Lücke, die ein handgeschriebener Extractor offen lässt:

  • Ein Client, der die App direkt erreicht, ist der Socket-Peer, und der Peer ist keiner deiner Proxys — der Lauf endet dort, und der Header wird nie gelesen. Ein gefälschter x-forwarded-for entscheidet nichts.
  • Müll, den ein Client davorschreibt, sitzt links von seiner echten Adresse und wird nie erreicht.
  • Ein Eintrag, der keine gültige Adresse ist, matcht kein CIDR, gilt als nicht vertrauenswürdig, beendet den Lauf — und scheitert dann auch an allow.

Wo dein Proxy einen Single-Value-Header setzt statt anzuhängen, richte denselben Lauf darauf aus. Bei Cloudflare ist das CF-Connecting-IP:

const ipAllowlistOptions = IpAllowlistOptions.create()
.withAllow('10.0.0.0/8')
.withTrustedProxies('173.245.48.0/20', '103.21.244.0/22') // …die veröffentlichten Cloudflare-Ranges
.withForwardedHeader('cf-connecting-ip');