Skip to content

Client IP spoofing: InsecureClientIp in all handlers allows forged IPs; add configurable ip_source #427

@theredspoon

Description

@theredspoon

Problem

All API handlers use `InsecureClientIp` from `axum-client-ip`. This extractor scans headers in a fixed order and takes the leftmost `X-Forwarded-For` value. The leftmost XFF value is client-controlled — any client can send `X-Forwarded-For: 1.2.3.4` and tuwunel will log, rate-limit, and feed security tooling against that forged address.

The library's own documentation describes `InsecureClientIp` as appropriate when you "prefer to sacrifice security for probably statistically better IP determination," citing geolocation as the intended use case. Rate limiting and security tooling are the explicit counter-examples.

Two concrete problems with the current scan order:

  1. Leftmost = client-controlled. A client sets `X-Forwarded-For: ` and wins.
  2. `CF-Connecting-IP` is checked last, after `X-Forwarded-For`. Cloudflare Tunnel deployments remain spoofable even though `CF-Connecting-IP` is trustworthy — a client can inject a forged XFF value that gets picked up first.

Affected deployments

Any tuwunel instance behind a reverse proxy — nginx, Caddy, Cloudflare Tunnel, Fly.io, AWS CloudFront. This is the recommended production topology per the docs.

Proposed solution

Add an optional `ip_source` config field. When absent, handlers continue using `InsecureClientIp` exactly as today — no operator action required, no behavior change. When set, a request middleware resolves the IP using `SecureClientIp` with the configured source and stores it in extensions; handlers read from a new `ClientIp` extractor backed by those extensions.

`SecureClientIp` reads from exactly one configured source and takes the rightmost value on multi-value headers. The rightmost value is appended by the proxy and cannot be overridden by the client. It is spoofing-resistant.

# Not set (default) — InsecureClientIp behavior unchanged, no migration required

# Direct connection; explicitly disable header scanning
ip_source = "connect_info"

# Behind nginx or Caddy
ip_source = "rightmost_x_forwarded_for"

# Behind Cloudflare Tunnel (cloudflared)
ip_source = "cf_connecting_ip"

Supported values map 1:1 to `SecureClientIpSource` variants already in the crate:

Value Header Proxy
connect_info TCP peer address only Direct, or to explicitly disable header scanning
rightmost_x_forwarded_for X-Forwarded-For rightmost nginx, Caddy
rightmost_forwarded Forwarded rightmost RFC 7239 proxies
x_real_ip X-Real-IP nginx
cf_connecting_ip CF-Connecting-IP Cloudflare, cloudflared
true_client_ip True-Client-IP Cloudflare Enterprise, Akamai
fly_client_ip Fly-Client-IP Fly.io
cloudfront_viewer_address CloudFront-Viewer-Address AWS CloudFront

ClientIp extractor design

The `ClientIp` wrapper extractor branches at request time:

  • If `SecureClientIpSource` is present in extensions (i.e. `ip_source` is configured), delegate to `SecureClientIp` with that source.
  • If not present, fall back to `InsecureClientIp` — preserving today's behavior exactly, including its header fallback chain.

This means Unix socket deployments are handled correctly: when `ip_source` is absent, the fallback to `InsecureClientIp`'s header scan remains in place, which matters because Unix sockets cannot provide `ConnectInfo` (see #310).

Note: `SecureClientIpSource::ConnectInfo` is already installed as an extension in `src/router/layers.rs` today (hardcoded). The infrastructure for `SecureClientIp` to resolve in handlers already exists — this proposal makes the source configurable rather than adding a new dependency.

Trade-offs

Security improvement is opt-in. Operators who do not set `ip_source` keep existing behavior. This is intentional — the alternative is breaking all proxy deployments on update.

`ip_source` is restart-required. The router layer stack is built once at startup and is not rebuilt on config reload. Hot-reloading `ip_source` will be rejected with an error.

Hard failure when configured header is absent. `SecureClientIp` returns an error rather than falling back to the next available source. An operator who sets `cf_connecting_ip` with a misconfigured proxy will see errors rather than a degraded fallback. A startup warning when `ip_source` is set to a header-based value can prompt operators to verify their proxy configuration.

Benefits once adopted

  • CrowdSec HTTP scenarios and fail2ban see real client IPs behind all supported proxy topologies
  • Rate limiting cannot be bypassed by header injection
  • First-class support for Cloudflare Tunnel, Fly.io, and CloudFront — deployments where the correct header currently loses to a spoofable XFF in the scan order

Planned contributions

I intend to submit two PRs against this issue:

  1. Handler migration — introduce the `ClientIp` extractor with the two-mode fallback logic described above, and migrate all handlers from `InsecureClientIp` to `ClientIp`. Default behavior is unchanged for all existing deployments.

  2. Configurable `ip_source` — add the `ip_source` config field (typed enum, serde `rename_all = "snake_case"`), wire it into the layer stack, add a startup warning for header-based sources, and reject `ip_source` changes during hot reload. This PR builds on PR 1 and is where the spoofing protection becomes available to operators.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't right.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions