Tools · 18 min read · Updated

Cloudflare Rules for Drupal: The Security, Redirect, and Cache Rules Every Site Needs

Copy-ready Cloudflare rules for Drupal 10 and 11: block scanners and Drupalgeddon probes, challenge /user/login, stop JSON:API scraping, set up 301 redirects and cache anonymous pages, all on the Free plan.

cloudflare rules for drupal

A handful of Cloudflare rules stop most of the junk traffic that reaches a Drupal site: password guessing on /user/login, scanners looking for .env files and settings.php, bots still trying the Drupalgeddon exploits years after they were patched, and thousands of WordPress probes against a site that has no WordPress at all. The same rules can also cache your pages at the edge, so anonymous visitors never wait for PHP.

This guide lists the rules, ready to copy, for Drupal 10 and Drupal 11. Each one has the expression to paste, the action to pick, and what it might break. Replace example.com with your own domain. If you run WordPress too, see the companion guide, Cloudflare Rules for WordPress.

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
Configuration rulesRules → Configuration Rules10 rules

Cloudflare is retiring Page Rules, the old all-in-one rule type. If you still have Page Rules, move them to the rule types above.

To paste an expression, open a new rule and click Edit expression under the expression builder.

Before you start: make Drupal see real visitor IPs

Do this first. Once your site is behind Cloudflare, every request reaches your server from a Cloudflare IP address. If Drupal isn’t told about it, it believes all your visitors share a handful of IPs. Drupal’s built-in flood control blocks an IP after 50 failed logins in an hour, so one bot can lock everyone out of the login form. Your logs, Webform submissions and Ban module entries also record Cloudflare’s IPs instead of the visitor’s.

There are two ways to fix this:

  • Install the Cloudflare module. It restores the visitor’s IP from Cloudflare’s CF-Connecting-IP header, and it can purge Cloudflare’s cache when content changes (see the cache section).
  • Or configure the reverse proxy in settings.php, using the IP ranges published at cloudflare.com/ips:
$settings['reverse_proxy'] = TRUE;
$settings['reverse_proxy_addresses'] = [
  '173.245.48.0/20',
  '103.21.244.0/22',
  // ...add every IPv4 and IPv6 range from https://www.cloudflare.com/ips/
];

Many managed Drupal hosts (Pantheon, Acquia, Platform.sh and similar) already sit behind their own proxy and handle this for you. Check their documentation before adding the settings above.

Check what your site needs

Some Drupal paths look like attack targets but are used by real features. Check this list before you block anything:

PathUsed bySafe to block?
/jsonapi/, ?_format=jsonHeadless or decoupled front ends (Next.js, Gatsby), mobile apps, some JavaScript widgetsYes, unless your site is headless or a feature loads data from it (rule 3)
/system/files/Private file downloads, checked against Drupal permissionsNo. Never block it, and never cache it
/media/oembedRemote video embeds (YouTube, Vimeo) shown to visitorsNo
/batchLong admin tasks, imports, and some contrib modulesNo. Never block or cache it
/cron/<key>External cron services that run Drupal cron by URLNo. Don’t block or cache it
/update.phpDatabase updates after a core or module updateChallenge it (rule 2). Block it if you always run drush updatedb
/core/authorize.phpInstalling modules through the Update Manager screenYes, if you install modules with Composer (rule 1 blocks it)
.../statistics.phpNode view counters from the Statistics moduleNo. Rule 1 leaves it out

Two habits make every rule below safer:

  1. Deploy one rule at a time. After each one, browse the site, log in, save a node and submit a form.
  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 visitor or a service you use, add an exception.

Security rules

Three custom rules cover what most Drupal sites 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 exploit probes

A Drupal site only needs two PHP files in the browser: /index.php, which serves every page, and /update.php. Every other .php request is a probe: wp-login.php, xmlrpc.php, settings.php, /core/install.php, or a backdoor someone hopes is sitting in your files folder. This rule blocks all of them, plus files that should never be public.

  • Rule name: Block scanners and sensitive files
  • Action: Block
