Trust CF-Connecting-IP on CDN vhost so anon users get per-user buckets#6
Trust CF-Connecting-IP on CDN vhost so anon users get per-user buckets#6rainxchzed merged 1 commit intomainfrom
Conversation
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 7 minutes and 32 seconds.Comment |
Summary
Root cause of "anon users get rate-limited even though we have 4 pool tokens".
The 4-token pool feeds upstream GitHub calls (~20k/hr aggregate). Backend's own per-IP rate-limit bucket is a separate layer. Both work — but the IP-keying was broken on the CDN-fronted path.
The bug
CLAUDE.md and
Caddyfile.prodwere written when the CDN was Gcore. The site has since migrated to Cloudflare (response headers carrycf-rayandserver: cloudflare), but the Caddyfile still:Result: every anonymous user behind a single Cloudflare POP arrives at the backend with the same X-Forwarded-For (the POP IP), shares one rate-limit bucket. With ~10k DAU spread over ~10 popular POPs, each POP gets ~1k users sharing 360/min = trip the limit constantly.
Logged-in users escape because the search bucket re-keys to
tok:<hash>.The fix
Caddyfile.prod— onapi.github-store.org(the CDN-fronted vhost), stop overwriting XFF and stop strippingCF-Connecting-IP. The api-direct vhost keeps both protections (it has no proxy in front, so client-supplied headers must not be trusted there).Plugins.kt—forwardedFor()now readsCF-Connecting-IPfirst, then XFF first IP, then literal "unknown". Cloudflare always overwrites the CF-Connecting-IP header it sets, so on the CDN path it's the real client IP and forge-resistant.CLAUDE.md— corrects the CDN provider (Gcore → Cloudflare) and explains the new IP-resolution chain.Forgery analysis
api.github-store.org): Cloudflare DNS is the only A record. Direct-IP requests with a forged Host header could in principle reach the backend's TLS endpoint (origin IP is the same asapi-direct). An attacker doing this could send a forgedCF-Connecting-IP. Worst-case impact: rate-limit evasion (cycle the forged IP per request to bypass the bucket). NOT a privacy or auth issue — pool-token consumption is still bounded by GitHub's own rate limits, which the attacker can't multiply. If we want to close this, the recommended next step is Cloudflare Authenticated Origin Pulls (Cloudflare presents a client cert; Caddy enforces it). Free Cloudflare feature, ~5 min to enable.api-direct.github-store.org): unchanged. Caddy still overrides XFF with the TCP source and strips CF-Connecting-IP. No forgery vector.Test plan
./gradlew testgreen.https://api.github-store.org/v1/healthfrom two different IPs and confirm via prod logs that the bucket key reflects each real client IP, not the Cloudflare POP IP.api-direct.github-store.orgwith a forgedCF-Connecting-IPheader — confirm Caddy strips it before the request reaches the app, so the bucket key falls back to TCP source.