Cloudflare Rules for Laravel: The Security, Redirect, and Cache Rules Every App Needs
Copy-ready Cloudflare rules for Laravel 11, 12 and 13: trust Cloudflare IPs, block .env and Ignition probes, lock Telescope and Horizon to your IP, challenge /login, and cache public pages safely.

A handful of Cloudflare rules stop most of the junk traffic that reaches a Laravel app: bots hunting for your .env file, scanners trying the old Ignition and PHPUnit exploits, password guessing on /login, and strangers poking at Telescope, Horizon and log viewers. The same setup also gets Laravel to see real visitor IPs behind Cloudflare. Without that, the login throttle sees one shared IP and can lock out every user at once.
This guide lists the rules, ready to copy, for Laravel 11, 12 and 13. Each one has the expression to paste, the action to pick, and what it might break. Replace example.com with your own domain. This is the third guide in the series, after Cloudflare Rules for WordPress and Cloudflare Rules for Drupal.
Where the rules live
You create every rule below in the Cloudflare dashboard for your domain. The menu names have moved over the years, so look for these:
| Rule type | Dashboard location | Free plan allowance (at the time of writing) |
|---|---|---|
| Custom rules (security) | Security → Security rules (older: Security → WAF → Custom rules) | 5 rules |
| Rate limiting rules | Security → Security rules (older: Security → WAF → Rate limiting rules) | 1 rule |
| Redirect rules | Rules → Redirect Rules | 10 rules |
| Cache rules | Caching → Cache Rules (or Rules → Cache Rules) | 10 rules |
To paste an expression, open a new rule and click Edit expression under the expression builder.
Before you start: trust Cloudflare in Laravel
Do this first. Behind Cloudflare, every request reaches your server from a Cloudflare IP address. Until Laravel trusts those addresses:
$request->ip()returns a Cloudflare IP, so the login throttle in Breeze, Jetstream and Fortify (5 attempts per email and IP) and anyRateLimiteryou define count all your users together. One bot can lock everyone out.- Laravel doesn’t know the visitor used HTTPS, so
url(),route()andasset()can generatehttp://links. That causes mixed-content warnings and redirect loops. - Your logs, audit trails and fraud checks record Cloudflare’s IPs instead of the visitor’s.
The easiest fix is the monicahq/laravel-cloudflare package, which loads Cloudflare’s current IP ranges and trusts only those. Install it with composer require monicahq/laravel-cloudflare, then replace the middleware in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) {
$middleware->replace(
\Illuminate\Http\Middleware\TrustProxies::class,
\Monicahq\Cloudflare\Http\Middleware\TrustProxies::class
);
})
Cloudflare’s IP ranges change occasionally, so refresh them daily in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('cloudflare:reload')->daily();
If you’d rather not add a package, list the ranges from cloudflare.com/ips yourself:
->withMiddleware(function (Middleware $middleware) {
$middleware->trustProxies(at: [
'173.245.48.0/20',
'103.21.244.0/22',
// ...every IPv4 and IPv6 range from https://www.cloudflare.com/ips/
]);
})
Don’t use trustProxies(at: '*') unless your server only accepts traffic from Cloudflare (through a firewall or Cloudflare Tunnel). Otherwise anyone can fake their IP with an X-Forwarded-For header. Laravel Forge, Vapor and Cloud users should check their platform’s documentation first, as some set this up for you.
Check what your app needs
Some Laravel paths look like attack targets but are used by real features. Check this list before you block or challenge anything:
| Path | Used by | Safe to block or challenge? |
|---|---|---|
/livewire/... (Livewire 3) or /livewire-<hash>/... (Livewire 4) | Every Livewire and Filament action, file upload and the Livewire script | No. Never block, challenge or cache it |
/api/... | Mobile apps, SPAs and other servers | Don’t challenge it: apps can’t solve a challenge. Use rate limiting instead |
/sanctum/csrf-cookie | SPA login with Sanctum | No |
/broadcasting/auth | Private channels with Laravel Echo, Reverb or Pusher | No |
/up | Laravel’s health check, used by uptime monitors and load balancers | No. Don’t challenge or cache it |
/stripe/webhook and other webhooks | Cashier, Paddle and other payment providers | No. A challenge breaks payments |
/build/..., /storage/... | Vite assets and files on the public disk | No. Cache them (see the cache section) |
/vendor/... | Assets that packages publish to public/vendor, such as Telescope and Horizon | Don’t block the folder; rule 1 only blocks PHP files |
Two habits make every rule below safer:
- Deploy one rule at a time. After each one, log in, submit a form, and use a Livewire or Inertia page if your app has them.
- Watch Security → Events for a day. Every request a rule blocks or challenges appears there with the rule name. If you see a real user or a service you use, add an exception.
Security rules
Three custom rules cover what most Laravel apps need, leaving two of the five Free plan slots for your own exceptions. Create them in this order, because Cloudflare runs custom rules from top to bottom.
Rule 1: Block scanners, stray PHP files and known exploit paths
A Laravel app sends every request through public/index.php, so no other .php file should ever be requested. Bots request them all day anyway: wp-login.php, xmlrpc.php, and eval-stdin.php from old PHPUnit installs (CVE-2017-9841). They also look for .env files, logs and the Ignition endpoint that allowed remote code execution on apps left in debug mode (CVE-2021-3129).
- Rule name:
Block scanners and sensitive files - Action: Block
(http.request.uri.path contains "/.env")
or (http.request.uri.path contains "/.git")
or starts_with(http.request.uri.path, "/_ignition/")
or starts_with(http.request.uri.path, "/storage/logs/")
or (http.request.uri.path in {"/composer.json" "/composer.lock" "/artisan" "/phpunit.xml" "/package.json"})
or ends_with(http.request.uri.path, "/installed.json")
or starts_with(http.request.uri.path, "/wp-admin")
or starts_with(http.request.uri.path, "/wp-content")
or starts_with(http.request.uri.path, "/wp-includes")
or (ends_with(http.request.uri.path, ".php") and http.request.uri.path ne "/index.php")
or ends_with(http.request.uri.path, ".log")
or ends_with(http.request.uri.path, ".sql")
or ends_with(http.request.uri.path, ".bak")
What each part stops:
.env,.git: your database password,APP_KEYand API keys, and your source code. If the web root points atpublic/, as it should, these aren’t reachable anyway. On shared hosting where the web root is the project folder, this rule is what stands between bots and your.envfile./_ignition/: the Ignition error page’s endpoints. They only exist in debug mode, which should never be on in production, but blocking them costs nothing.composer.json,installed.json,artisan,phpunit.xml: files that reveal your exact package versions, which tells attackers which vulnerabilities to try.- WordPress paths: a Laravel app has no reason to answer them, and each one would otherwise boot Laravel just to return a 404.
- Any
.phpfile except/index.php: covers PHPUnit’seval-stdin.php, WordPress files and uploaded web shells. Published assets in/vendor/(CSS and JavaScript) are not affected. .log,.sql,.bak: logs, database dumps and backups. Laravel logs contain stack traces, queries and sometimes personal data.
Rule 2: Lock developer dashboards to your IP
Telescope, Horizon, Pulse and log viewers show requests, queries, jobs, exceptions and sometimes user data. Each has its own authorization gate, but a single mistake in that gate exposes everything. Only you need these pages, so block everyone else at Cloudflare.
- Rule name:
Block dashboards except admin IP - Action: Block
(starts_with(http.request.uri.path, "/telescope")
or starts_with(http.request.uri.path, "/horizon")
or starts_with(http.request.uri.path, "/pulse")
or starts_with(http.request.uri.path, "/log-viewer")
or starts_with(http.request.uri.path, "/_debugbar"))
and not ip.src in {203.0.113.10}
- Replace
203.0.113.10with your own IP. Find it athttps://example.com/cdn-cgi/trace, on the line startingip=. Add your team’s IPs separated by spaces. - If your IP changes often, change the action to Managed Challenge and remove the last line, and rely on the dashboards’ own gates.
- If you changed a dashboard’s path in its config file (for example
TELESCOPE_PATH), use your path in the rule. - Debugbar should never be installed on production, which is why
/_debugbaris in the list.
Rule 3: Challenge the login pages and the admin panel
Password-guessing bots target /login, and spam bots hit /register and /forgot-password, which sends an email for every request. A Managed Challenge stops them, while a real person clicks through once (or not at all).
- Rule name:
Challenge login and admin - Action: Managed Challenge
(http.request.uri.path in {"/login" "/register" "/forgot-password"}
or http.request.uri.path eq "/admin"
or starts_with(http.request.uri.path, "/admin/")
or starts_with(http.request.uri.path, "/nova"))
and not ip.src in {203.0.113.10}
- The paths are the defaults for Breeze, Jetstream and Fortify, Filament (
/admin) and Nova (/nova). Change them to match your routes. - The challenge runs when the visitor opens the page. After that, the form posts, Livewire and Inertia requests on the page work normally, because the browser keeps Cloudflare’s clearance cookie.
- Don’t challenge API logins. If a mobile app or an SPA on another domain logs in through
/api/loginor/login, it can’t solve a challenge. Leave that path out and rely on rate limiting. - Password reset links (
/reset-password/...) are left out on purpose. They come from your own emails and only work once.
Use your two free slots for exceptions you find in Security → Events, for example a Skip rule for a monitoring service, or for a partner’s server that calls your API.
Rate limiting rule
One rate limiting rule slows down bots that get past the challenge and hammer the login, registration and password reset forms.
- Rule name:
Limit login, register and password reset - Counting by: IP
- Rate: 5 requests per 10 seconds
- Action: Block for 10 seconds (on the Free plan, 10 seconds is usually the only period and duration you can pick)
http.request.uri.path in {"/login" "/register" "/forgot-password"}
- On the Free plan, rate limiting rules can only match on the URL path, so the rule counts page views and form posts together. Five in ten seconds still leaves plenty of room for a real person. On Pro and higher, add
and http.request.method eq "POST", raise the duration, and add a second rule for/api/. - Livewire login forms (the Breeze Livewire stack and Filament) post to the Livewire endpoint, not to
/login, so this rule only counts the page views. Laravel’s own login throttle still protects the form, as long as Laravel sees real visitor IPs (see “Before you start”). - Keep Laravel’s throttling on as well: the
throttlemiddleware on API routes and the login limiter in Fortify or Breeze. Cloudflare slows bots down at the edge; Laravel’s limits work per user and per email.
Redirect rules
Redirects at Cloudflare happen before the request reaches your server, so they are faster than redirects in Laravel and don’t boot the framework. Use them for whole-domain and whole-section moves.
For every redirect below, use status 301 (permanent) and tick Preserve query string, so tracking links such as ?utm_source= survive the redirect.
Redirect 1: www to the bare domain
Pick one version of your domain and send the other to it. This example sends www.example.com to example.com. Set APP_URL in your .env file to the same version, because Laravel uses it for links in emails, queued jobs and console commands.
- Rule name:
www to root - When incoming requests match:
http.host eq "www.example.com" - Then: Dynamic, with the expression
concat("https://example.com", http.request.uri.path)
The www DNS record must be proxied (orange cloud) for Cloudflare to see the request. To go the other way, swap the two host names.
Redirect 2: HTTP to HTTPS
You don’t need a rule for this. Turn on SSL/TLS → Edge Certificates → Always Use HTTPS, and set SSL/TLS → Overview to Full (strict). In Flexible mode, Cloudflare talks to your server over plain HTTP. If your app also forces HTTPS, for example with URL::forceScheme('https') or a redirect middleware, you get the “too many redirects” error.
Redirect 3: An old domain or folder
When you move the whole app, or a section of it, use a wildcard redirect so every old URL lands on its new address.
- Old domain to new domain: request URL
https://olddomain.com/*, target URLhttps://example.com/${1} - Section moved to a new folder: request URL
https://example.com/docs/v1/*, target URLhttps://example.com/docs/${1}
In the rule, choose Wildcard pattern as the matching type. The * in the request URL captures the rest of the path, and ${1} puts it back in the target.
What to leave to Laravel
| Redirect | Best place | Why |
|---|---|---|
| www ↔ bare domain, HTTP → HTTPS | Cloudflare | Applies to every URL, and runs before Laravel boots |
| Old domain or whole folder | Cloudflare wildcard rule | One rule covers thousands of URLs |
| Lists of old URLs | Cloudflare Bulk Redirects | Uploaded as CSV, and doesn’t use your ten rule slots |
| A renamed route | Route::permanentRedirect('/old', '/new') in routes/web.php | Lives in the code, next to the route, and is deployed with it |
| Old slugs of database records | Laravel, by looking up the old slug and redirecting | Only your app knows which record an old slug belonged to |
| Trailing slashes | Laravel’s public/.htaccess, or your Nginx config | Already handled; a Cloudflare rule can cause loops |
Never redirect /livewire, /api, /broadcasting, /sanctum or your webhook paths. Apps and payment providers don’t follow redirects on POST requests the way browsers do.
Cache rules
Caching HTML at Cloudflare works differently in Laravel than in WordPress or Drupal. Every route in routes/web.php runs the web middleware group, which starts a session and sets the session and XSRF-TOKEN cookies on every response, even for guests. Pages with forms also contain a CSRF token for that visitor. If Cloudflare cached such a page, every visitor would get the same token, and their form posts would fail with a “419 Page Expired” error.
So don’t cache Laravel pages with a path-based rule. Instead, let Laravel mark the pages that are safe to cache, and have Cloudflare follow those headers.
Step 1: Serve public pages without sessions
Put routes that look the same for everyone (blog posts, documentation, landing pages) in their own route file, for example routes/public.php, and load it without the web group in bootstrap/app.php:
use Illuminate\Support\Facades\Route;
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
then: function () {
Route::middleware([
\Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers:public;max_age=300;s_maxage=3600;etag',
])->group(base_path('routes/public.php'));
},
)
SubstituteBindingskeeps route model binding working, so/blog/{post:slug}still works.cache.headersis Laravel’s built-in middleware.s_maxage=3600tells Cloudflare to keep the page for an hour, whilemax_age=300lets browsers keep it for five minutes.- Pages in this file can’t use sessions: no login state, flash messages,
@csrfforms or$errors. Give them a layout without those, or load the personal parts (such as “Log in” or the user’s name) with JavaScript. - Keep each route in one file only. If the same URL is defined in both files, one silently replaces the other.
Step 2: Cache rule that follows Laravel’s headers
- Rule name:
Cache pages Laravel marks public - Cache eligibility: Eligible for cache
- Edge TTL: Use cache-control header if present, bypass cache if not
- Browser TTL: Respect origin
(http.host eq "example.com")
and not starts_with(http.request.uri.path, "/livewire")
and not starts_with(http.request.uri.path, "/api/")
and not starts_with(http.request.uri.path, "/broadcasting")
and not starts_with(http.request.uri.path, "/sanctum")
and not http.request.uri.path in {"/up" "/login" "/register" "/forgot-password"}
- Why this is safe: Cloudflare only caches responses that send
Cache-Control: publicand no cookies. Normal Laravel pages sendprivateorno-cacheheaders and session cookies, so they are never cached. The path exclusions are a second safety net. starts_with("/livewire")covers Livewire 3 (/livewire/) and Livewire 4 (/livewire-<hash>/).- Responses from API routes can opt in the same way: add
cache.headers:public;s_maxage=60to a public, read-only endpoint, and remove/api/from the exclusions.
Step 3: Purge when content changes
When a post is updated, clear its URL from Cloudflare so visitors don’t wait for the cache to expire. Create an API token in Cloudflare with the Zone → Cache Purge permission, add it and your zone ID to config/services.php, and call the purge API from a model observer:
use Illuminate\Support\Facades\Http;
public function saved(Post $post): void
{
Http::withToken(config('services.cloudflare.token'))
->post('https://api.cloudflare.com/client/v4/zones/'.config('services.cloudflare.zone').'/purge_cache', [
'files' => [route('blog.show', $post), route('blog.index')],
]);
}
Put the call in a queued job if you don’t want saving a post to wait for Cloudflare’s reply.
Cache rule 2: Keep Vite assets for a year
Vite builds your CSS and JavaScript into public/build/assets with a hash in every file name. A changed file gets a new name, so these files can be cached for a year without risk.
- Rule name:
Cache Vite build assets - Cache eligibility: Eligible for cache
- Edge TTL: 1 year
- Browser TTL: 1 year
starts_with(http.request.uri.path, "/build/assets/")
Files on the public disk (/storage/...) keep Cloudflare’s default caching for images. If users can replace a file under the same name, keep the browser TTL short for that folder.
Check that caching works
Open a public page in a private window, then look at the cf-cache-status response header in your browser’s developer tools (Network tab). Reload once:
| Value | Meaning |
|---|---|
HIT | Served from Cloudflare. Working |
MISS | Not cached yet. Reload and it should become HIT |
BYPASS | The page sent private or no-cache, or a rule skipped it. Expected for normal Laravel pages |
DYNAMIC | Not cached. The rule didn’t match, or the page set cookies. If you see a laravel-session (older apps: laravel_session) or XSRF-TOKEN cookie on a page you meant to cache, the route is still in routes/web.php |
Settings that aren’t rules
A few switches do as much as the rules above. Check each one once:
| Setting | Where | Recommended | Note |
|---|---|---|---|
| SSL mode | SSL/TLS → Overview | Full (strict) | Needs a valid certificate on your server; Forge and most hosts include one for free |
| Always Use HTTPS | SSL/TLS → Edge Certificates | On | Replaces any HTTP-to-HTTPS redirect in your app or server |
| Minimum TLS version | SSL/TLS → Edge Certificates | TLS 1.2 | Older versions are insecure and almost unused |
| HSTS | SSL/TLS → Edge Certificates | On, only once HTTPS works everywhere | Browsers then refuse HTTP for the set time, so a mistake is hard to undo |
| Bot Fight Mode | Security → Bots | Test before keeping | On the Free plan, a rule can’t skip it, so it may block your API clients, webhooks or monitoring |
| Rocket Loader | Speed → Optimization | Off | It changes how scripts load and often breaks Livewire, Alpine.js and Inertia |
Three fixes outside Cloudflare
APP_DEBUG=falsein production. Debug mode shows stack traces, environment variables and (with old Ignition versions) allowed remote code execution. No Cloudflare rule replaces this setting.- Point the web root at
public/. On shared hosting, never upload the whole project intopublic_html. Put the project one level up and makepublic_htmlpoint to itspublicfolder. - Keep the scheduler running. The
cloudflare:reloadschedule above, queued purges and your own tasks need a server cron entry:* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1.
Checklist
- Laravel trusts Cloudflare’s IPs (laravel-cloudflare package or
trustProxies) - Security rule 1: Block scanners and sensitive files
- Security rule 2: Block dashboards except admin IP
- Security rule 3: Challenge login and admin
- Rate limiting: Limit login, register and password reset
- Redirect: www to root (or root to www), with
APP_URLmatching - Always Use HTTPS on, SSL mode Full (strict)
- Public pages in
routes/public.phpwithcache.headers, plus the cache rule and purge on save - Cache rule for Vite build assets
APP_DEBUG=false, web root onpublic/, scheduler running
How to test each rule
- Blocked paths: in a private window, open
https://example.com/.env,https://example.com/_ignition/health-checkandhttps://example.com/wp-login.php. All three should show a Cloudflare “blocked” page. - Dashboards: open
https://example.com/telescopeon your phone with Wi-Fi turned off. It should be blocked; on your own connection it should load. - Login challenge: open
https://example.com/loginin a private window. You should see a short “Verifying you are human” check, then the login form. Log in to confirm the form still works. - Real IPs: temporarily log
request()->ip()on a test route and open it. You should see your own IP, not one of Cloudflare’s. - Your app’s features: use a Livewire or Inertia page, upload a file, trigger a broadcast, and run a test payment so the webhook fires.
- Events: after a day, open Security → Events and filter by each rule name. Check that the blocked requests are bots, not users or services you rely on.
FAQ
Will these rules break my API? No, as long as you don’t challenge API paths. Rule 1 only blocks files that no API uses, and rules 2 and 3 only cover dashboards and browser login pages. If a mobile app uses /login on the same domain, move its login to /api/login or remove /login from rule 3.
I locked myself out of Telescope or the admin panel. What now? In Cloudflare, open Security → Security rules and switch off the rule with its toggle. Then add your current IP to the rule and switch it back on.
Do I still need Laravel’s own rate limiting and CSRF protection? Yes. Cloudflare filters bots before they reach your server, but it doesn’t know your users. Keep CSRF protection, the login throttle and API rate limits on.
Does this work for Laravel 10 and older? The Cloudflare rules are the same. The Laravel code differs: older apps configure trusted proxies in app/Http/Middleware/TrustProxies.php and routes in app/Providers/RouteServiceProvider.php.
Can I use these rules on the Free plan? Yes. Everything in this guide fits the Free plan: three of the five custom rules, the one rate limiting rule, and a few of the ten redirect and cache rules.
More Cloudflare guides: Cloudflare Rules for WordPress, Cloudflare Rules for Drupal, Cloudflare OWASP Core Ruleset explained and Cloudflare Workers use cases.







