IP allow-list
Per-route allow-list of client IPs and CIDR ranges. Requests from a client IP that is not in the list are rejected with 403 Forbidden before they reach auth, rate-limit, or the backend.
Labels
labels: - "sozune.http.<svc>.ipAllowList=<ip-or-cidr>,<ip-or-cidr>,…"
The value is a comma-separated list of:
- IPv4 or IPv6 addresses (auto-promoted to
/32or/128) - IPv4 or IPv6 CIDR ranges
If the label is absent or empty, the filter is disabled and every client is served.
Example
labels: - "sozune.http.api.host=api.example.com" - "sozune.http.api.ipAllowList=10.0.0.0/8,192.168.1.5,2001:db8::/32"
A request from 10.5.7.99 is served. A request from 203.0.113.7 gets a 403 Forbidden with body sozune: client IP not allowed for host 'api.example.com'..
Behaviour
- The middleware runs first in the route's stack — a denied client never reaches request-match, auth, rate-limit, or the backend.
- All
403rejects are logged atWARNlevel with the denied IP and host. - A successful allow is logged at
DEBUG. - Allow-list entries that don't parse are logged and dropped silently: a typo can only narrow access, never widen it.
- If every entry of an allow-list is invalid, the middleware is not installed and the route stays reachable. The per-entry warnings remain in the logs so the operator can fix the configuration.
Client-IP resolution
This is the hard part of any IP allow-list — and the one most implementations get wrong. X-Forwarded-For is set by whoever speaks last to Sōzune. If that's an attacker on the public internet, the header is attacker-controlled. A naïve "trust the leftmost XFF entry" resolver can be bypassed by anyone with curl:
curl -H "X-Forwarded-For: 10.0.0.1" https://api.example.com/ # pretends to be 10.0.0.1
Sōzune solves this by requiring you to declare which reverse-proxies are trusted to set X-Forwarded-For. The list lives in config.yaml under proxy.trusted_proxies:
proxy: http: listen_address: 80 https: listen_address: 443 trusted_proxies: - 10.0.0.0/8 # your internal load balancer / k8s ingress - 172.16.0.0/12
With this setup, the client IP is resolved like this:
trusted_proxiesis empty (the default):X-Forwarded-Foris ignored entirely. The direct TCP peer is the client. Safe to deploy Sōzune publicly without any further configuration.trusted_proxiesis set but the TCP peer isn't in it:X-Forwarded-Foris still ignored — an untrusted peer can't speak about who is behind it.trusted_proxiesis set and the TCP peer is one of them: walkX-Forwarded-Forright to left, skipping every entry that is itself a trusted proxy. The first non-trusted entry is the client. If every XFF entry is trusted (a long internal chain), fall back to the TCP peer.
The TCP peer is the address that connected to Sōzune. Middlewares run in a separate server that only Sōzu talks to, over loopback; Sōzu hands that address over as the last X-Forwarded-For entry, and that is the peer the rules above apply to. An entry of 127.0.0.1 therefore matches a client that really connects from the machine itself, not every client.
This is the standard rightmost trusted algorithm — the same model Cloudflare, NGINX with set_real_ip_from, and HAProxy use. It is the only safe way to honour X-Forwarded-For without letting an attacker forge their identity.
IPv6 / dual-stack note
An IPv4-mapped IPv6 address (::ffff:1.2.3.4) is normalised to its IPv4 form before matching. An allow-list entry of 1.2.3.4 therefore matches a client that arrived on a dual-stack IPv6 socket holding that mapping. A pure IPv6 address like ::1.2.3.4 does not match an IPv4 rule — the two address families remain distinct.
Resolution failure
If neither the TCP peer nor any usable X-Forwarded-For entry yields an IP, the request is denied. The middleware fails closed: gating on identity requires identity.
Combining with other middleware
The allow-list runs before request-match, forward-auth, and rate-limit. A denied client therefore never consumes a rate-limit token, never triggers a forward-auth call, and never reaches the backend. This is the right order for an IP filter: a blocked client must cost as little as possible.
REST / YAML surface
The same field is also available on the entrypoint payload directly, for the file provider, the HTTP provider, and the REST API:
{ "name": "api", "protocol": "Http", "config": { "hostnames": ["api.example.com"], "ip_allow_list": ["10.0.0.0/8", "192.168.1.5", "2001:db8::/32"], // … } }
An empty array (or omitting the field entirely) disables the filter.