(http.request.uri.path contains "/.env")
or (http.request.uri.path contains "/.git")
or (http.request.uri.path in {"/composer.json" "/composer.lock" "/web.config" "/CHANGELOG.txt" "/core/CHANGELOG.txt" "/core/INSTALL.txt"})
or starts_with(http.request.uri.path, "/vendor/")
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 not http.request.uri.path in {"/index.php" "/update.php"}
    and not ends_with(http.request.uri.path, "/statistics.php"))
or ends_with(http.request.uri.path, ".sql")
or ends_with(http.request.uri.path, ".bak")
or ends_with(http.request.uri.path, ".yml")
or (http.request.uri.query contains "element_parents=" and http.request.uri.query contains "%23")

What each part stops:

  • .env, .git, /vendor/, composer.json: scanners looking for passwords, source code and your exact package versions. If your web root is the web/ folder (the standard Composer layout), these aren’t reachable anyway, and the rule stops the requests before they reach your server.
  • CHANGELOG.txt, INSTALL.txt: they reveal your Drupal version, which tells attackers which exploits to try. robots.txt is not affected.
  • /wp-admin, /wp-content, /wp-includes: WordPress probes. A Drupal site has no reason to answer them, and each one would otherwise run Drupal just to show a 404 page.
  • Any other .php file: covers wp-login.php, xmlrpc.php, /core/install.php, /core/rebuild.php, /core/authorize.php, settings.php, and PHP files planted in /sites/default/files/. If you install modules through the Update Manager screen, add "/core/authorize.php" to the list of allowed files.
  • .sql, .bak, .yml: database dumps, backups and configuration exports. Drupal’s .htaccess already protects .yml files on Apache, but Nginx sites often don’t, and blocking at Cloudflare covers both.
  • element_parents= with %23: the signature of the Drupalgeddon 2 exploit (CVE-2018-7600), which bots still send today. Drupal’s own file upload widget uses element_parents too, but never with an encoded # (%23), so real uploads keep working.

Rule 2: Challenge the login page and the admin area

Password-guessing bots hit /user/login and /user/password all day. A Managed Challenge stops them, while a real person clicks through once (or not at all) and logs in normally.

  • Rule name: Challenge login and admin
  • Action: Managed Challenge
(http.request.uri.path contains "/user/login"
  or http.request.uri.path contains "/user/password"
  or http.request.uri.path contains "/user/register"
  or http.request.uri.path eq "/admin"
  or starts_with(http.request.uri.path, "/admin/")
  or http.request.uri.path eq "/update.php")
and not ip.src in {203.0.113.10}
  • Replace 203.0.113.10 with your own IP so you are never challenged. Find it at https://example.com/cdn-cgi/trace, on the line starting ip=. If your IP changes often, delete the last line of the expression.
  • contains is used for the user pages so multilingual sites are covered too: /fr/user/login matches. If your admin pages also use a language prefix, add or http.request.uri.path contains "/admin/".
  • Password reset links (/user/reset/...) are left out on purpose. They come from your own emails and only work once.
  • If visitors can’t create their own accounts (Configuration → People → Account settings is set to “Administrators only”), you can block /user/register instead of challenging it.
  • If you show the User login block on public pages, its form posts to the page it sits on, not to /user/login, so this rule can’t protect it. Remove the block and link to /user/login instead.
  • If you are the only person who logs in and your IP is fixed, change the action to Block for even stronger protection.

Rule 3: Block anonymous access to JSON:API and REST

When the JSON:API module is on, /jsonapi/node/article hands your whole site to scrapers as neat JSON, page after page. Adding ?_format=json to a node or user URL does the same through the REST module. It also opens a second login, /user/login?_format=json, which password guessers use to avoid your login form. This rule blocks all of it for visitors who are not logged in.

  • Rule name: Block anonymous API
  • Action: Block
