Skip to content

Trust CF-Connecting-IP on CDN vhost so anon users get per-user buckets#6

Merged
rainxchzed merged 1 commit intomainfrom
trust-cloudflare-real-ip
May 4, 2026
Merged

Trust CF-Connecting-IP on CDN vhost so anon users get per-user buckets#6
rainxchzed merged 1 commit intomainfrom
trust-cloudflare-real-ip

Conversation

@rainxchzed
Copy link
Copy Markdown
Member

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.prod were written when the CDN was Gcore. The site has since migrated to Cloudflare (response headers carry cf-ray and server: cloudflare), but the Caddyfile still:

request_header X-Forwarded-For {remote_host}   # overrides XFF with TCP source
request_header -CF-Connecting-IP                # strips Cloudflare's real-IP header

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

  1. Caddyfile.prod — on api.github-store.org (the CDN-fronted vhost), stop overwriting XFF and stop stripping CF-Connecting-IP. The api-direct vhost keeps both protections (it has no proxy in front, so client-supplied headers must not be trusted there).
  2. Plugins.ktforwardedFor() now reads CF-Connecting-IP first, 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.
  3. CLAUDE.md — corrects the CDN provider (Gcore → Cloudflare) and explains the new IP-resolution chain.

Forgery analysis

  • CDN path (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 as api-direct). An attacker doing this could send a forged CF-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.
  • Direct path (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 test green.
  • After deploy, hit https://api.github-store.org/v1/health from two different IPs and confirm via prod logs that the bucket key reflects each real client IP, not the Cloudflare POP IP.
  • Hit api-direct.github-store.org with a forged CF-Connecting-IP header — confirm Caddy strips it before the request reaches the app, so the bucket key falls back to TCP source.
  • Watch 429 rate over 24h — should drop materially as each user gets their own bucket.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@rainxchzed has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 32 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff37c0a1-d102-4ceb-ab46-5f1786b3aaaa

📥 Commits

Reviewing files that changed from the base of the PR and between 1fac609 and fe03f63.

📒 Files selected for processing (3)
  • CLAUDE.md
  • Caddyfile.prod
  • src/main/kotlin/zed/rainxch/githubstore/Plugins.kt
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch trust-cloudflare-real-ip

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.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 7 minutes and 32 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@rainxchzed rainxchzed merged commit e205295 into main May 4, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant