Forward the real client address from a server-side proxy to a backend without trusting forgeable headers. Use when an API behind a BFF or reverse proxy rate-limits or audits every user as one address.
Installs into .claude/skills of the current project.
Are you the author of Proxy Client Address?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/khaledsaeed18-proxy-client-address)
---
name: proxy-client-address
description: Forward the real client address from a server-side proxy to a backend without trusting forgeable headers. Use when an API behind a BFF or reverse proxy rate-limits or audits every user as one address.
argument-hint: "(optional) the front-end proxy file and the backend's middleware"
---
When a browser calls a front-end server that forwards to an API, the API's peer is the front-end for every user: one rate-limit bucket and one audit address for everyone. Fix it with a named address the API believes only next to a shared secret, derived from the one forwarded header the edge proxy wrote itself.
## Hard rules: never break these
- **Never trust `X-Forwarded-For` from the client side.** Only the entry the edge proxy appended is real; confirm which entry that is for the proxy in front of you before relying on it.
- **The secret is runtime, server-only, identical on both sides,** generated by the owner (`openssl rand -base64 32`), pasted into both deployments, never in a repository or a client bundle.
- **Strip the headers on the way in.** The front-end deletes any `x-<app>-proxy-secret` or `x-<app>-client-ip` a browser sent before setting its own; the API deletes both after reading them.
- **Compare the secret in constant time** on digests, so length and content leak nothing.
- **Without the secret, behave as before.** The front-end falls back to the plain rewrite; the API falls back to `req.ip`. Rollout is safe in any order.
## Step 1: Establish the proxy's behaviour
Read the edge proxy's configuration (Traefik: `docker inspect coolify-proxy --format '{{json .Args}}' | grep -i forwarded`). With no trusted IPs configured, Traefik discards client-sent `X-Forwarded-*` and writes the peer address itself, so the **last** entry is the client; a platform edge behaves the same. If trusted IPs or `insecure` are set, the client's forged entries survive in front of the real one and the last entry is still the real one. A Node server does not append. Document the finding.
## Step 2: Front-end side (the BFF)
In the Next.js proxy (or middleware), for `/api/*` requests: when the secret is configured, rewrite to the API origin with two added request headers, else return nothing and let `next.config` rewrites handle it. Server-side fetches (Server Components) add the same headers from `next/headers`. Snippets in [reference/snippets.md](./reference/snippets.md).
## Step 3: API side
A middleware early in the chain: read both headers, delete them, verify the presented secret against the configured one on SHA-256 digests with `timingSafeEqual`, accept the named address only if it is a valid IP, else use `req.ip`. Store the result on the request (`req.clientIp`) and make the rate limiter's key generator and the audit writer read it. `trust proxy` stays set for `req.ip` to be meaningful behind the edge.
## Step 4: Roll out and prove it
1. Ship both code sides (they are inert without the secret).
2. Owner sets `API_PROXY_SECRET` on the front-end app and `DASHBOARD_PROXY_SECRET` (or the chosen name) on the API, same value, then redeploys both.
3. Prove it: a request through the front-end to a non-existent API route with a forged `X-Forwarded-For: 203.0.113.9`, from two different networks, must be logged by the API with each real address; the same forged header plus a wrong secret sent straight to the API must be logged with the sender's address. The `deploy-verification` skill has the commands.
Report the proxy finding, the header names chosen, the variable names on each side, and the proof lines from the API log.