(starts_with(http.request.uri.path, "/jsonapi")
  or http.request.uri.query contains "_format=")
and not http.cookie contains "SESS"
  • Drupal names its login cookie SESS... on HTTP and SSESS... on HTTPS, followed by a long random string. Checking for SESS matches both, so logged-in editors keep full access.
  • Skip this rule on headless sites. If a Next.js or other front end reads content from Drupal, it needs the API. Exempt your front-end server instead by adding and not ip.src in {198.51.100.20} with its IP, or don’t use the rule.
  • Some Views use a REST export display on a custom path for maps, sliders or calendars. Those paths aren’t covered by the rule and keep working, but test any feature that loads data with ?_format=.
  • A bot can send a fake cookie, so this is a filter, not a lock. That’s fine: the API only returns content that anonymous visitors are allowed to see anyway.

Also check one permission in Drupal: under People → Permissions, untick View user information for Anonymous user. Otherwise /user/1, /user/2 and so on show your usernames to anyone.

Use your two free slots for exceptions you find in Security → Events, for example a Skip rule for an uptime monitor or a payment provider’s webhook in Drupal Commerce.

Rate limiting rule

One rate limiting rule slows down anything that gets past the challenge: bots that pass it once and then hammer the login and password reset forms, and comment spam. A real person never loads these pages more than a few times in ten seconds.

  • Rule name: Limit login, password and comment requests
  • 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 contains "/user/login")
or (http.request.uri.path contains "/user/password")
or (http.request.uri.path contains "/comment/reply/")
  • On the Free plan, rate limiting rules can only match on the URL path, so the rule counts both page views and form posts. Five in ten seconds still leaves plenty of room for a real person. On Pro and higher, add and http.request.method eq "POST" and raise the block duration to 10 minutes or more.
  • Drupal’s own flood control keeps working behind this rule, as long as Drupal sees real visitor IPs (see “Before you start”).
  • Keep spam protection on your forms as well, for example the Honeypot or Antibot module, or Cloudflare Turnstile through the Turnstile module. The rate limit only slows bots down; it doesn’t judge content.

Redirect rules

Redirects at Cloudflare happen before the request reaches your server, so they are faster than the Redirect module and don’t start Drupal. Use them for whole-domain and whole-section moves. Leave single-page redirects to Drupal (see the table at the end of this section).

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. Make sure $settings['trusted_host_patterns'] in settings.php allows the version you keep, and update $base_url if you set it.

  • 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. Set SSL/TLS → Overview to Full (strict) too. The Flexible mode sends traffic to your server over plain HTTP, and if your server or a module also forces HTTPS, you get the “too many redirects” error.

Redirect 3: An old domain or folder

