Skip to content

fix(frankenphp): give PHP the visitor IP and HTTPS behind a proxy - #62

Merged
guillaume-sainthillier merged 1 commit into
mainfrom
fix/frankenphp-client-ip
Oct 12, 2026
Merged

guillaume-sainthillier merged 1 commit into
mainfrom
fix/frankenphp-client-ip

Conversation

@guillaume-sainthillier

Copy link
Copy Markdown
Member

Caddy resolves the visitor through trusted_proxies (private ranges + Cloudflare), but only for its own {client_ip}. PHP still got the proxy's address as REMOTE_ADDR, the raw X-Forwarded-For and no HTTPS, so Symfony either saw Traefik's IP or, with private_ranges, the Cloudflare edge IP (Symfony doesn't know Cloudflare's ranges).

Like mod_remoteip in the Apache images, php_server now gets:

  • REMOTE_ADDR = {client_ip} (the visitor, resolved right to left through the trusted chain)
  • HTTPS=on when the request is served over HTTPS or forwarded with X-Forwarded-Proto: https
  • no X-Forwarded-For, so PHP doesn't walk the chain a second time

The README gets a Running Behind a Reverse Proxy section: what the images trust, the Traefik forwardedHeaders.trustedIPs setting Cloudflare needs, and which Symfony trusted_proxies values to use.

Tested on the real Cloudflare → Traefik → container chain

On the SILARHI server, the published 8.5-frankenphp-bookworm image plus this Caddyfile was run as two temporary containers behind the production Traefik (websecure trusts exactly Cloudflare's 22 published ranges), at labs.silarhi.fr/proxytest (classic) and /proxytest-worker (symfony-worker-native.Caddyfile). A Symfony 8.1 app reported what Request sees with each trusted_proxies value. Removed afterwards.

Request Before (live /frankenphp) After, classic and worker
Normal visit REMOTE_ADDR = Traefik 172.18.0.12, HTTPS empty REMOTE_ADDR = my IP, HTTPS=on
Symfony, trusted_proxies empty Traefik IP, http:// my IP, https://labs.silarhi.fr
Symfony, private_ranges Cloudflare edge IP, https:// my IP, https://labs.silarhi.fr
Spoofed X-Forwarded-For: 6.6.6.6 / X-Forwarded-Proto: http through Cloudflare – ignored: my IP, https
Direct to the origin, bypassing Cloudflare – not possible: 80/443 are filtered

Locally, behind Traefik v3.5: same results for the classic and native worker modes, and no value carried over between two worker requests from different clients.

Not changed by this PR: with trusted_proxies: '127.0.0.1,REMOTE_ADDR' (Apache and FrankenPHP alike), Symfony trusts the visitor and takes a spoofed X-Forwarded-Port: 8443 into generated URLs (https://labs.silarhi.fr:8443), reproduced through Cloudflare. The README now says not to use it; scan4all/API-Wow, airbus/video-creator.airbus.com and eleo/training do locally.

Not tested: IPv6 visitors (no IPv6 on the test machine); the Cloudflare v6 ranges are in the trusted list.

The README section is appended after "Image Variants Explained", like #59's Configuration section: whichever merges second needs a trivial rebase.

Caddy resolved the visitor through trusted_proxies (private ranges and
Cloudflare), but only for its own {client_ip}: PHP still got the proxy's
address as REMOTE_ADDR, the raw X-Forwarded-For and no HTTPS. Behind
Cloudflare and Traefik, Symfony then saw Traefik's IP with no trusted
proxies, or the Cloudflare edge IP with private_ranges, which doesn't
include Cloudflare's ranges.

Like mod_remoteip in the Apache images, php_server now sets REMOTE_ADDR to
{client_ip} and HTTPS to "on" when the request was served over HTTPS or
forwarded with X-Forwarded-Proto: https, and X-Forwarded-For is removed so
PHP doesn't walk the chain again.

The README gets a "Running Behind a Reverse Proxy" section: what the images
trust, the Traefik setting Cloudflare needs, and which Symfony
trusted_proxies values to use.
@guillaume-sainthillier
guillaume-sainthillier merged commit 67add49 into main Oct 12, 2026
55 checks passed
@guillaume-sainthillier
guillaume-sainthillier deleted the fix/frankenphp-client-ip branch October 12, 2026 14:14
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