- Shipped
- August 5, 2026 at 6:53 AM UTC
- Author
- Kamo
- Commit
- 0536219
The rules were stored, cached to Redis and never consulted. AccessCheckController was the only caller of the evaluation and nothing called AccessCheckController; APIService's filter resolved orgs from a Redis key nothing wrote and treated the miss as permission. Configuring a geo block did nothing at all. AccessDecisionService is now the single authority. Login calls it directly, the gateway filter and the kamo-internal proxy reach the same decision through the check endpoint, and the policy exists once instead of once per layer. The Node copy had drifted far enough to match nothing: it compared rule types to "whitelist"/"temp-block" while the publisher writes WHITELIST/TEMP_BLOCK. GeoLite resolution is cached in Redis for a day, positively and negatively, and skips private addresses entirely. The address comes from a header, so without a negative cache a caller rotating bogons turns every request into a fresh probe of the 5.7M-row table — the DoS shape findByIpInRange was already rewritten once to avoid. ClientIpResolver reads the RIGHT-most public hop of X-Forwarded-For, not the left-most. The left element is whatever the client sent: proxies append, so "X-Forwarded-For: 8.8.8.8" plus Traefik's own append would have let anyone pick the country their block is judged against. Every geo rule would have been one header away from bypass. Fail-open throughout, per dependency and asserted per dependency. An access control that fails closed on a Redis wobble locks every tenant out of the platform, which is a worse incident than the window it closes. A whitelisted address also bypasses geo, so an admin who blocks their own country keeps a way back in.