When you move the whole site, 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/news/*, target URL https://example.com/blog/${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.

Redirect 4: A single old URL

Migrations often leave a few old addresses behind, such as a feed URL from Drupal 7. For a single URL, a static redirect works:

  • When incoming requests match: http.request.uri.path eq "/feed"
  • Then: Static, URL https://example.com/rss.xml

For long lists of old URLs, use Bulk Redirects (Rules → Redirect Rules → Bulk Redirects), which reads a list you upload as CSV and doesn’t use your ten rule slots. The Redirect module can also import the old Drupal 7 redirects during the migration.

What to leave to Drupal

RedirectBest placeWhy
www ↔ bare domain, HTTP → HTTPSCloudflareApplies to every URL, and runs before Drupal starts
Old domain or whole folderCloudflare wildcard ruleOne rule covers thousands of URLs
Lists of old URLsCloudflare Bulk Redirects, or the Redirect moduleBulk Redirects is faster; the module lets editors manage the list
Nodes whose alias changedRedirect module with PathautoIt creates the redirect automatically when an alias changes
/node/123 to its alias, trailing slash, upper/lower caseRedirect module (“Enforce clean and canonical URLs”)It knows every alias; a Cloudflare rule can’t, and can cause loops

Never redirect /user/login, /admin, /update.php, /batch or /core/: a wrong redirect there can lock you out of your own site.

Cache rules

By default, Cloudflare caches images, CSS and JavaScript but not your HTML pages, so every page view still reaches Drupal. Drupal’s Internal Page Cache makes those views fast, but they still use your server. Two cache rules fix that: one caches pages for anonymous visitors at Cloudflare, and one keeps static files longer.

Cache rule 1: Cache pages for anonymous visitors

  • Rule name: Cache Drupal pages
  • Cache eligibility: Eligible for cache
  • Edge TTL: Ignore cache-control header and use this TTL: 2 hours (raise it to 1 day once purging on save works)
  • Browser TTL: Respect origin
(http.host eq "example.com")
and not http.cookie contains "SESS"
and not starts_with(http.request.uri.path, "/admin")
and not starts_with(http.request.uri.path, "/user")
and not starts_with(http.request.uri.path, "/system/")
and not starts_with(http.request.uri.path, "/batch")
and not starts_with(http.request.uri.path, "/cron/")
and not starts_with(http.request.uri.path, "/jsonapi")
and not http.request.uri.path in {"/update.php" "/cart" "/checkout"}
and not starts_with(http.request.uri.path, "/checkout/")
and not http.request.uri.query contains "_format="
  • Why the cookie matters: Drupal only gives a visitor a SESS cookie when it needs to remember them: when they log in, add a product to a Drupal Commerce cart, or see a status message such as “Your message has been sent”. Those pages are personal and must never be cached and shown to someone else. Visitors without the cookie all see the same page, so it’s safe to cache.
  • /system/ covers private files (/system/files/) and Drupal’s AJAX callbacks. Private files are checked against permissions on every request, so caching them would give them to anyone.
  • /cron/ must reach Drupal every time, otherwise an external cron service gets a cached reply and cron never runs.
  • CAPTCHA forms: the CAPTCHA module puts a one-time token in the form, so a cached copy shows an expired token and the form fails. Leave those pages out by path, or switch to Cloudflare Turnstile or Antibot, which work with cached pages.
  • Purge when you save. Without it, an updated node can take up to the Edge TTL to appear for visitors. The Cloudflare module, together with the Purge module, clears Cloudflare’s cache when content changes. Where your plan supports purge by cache tag, it clears only the pages that show the changed content, the same way Drupal clears its own cache. Without the module, purge by hand after big changes in Caching → Configuration → Purge Cache.

Cache rule 2: Keep core, module, theme and file assets longer

Drupal combines CSS and JavaScript into files in /sites/default/files/css/ and /sites/default/files/js/, and each new version gets a new file name. Drupal also adds a short query string to the core, module and theme files it links, and changes it when you update or clear caches. Because a changed file gets a new URL, long caching is safe.

  • Rule name: Cache static files longer
  • Cache eligibility: Eligible for cache
  • Edge TTL: 1 month
  • Browser TTL: 1 month
starts_with(http.request.uri.path, "/core/")
or starts_with(http.request.uri.path, "/modules/")
or starts_with(http.request.uri.path, "/themes/")
or starts_with(http.request.uri.path, "/libraries/")
or starts_with(http.request.uri.path, "/sites/default/files/")
  • If you have a multisite, replace /sites/default/files/ with /sites/ so every site’s public files are covered.
  • Image styles (/sites/default/files/styles/...) are generated on the first request and then served from disk, so they cache well too. After you change an image style’s settings, purge Cloudflare’s cache so the new versions appear.

Check that caching works

Open a 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
BYPASSA rule or cookie skipped the cache. Expected when logged in
DYNAMICNot cached. The rule didn’t match, or the page sends a Set-Cookie header (often a module starting a session for every visitor)

Drupal adds its own header, X-Drupal-Cache. On a MISS from Cloudflare it shows whether Drupal’s Internal Page Cache answered (HIT) or the page was built from scratch (MISS). Keep the Internal Page Cache module on: it’s the second layer behind Cloudflare.

Two fixes outside Cloudflare

  • Replace Automated Cron. Drupal’s Automated Cron module runs cron during visitor requests. Once Cloudflare serves your pages, far fewer requests reach Drupal and cron runs late or not at all. Uninstall Automated Cron and add a server cron job instead, for example */15 * * * * cd /var/www/example && vendor/bin/drush cron.
  • Find modules that start sessions. If pages show DYNAMIC for anonymous visitors, a module is probably setting a cookie on every page. Check the response headers for Set-Cookie, then look for modules that store data in the session for anonymous users, such as some cookie banners, A/B testing and “recently viewed” modules.

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; most hosts include one for free
Always Use HTTPSSSL/TLS → Edge CertificatesOnReplaces any HTTP-to-HTTPS redirect rule
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. If the Security Kit module already sends HSTS, set it in one place only
Block AI botsSecurity → BotsYour choiceStops AI crawlers that ignore robots.txt from copying your content
Bot Fight ModeSecurity → BotsTest before keepingOn the Free plan, a rule can’t skip it, so it may block payment webhooks, feeds or other services
Rocket LoaderSpeed → OptimizationOffIt changes how scripts load and often breaks Drupal’s JavaScript (Drupal.behaviors), AJAX forms, and CKEditor 5

