You add an address to a blocklist, but the request still gets through. Or every visitor gets blocked at once. Both outcomes can come from evaluating the proxy’s address instead of the client’s.
Before choosing a filtering tool, trace where the address came from and why you trust it.
Separate the connection from the claim
Consider this fictional path:
browser -> edge proxy -> Traefik -> application
Traefik’s connected peer is the edge proxy. A forwarded header can describe an earlier client, but headers are data. A directly connected caller can send its own X-Forwarded-For unless the receiving boundary handles that input deliberately.
The diagram starts with establishing the address because an accurate allow/deny lookup cannot repair an untrusted input.
Work through the policy input
Traefik’s ipAllowList normally compares allowed ranges with the request’s remote address. Its ipStrategy options can select an address from a forwarding chain. Choosing a depth requires understanding your actual trusted hops. See the IPAllowList reference.
Suppose the edge has address 192.0.2.10 and the client is 198.51.100.25. A lookup against 192.0.2.10 answers whether the edge is allowed. It does not distinguish that client from another visitor using the same edge.
A lookup against a caller-supplied 198.51.100.25 has the opposite problem: it may distinguish visitors using claims they chose themselves.
Pause and predict
A direct caller adds an allowed address to X-Forwarded-For. Should that be enough to pass an IP policy?
Need a hint?
Who is authorized to assert an earlier client's identity?
Show the reasoning
Test classification without a network
An allowlist names permitted sources. A blocklist names denied sources. Start by testing that logic independently from header handling:
from ipaddress import ip_address, ip_network
blocked = [ip_network("198.51.100.0/24"), ip_network("2001:db8:1::/48")]
def denied(address):
candidate = ip_address(address)
return any(candidate in network for network in blocked)
cases = {
"198.51.100.25": True,
"203.0.113.25": False,
"2001:db8:1::25": True,
"2001:db8:2::25": False,
}
for address, expected in cases.items():
actual = denied(address)
assert actual == expected, address
print(address, "deny" if actual else "allow")
These are documentation addresses. Save the code as classify.py and run python3 classify.py. Each assertion checks an expected decision. This program deliberately doesn’t parse headers or serve requests; those are separate trust and integration problems.
Put a decision service in the path only when needed
Traefik ForwardAuth can delegate a decision to another service: a successful 2xx response permits forwarding; other responses are returned to the caller. See the ForwardAuth contract.
If you implement a blocklist there, the service becomes a dependency on the request path. Specify how it receives a trustworthy address, how long the proxy waits, and how failure appears to the operator. Do not turn an unavailable decision into an unconditional approval.
The original version of this post used an nginx lookup without fully establishing the forwarded-client boundary. A small configuration is only useful when its input means what you think it means.
Try a four-case integration check
Use a backend that returns a known marker. Verify an allowed client, a blocked client, a direct caller with a forged forwarding header, and an unavailable policy service. Record both the proxy response and whether the backend received a request.
Apply the idea
A blocked request times out. Have you proved that the blocklist rejected it?
Need a hint?
A dead backend can also produce a failed request.
Show the reasoning
You are ready to adopt the policy when you can name the trusted address source and demonstrate both successful access and deliberate rejection.