Repository navigation
Conversation
Cloudflare puts the caller's address in True-Client-IP, and for most of our apps the manage_x_forwarded iRule copies it into X-Forwarded-For on the F5. Fizzy's HTTPS virtual server is fastL4 passthrough, so no HTTP iRule can run, and the app tier's kamal-proxy access log — which the !o 4xx report reads — carries an internal address instead. kamal-proxy 0.10.0 added --client-ip-header, which makes the proxy trust a named header for its own access log and for the X-Forwarded-For it sends on. Pass it at fizzy-lb, with --forward-headers so the rewritten value actually reaches the app tier: X-Forwarded-For is dropped by default when the proxy terminates TLS, as it does there. That needs the accessory off the `lb` tag, which predates the flag, so pin it to v0.10.0 — a moving tag made what a host runs depend on when it last rebooted anyway. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🔵 Needs a closer look
The rollout changes production proxy trust and header-chain semantics while downstream log-index compatibility remains unverified.
Pull request overview
Updates SaaS load balancers to propagate Cloudflare’s real client IP into application-tier logs.
Changes:
- Trusts
True-Client-IPand forwards proxy headers in all environments. - Pins load balancers to kamal-proxy v0.10.0.
- Documents why
TrackTrueClientIpremains necessary.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
saas/script/configure-lb-staging.sh |
Configures staging client-IP forwarding. |
saas/script/configure-lb-production.sh |
Configures production client-IP forwarding. |
saas/script/configure-lb-beta.sh |
Configures beta client-IP forwarding. |
saas/lib/fizzy/saas/true_client_ip.rb |
Documents retained Rails middleware behavior. |
saas/config/deploy.staging.yml |
Pins the staging proxy image. |
saas/config/deploy.production.yml |
Pins the production proxy image. |
saas/config/deploy.beta.yml |
Pins the beta proxy image. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes Fizzy: !o 4xx fizzy returns internal IPs, not public IPs.
The bug
Cloudflare puts the caller's address in
True-Client-IP. For most of our apps the F5'smanage_x_forwardediRule copies that intoX-Forwarded-For, but fizzy's HTTPS virtual server isfastL4passthrough, so no HTTP iRule can run. kamal-proxy only ever knew aboutX-Forwarded-Forand the TCP peer, so the app tier's access log — theweb-*index the!o 4xxreport reads — records an internal address. Rails escaped this becauseTrackTrueClientIpdoes the copy itself beforeActionDispatch::RemoteIp.The change
kamal-proxy 0.10.0 added
--client-ip-header(Kevin's #224 work), which makes the proxy trust a named header both for its own access log and for theX-Forwarded-Forit sends on. So, at fizzy-lb:--client-ip-header=True-Client-IPinsaas/script/configure-lb-*.sh.--forward-headersalongside it. Without this the rewrite only fixes the LB's own log:X-Forwarded-Foris dropped by default when the proxy terminates TLS, which it does here, so the app tier would keep seeing the peer address. kamal-proxy's own test for the flag pairs the two the same way.basecamp/kamal-proxy:lbtobasecamp/kamal-proxy:v0.10.0. Required, not cosmetic — thelbtag was built in Nov 2025 and would reject--client-ip-headeroutright. Pinning also stops what a host runs depending on when it last rebooted, same reasoning as hotcell's image indeploy.yml.TrackTrueClientIpstays. The card thread suggested it becomes deletable, and it does become redundant for traffic through the LB, but it still covers the app when reached directly and it collapses the forwarded chain to the one address we trust. Worth removing separately, once this is live.Worth a look before rolling out
remote_addrbecomes a list,"<public>, <lb peer>", becauseSetXForwardedalways appends the peer. The public address is first, but ifclient.ipis mapped as an ESipfield, a two-element string may fail to index — check the report and the mapping before running the production script. A single clean value at the app tier needs Kamal to exposeclient_ip_headerfor the managed proxy; 2.12.0 doesn't, so the app-tier proxy stays on Kamal's defaultv0.9.2for now.--client-ip-headermeans anything reaching the LB with aTrue-Client-IPheader sets its own logged address — butTrackTrueClientIpalready trusts that header unconditionally today, so this places no trust we weren't already placing.lbtag's provenance isn't recorded anywhere. Every flag the configure scripts pass (--read-target,--writer-affinity-timeout,--tls-acme-cache-path) exists in v0.10.0, and the one unmerged load-balancer branch addsX-Kamal-Writer, which fizzy doesn't reference. Still worth staging first.Rollout
Config changes only — no app deploy needed, but neither file takes effect on its own:
then the same for production.
Testing
bin/cigreen locally (every step;gh signoffonly wanted the branch pushed).🤖 Generated with Claude Code