Checklist

  • Drupal sees real visitor IPs (Cloudflare module or reverse_proxy settings)
  • Security rule 1: Block scanners and sensitive files
  • Security rule 2: Challenge login and admin
  • Security rule 3: Block anonymous API (skip on headless sites)
  • Rate limiting: Limit login, password and comment requests
  • Redirect: www to root (or root to www), with trusted_host_patterns updated
  • Always Use HTTPS on, SSL mode Full (strict)
  • Cache rule 1: Cache Drupal pages, with purging on save
  • Cache rule 2: Cache static files longer
  • Automated Cron replaced by a server cron job

How to test each rule

  1. Blocked paths: in a private window, open https://example.com/.env, https://example.com/core/CHANGELOG.txt, https://example.com/wp-login.php and https://example.com/jsonapi. All four should show a Cloudflare “blocked” page.
  2. Login challenge: open https://example.com/user/login in a private window. You should see a short “Verifying you are human” check, then the login form.
  3. Your own access: log in, edit and save a node, upload an image, run Clear all caches under Configuration → Performance, and submit any forms on the site.
  4. Redirects: open http://www.example.com/some-page?utm_source=test. You should land on https://example.com/some-page?utm_source=test in a single hop.
  5. Events: after a day, open Security → Events and filter by each rule name. Check that the blocked requests are bots, not visitors or services you use.

FAQ

Will these rules hurt my SEO? No. Search engines don’t request .env files, log in, or read JSON:API; they read your pages. Cloudflare also recognises verified search engine bots. Your sitemap (for example /sitemap.xml from the Simple XML Sitemap module) and robots.txt are not affected.

I locked myself out of /user/login. What now? In Cloudflare, open Security → Security rules and switch off the login rule with its toggle. Then add your current IP to the rule and switch it back on. If Drupal itself says “There have been more than 5 failed login attempts”, that’s Drupal’s flood control: wait, or clear it with drush sql:query "DELETE FROM flood", and check that Drupal sees real visitor IPs.

Do I still need Drupal security modules? The rules stop most attacks before they reach Drupal, so modules have less to do. Keep core and contributed modules updated (the rules don’t fix an outdated module), follow Drupal security advisories, and use strong passwords with two-factor login through the TFA module.

Does this work for Drupal 7? Drupal 7 reached end of life on 5 January 2025, so the best fix is to upgrade. If you must keep it running, the ideas are the same, but the paths differ: Drupal 7 has cron.php, install.php, xmlrpc.php and CHANGELOG.txt in the web root, and many old sites use ?q=user/login URLs.

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 OWASP Core Ruleset explained, Cloudflare Workers use cases, and Cloudflare security for OpenCart.

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 *