Cloudflare in front of Laravel breaks request()->ip() — every request appears to come from Cloudflare's edge IPs. Sessions still work, but rate limiting, geolocation, and audit logs all see the wrong IP. Here's the correct fix.
The Problem
Without Cloudflare:
Browser (203.0.113.42) → Server
request()->ip() = '203.0.113.42'
With Cloudflare:
Browser (203.0.113.42) → Cloudflare (172.69.0.X) → Server
request()->ip() = '172.69.0.X' ❌ wrong
Cloudflare adds a CF-Connecting-IP header with the real IP. Laravel needs to know it can trust that header.
The Fix — TrustProxies
Laravel's App\Http\Middleware\TrustProxies middleware controls which proxies are trusted and which header to read. By default it's restrictive.
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
protected $proxies = '*'; // Trust all proxies — only safe behind Cloudflare
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO;
}
'*' looks dangerous but is correct if and only if your origin server is firewalled to only accept traffic from Cloudflare. If anyone can hit your origin directly, an attacker can spoof X-Forwarded-For and bypass IP-based controls.
Lock Down the Origin
Get Cloudflare's IP ranges:
curl https://www.cloudflare.com/ips-v4 -o /tmp/cf-ips-v4
curl https://www.cloudflare.com/ips-v6 -o /tmp/cf-ips-v6
UFW rules:
# Allow Cloudflare IPs
while read ip; do
sudo ufw allow from "$ip" to any port 443
done < /tmp/cf-ips-v4
# Block direct origin access
sudo ufw deny 80
sudo ufw deny 443
sudo ufw enable
Now your origin only accepts HTTPS from Cloudflare. Direct hits get dropped.
Read the Real IP
After TrustProxies runs:
request()->ip(); // 203.0.113.42 ✓
Or read the Cloudflare-specific header:
request()->header('CF-Connecting-IP'); // 203.0.113.42
I prefer request()->ip() because it works whether you're behind Cloudflare, AWS ALB, or no proxy.
Geolocation
Cloudflare exposes geolocation in headers (free tier includes country):
$country = request()->header('CF-IPCountry'); // 'US', 'BD', 'UK'
For paid plans, you also get city, postal code, and continent. No third-party geo-IP service needed.
Real Visitor Country in Logs
Add the Cloudflare IP to your access log format. In Caddy:
log {
output file /var/log/caddy/access.log
format json {
time_format iso8601
message_key msg
}
}
Then post-process:
jq '.request.headers."CF-Connecting-IP"[0]' /var/log/caddy/access.log
Cloudflare-Specific Quirks
Bot Fight Mode. Cloudflare blocks "obviously bot" traffic before it hits your origin. The first time a search engine indexes your site after enabling this, indexing stops. Disable for /sitemap.xml and /robots.txt:
Page Rules:
/sitemap.xml → Security Level: Essentially Off
/robots.txt → Security Level: Essentially Off
Caching dynamic content. Cloudflare caches by URL by default — including pages that depend on session state. A logged-out visitor's homepage gets cached and served to logged-in users. Either:
- Disable caching for HTML (Page Rule:
*→ Cache Level: Bypass) - Or set
Cache-Control: privateon session-aware pages
Argo Smart Routing. The $5/month upgrade routes traffic over Cloudflare's backbone instead of public internet. For users in regions far from your origin, p99 latency drops 30-50%. Worth it for global apps.
What I Skip
- Cloudflare Access for staging — overkill for personal projects, use HTTP basic auth in Caddy instead.
- Workers in front of Laravel — adds latency and complexity for marginal gain.
- R2 for assets — public/build is small enough that serving from origin is fine.
The one Cloudflare feature I always enable: HTTP/3. Free, faster, and connection migration over wifi switches is genuinely useful.
Al Amin Ahamed
Senior software engineer & AI practitioner. Laravel, PHP, WordPress plugins, WooCommerce extensions.
About me →More from the blog
← Older
Building a RAG Pipeline in Laravel with pgvector
Newer →
Supervisor for Laravel: Queue Workers, Scheduler, Horizon
One email a month. No noise.
What I shipped, what I read, occasional deep dive. Unsubscribe anytime.
Check your inbox — confirmation link sent.