Tools · 17 min read · Updated

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.

Cloudflare rules for laravel app

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 typeDashboard locationFree plan allowance (at the time of writing)
Custom rules (security)Security → Security rules (older: Security → WAF → Custom rules)5 rules
Rate limiting rulesSecurity → Security rules (older: Security → WAF → Rate limiting rules)1 rule
Redirect rulesRules → Redirect Rules10 rules
Cache rulesCaching → 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 any RateLimiter you define count all your users together. One bot can lock everyone out.
  • Laravel doesn’t know the visitor used HTTPS, so url(), route() and asset() can generate http:// 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:

PathUsed bySafe to block or challenge?
/livewire/... (Livewire 3) or /livewire-<hash>/... (Livewire 4)Every Livewire and Filament action, file upload and the Livewire scriptNo. Never block, challenge or cache it
/api/...Mobile apps, SPAs and other serversDon’t challenge it: apps can’t solve a challenge. Use rate limiting instead
/sanctum/csrf-cookieSPA login with SanctumNo
/broadcasting/authPrivate channels with Laravel Echo, Reverb or PusherNo
/upLaravel’s health check, used by uptime monitors and load balancersNo. Don’t challenge or cache it
/stripe/webhook and other webhooksCashier, Paddle and other payment providersNo. A challenge breaks payments
/build/..., /storage/...Vite assets and files on the public diskNo. Cache them (see the cache section)
/vendor/...Assets that packages publish to public/vendor, such as Telescope and HorizonDon’t block the folder; rule 1 only blocks PHP files

Two habits make every rule below safer:

  1. 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.
  2. 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_KEY and API keys, and your source code. If the web root points at public/, 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 .env file.
  • /_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 .php file except /index.php: covers PHPUnit’s eval-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.10 with your own IP. Find it at https://example.com/cdn-cgi/trace, on the line starting ip=. 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 /_debugbar is 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/login or /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 throttle middleware 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 URL https://example.com/${1}
  • Section moved to a new folder: request URL https://example.com/docs/v1/*, target URL https://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

RedirectBest placeWhy
www ↔ bare domain, HTTP → HTTPSCloudflareApplies to every URL, and runs before Laravel boots
Old domain or whole folderCloudflare wildcard ruleOne rule covers thousands of URLs
Lists of old URLsCloudflare Bulk RedirectsUploaded as CSV, and doesn’t use your ten rule slots
A renamed routeRoute::permanentRedirect('/old', '/new') in routes/web.phpLives in the code, next to the route, and is deployed with it
Old slugs of database recordsLaravel, by looking up the old slug and redirectingOnly your app knows which record an old slug belonged to
Trailing slashesLaravel’s public/.htaccess, or your Nginx configAlready 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'));
    },
)
  • SubstituteBindings keeps route model binding working, so /blog/{post:slug} still works.
  • cache.headers is Laravel’s built-in middleware. s_maxage=3600 tells Cloudflare to keep the page for an hour, while max_age=300 lets browsers keep it for five minutes.
  • Pages in this file can’t use sessions: no login state, flash messages, @csrf forms 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: public and no cookies. Normal Laravel pages send private or no-cache headers 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=60 to 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:

ValueMeaning
HITServed from Cloudflare. Working
MISSNot cached yet. Reload and it should become HIT
BYPASSThe page sent private or no-cache, or a rule skipped it. Expected for normal Laravel pages
DYNAMICNot 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:

SettingWhereRecommendedNote
SSL modeSSL/TLS → OverviewFull (strict)Needs a valid certificate on your server; Forge and most hosts include one for free
Always Use HTTPSSSL/TLS → Edge CertificatesOnReplaces any HTTP-to-HTTPS redirect in your app or server
Minimum TLS versionSSL/TLS → Edge CertificatesTLS 1.2Older versions are insecure and almost unused
HSTSSSL/TLS → Edge CertificatesOn, only once HTTPS works everywhereBrowsers then refuse HTTP for the set time, so a mistake is hard to undo
Bot Fight ModeSecurity → BotsTest before keepingOn the Free plan, a rule can’t skip it, so it may block your API clients, webhooks or monitoring
Rocket LoaderSpeed → OptimizationOffIt changes how scripts load and often breaks Livewire, Alpine.js and Inertia

Three fixes outside Cloudflare

  • APP_DEBUG=false in 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 into public_html. Put the project one level up and make public_html point to its public folder.
  • Keep the scheduler running. The cloudflare:reload schedule 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_URL matching
  • Always Use HTTPS on, SSL mode Full (strict)
  • Public pages in routes/public.php with cache.headers, plus the cache rule and purge on save
  • Cache rule for Vite build assets
  • APP_DEBUG=false, web root on public/, scheduler running

How to test each rule

  1. Blocked paths: in a private window, open https://example.com/.env, https://example.com/_ignition/health-check and https://example.com/wp-login.php. All three should show a Cloudflare “blocked” page.
  2. Dashboards: open https://example.com/telescope on your phone with Wi-Fi turned off. It should be blocked; on your own connection it should load.
  3. Login challenge: open https://example.com/login in 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.
  4. Real IPs: temporarily log request()->ip() on a test route and open it. You should see your own IP, not one of Cloudflare’s.
  5. 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.
  6. 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.

Rupak Nepali
Written byRupak Nepali

Author of four Opencart book.
The recent are Opencart 4 developer book and Opencart 4 user manual

Leave a Reply

Your email address will not be published. Required fields are marked *