Skip to main content
drupalreleases

Use direct release data with your AI assistant.

Learn more

Basic Firewall

Security covered Under active development 21 sites

Part of the Basic ecosystem · 10 projects

Basic Firewall evaluates incoming requests early in the request process, before Drupal's routing or authentication, to allow, challenge, or block them based on configurable rules. It offers an administrative interface for managing rules and presets, ensuring that blocked traffic is handled efficiently and securely.

Basic Firewall evaluates every incoming request against a set of rules and allows, challenges or blocks it. All of that happens before Drupal routes the request, starts a session, authenticates anyone, or checks the page cache. Traffic you reject costs almost nothing to serve.

It's built on kanopi/firewall (Lite Firewall) and puts every feature of that library behind a full administrative interface, so nothing needs to be configured by hand in YAML.

Why evaluate this early

The module registers an HTTP middleware at priority 280. That places it after Drupal's reverse_proxy middleware, so the client IP address has already been resolved and can be trusted. It also places it before page_cache, routing, sessions and authentication, so a rejected request never reaches any of them.

Running after the reverse proxy layer matters more than it sounds. If trusted proxies aren't set up, an attacker can forge an X-Forwarded-For header and walk straight through IP allow-lists, block-lists and per-IP rate limits. The status page reports whether your proxy configuration makes the client IP trustworthy.

For sites that want the earliest possible evaluation, an optional settings.php snippet runs the firewall before the Drupal container is built at all. You don't need it: the middleware already runs ahead of anything expensive.

Configuration is the source of truth

Rules live in ordinary Drupal configuration, so they export, import and diff like anything else. That configuration is compiled into a single YAML file in the private directory, in the library's own format, and that file is what the firewall reads at runtime. The library parses it once and caches the result, so loading the rules involves no database query and no configuration API.

The compiled file is a cache, and it's safe to delete. It's rebuilt whenever firewall settings are saved, configuration is imported, or caches are rebuilt. Exported configuration records only which presets are enabled, not their hundreds of individual patterns, so enabling a preset never floods a configuration diff.

It always fails open

If the compiled file is missing, unreadable or invalid, the request is allowed through, and the problem is reported on the status page and in Drupal's status report. A firewall misconfiguration will never be the reason your site is unreachable.

"Allowed through" is exactly the state you wouldn't notice, so the status report checks three separate things: whether the configuration is compiled, whether it's current, and whether the library can actually load it. A file that exists but can't be read is reported as an error, not as healthy. The settings forms refuse values the library would refuse to start with, so a typo can't quietly switch the firewall off.

Rule types

Rule type Matches on Notes IP address Client IP Single addresses, CIDR blocks, start-end ranges. IPv4 and IPv6. Request / URL Method, host, path, scheme, port, query, POST body, headers, cookies The workhorse. Can also count query values, to cap facet crawling. User agent Automated flag, bot flag, device, browser, OS, brand, model Parsed, not string-matched. Geolocation Country, continent, city, postal code, timezone Needs a MaxMind City or Country database. ASN Autonomous system number or organization Needs a MaxMind ASN database. Effective against hosting and VPN traffic. Rate limit Requests per path per time window Needs its own counter storage. Vulnerability score Method, country, network, attack patterns and user agent, summed Catches requests that are only suspicious in combination. IP reputation (AbuseIPDB) AbuseIPDB abuse confidence score for the client IP Needs a free API key. One cached lookup per visitor per day; fails open. IP reputation (any service) A score from a reputation service you choose Any endpoint that scores an address works: a commercial feed, an internal service, your own API. No PHP needed. Edge signal A TLS fingerprint, or the bot score your CDN computed Describes the client itself rather than what it claims to be. Needs a CDN in front of the site sending the headers. OWASP Core Rule Set The full CRS ruleset Requires a library release that ships it.

Every rule carries a response. The three that decide whether a request gets through are evaluated in a fixed order:

  • Allow. Let the request through and stop evaluating. Evaluated first, which makes an allow rule a reliable safety net.
  • Challenge. Serve an interstitial the visitor must solve. Evaluated second.
  • Block. Reject the request. Evaluated last.

Two more responses don't refuse anyone:

  • Redirect. Send the visitor somewhere else: a retired API path, an out-of-date campaign URL, or a "slow down" page instead of a bare 429.
  • Mark. Change nothing about the request, but leave a signal the site's own code can read.

Any rule can also be set to observe only. It logs every match and then lets the request carry on, so you can see what a new rule would do before letting it do it, while the rest of the firewall keeps enforcing.

Within a group, lower weights run first, so cheap checks (IP, path) can run ahead of expensive ones (Core Rule Set, vulnerability scoring). A rule can be limited to certain days and hours in a timezone you choose.

Rules are built rather than typed. Each condition is chosen from the variables the firewall actually exposes, with the operators that apply to it, so there's no syntax to remember and no way to name a variable that doesn't exist. The rules list then describes each rule in plain words rather than in the form it compiles to.

Challenges

A challenge serves a short interstitial instead of rejecting the request. A visitor who solves it receives a signed pass token and isn't challenged again until it expires. It slows automated traffic down without shutting people out. Four providers ship, each configured entirely through the interface:

  • math. One addition, with no JavaScript, no external script and no third-party service.
  • altcha. Proof of work, which costs a bot real CPU per solve, with single-use solutions.
  • Cloudflare Turnstile. A hosted challenge that is usually invisible to the visitor. Needs a site key and secret key.
  • Google reCAPTCHA. v2 by default, with v3 available. Needs a site key and secret key.

The two local providers ask nothing of any third party. The two remote ones send a verification request to Cloudflare or Google, and the interface says plainly what that means before you choose one — including that reCAPTCHA v3 scores a visitor rather than asking them anything, so a real person who scores below the threshold has no puzzle to solve and no retry that helps. That's why v2 is the default.

A custom class implementing the library's provider interface plugs in anything else, such as hCaptcha.

The interstitial's language, title, heading, introduction, button and error text can all be set from the interface, along with your own CSS or stylesheet. How long a solved challenge lasts is set per rule, so a visitor can be re-challenged on whatever interval suits the traffic. The signing secret is generated automatically on install and can be rotated from the interface.

What a blocked visitor sees

By default a block is one line of plain text with a reference the visitor can quote. Instead, it can be:

  • A page. An HTML page with its own language, title and heading, sent with a strict Content-Security-Policy: it has no script and no form, and the policy keeps it that way. Or use your own template, with your logo, header and footer.
  • JSON for API clients. A client that asks for JSON gets {"error": "blocked", …} with the reference, so it has something it can parse.
  • A rule's own message. A block rule can say why it refused, such as "Automated access to search is not allowed", in place of the site-wide wording.

A lockdown gets its own message and the same page option. Every color on the block, lockdown and challenge pages is a CSS custom property, so a line such as :root { --fw-accent: #0b5; } themes all three.

Presets

The library ships maintained rule sets, and switching one on includes it by reference rather than copying it in. Its rules update when you update the library, with no re-import step. They include:

  • Malicious requests and malicious URLs: attack signatures, backdoor filenames, configuration files and executable extensions.
  • Drupal and WordPress paths, and rate limiting for logins, APIs, forms and search.
  • AI crawlers (blocked or challenged), AI answer engines, and an allowance for search engine bots.
  • A honeypot of paths no legitimate client asks for.
  • Host-specific storage and logging, such as Pantheon's.

Because these rule sets are deliberately broad, every preset can be read before it's enabled: each rule, what it responds with, the patterns themselves, and the raw file with its comments. Presets that could lock you out say so before you switch them on.

Other modules can contribute their own presets through hook_basic_firewall_presets(), supplying either a YAML file or the configuration inline, and adjust the whole list with hook_basic_firewall_presets_alter().

Imported rule lists

A rule can take its entries from a list published somewhere else, such as a threat feed, a cloud provider's address ranges, or a file your team keeps in the repository, instead of having them typed in and kept up to date by hand. Lists are refreshed on cron or with drush basic-firewall:sources. The firewall never fetches one while serving a request, so a visitor never waits on somebody else's server.

Storage

When a rule blocks a request, the client is recorded so later requests are rejected immediately, without re-evaluating every rule.

Backend Use when File A single web node. No database needed, and it works at the earliest possible point. Database Several web nodes sharing one block list. Uses Doctrine DBAL, not Drupal's database layer. Redis Several web nodes, and blocks expire on their own, so there's nothing to sweep during an attack. Memcached Several web nodes on a fleet that has Memcached and not Redis.

For database storage, where the connection comes from is a separate choice, with four options:

  • Reuse Drupal's own credentials. They're never written anywhere — not into Drupal configuration, and not into the compiled file. They're read fresh on every request and handed to the firewall directly, so a platform that rotates credentials can't leave the firewall holding a stale copy, and no plaintext password reaches disk.
  • Individual connection parameters. Host, port and database name stay in configuration, where they're useful in a diff, while the password comes from the environment.
  • A connection DSN. One string, stored as given.
  • Let an enabled preset supply it. For presets that carry a connection of their own, such as the Pantheon storage preset.

The interface says which of these is in effect, and warns when a preset supplies a connection that another choice is overriding — a combination that otherwise looks correct and quietly does nothing.

Rate limit counters have their own storage, chosen separately: a file, a database, or Redis.

The library's own caches (parsed user agents, locations, the parsed configuration) can live in files, APCu, Redis or Memcached, or in Drupal's cache, which keeps them off the files directory on hosting where it's a network mount. When a library update adds a column or an index to one of the firewall's tables, the module adds it for you, on database updates and on cron.

Multisite

Table names and Redis keys are namespaced from the database connection's own prefix, so sibling sites sharing one database or one Redis instance keep separate block lists and separate rate limit counters.

Keeping secrets out of configuration

Every field that can hold a secret (API keys, the challenge signing secret, database passwords) accepts a token instead of the secret itself:

api_key: "%env(ABUSEIPDB_API_KEY)%"
secret:  "%file(/etc/firewall/hmac.key)%"

%env()% reads an environment variable. %file()% reads a file, for secrets that arrive as mounted files rather than variables. It has to be enabled deliberately in settings.php, with an allowlist of directories it may read from.

Other features

  • Log-only mode. The module installs with no rules and in log-only mode, so nothing is blocked until you say so. Every would-be block is recorded with the full request.
  • A log you can query. The database log handler feeds a Log tab that answers which rule blocked the most clients, whether a rule has ever matched, and what happened to an address before its owner complained. It stays fast on a table with millions of records.
  • Test a request. Describe a request in the interface and see which rule would match it, and why, without sending real traffic. It records nothing and blocks nobody.
  • Lockdown. Refuse everyone except a list of addresses. Unlike a block-everything rule, it records nobody, so the block list doesn't fill up with the whole internet during an incident.
  • Panic file. A path the firewall checks on every request. Writing a mode name into it changes the operating mode immediately, with no deploy and no cache clear.
  • Escalation. Repeat offenders are blocked for longer each time, in stages you define.
  • Exempt a role. Members of a chosen role can be skipped entirely. Roles don't exist yet when the middleware runs, so this adds a second, later evaluation point for authenticated traffic only.
  • Logging through Monolog, because the firewall runs before Drupal's logging system exists. An option forwards those events into dblog or syslog once Drupal has finished booting.
  • Blocked client list in the interface, with a lookup and a confirmed unblock.
  • Export and import the firewall's configuration as one portable document, from the interface or with Drush.
  • Advanced tab for raw library configuration, for anything the interface doesn't cover.

Drush

drush basic-firewall:status            # what the firewall is doing right now
drush basic-firewall:rules             # rules in evaluation order
drush basic-firewall:rebuild           # recompile the configuration

drush basic-firewall:check IP          # is this address blocked?
drush basic-firewall:block IP          # block it
drush basic-firewall:unblock IP        # unblock it
drush basic-firewall:blocked           # list every blocked client
drush basic-firewall:clear-blocked     # empty the block list
drush basic-firewall:find-reference ID # which client a block reference belongs to

drush basic-firewall:sources           # refresh imported rule lists
drush basic-firewall:export            # export the configuration
drush basic-firewall:import FILE       # import it
drush basic-firewall:migrate-schema    # add missing table columns and indexes now
drush basic-firewall:cache-warm        # build the agent detection cache now

All are aliased to bfw:.

Requirements

  • Drupal 11.2 or later, or Drupal 12. Sites on Drupal 10.6 or an earlier Drupal 11 minor want the 1.x branch of this module instead.
  • PHP 8.3 or newer (Drupal 12 itself requires 8.5)
  • kanopi/firewall ^2.37 and kanopi/crs-engine ^1.0, installed automatically with Composer
  • A configured private file system. This is where the compiled configuration and the blocked-client data live, and the module won't run without it.

Optional:

  • MaxMind GeoLite2 databases, for the geolocation and ASN rule types. These are licensed separately and aren't shipped.
  • The redis or memcached PHP extension, for those storage backends.

Installation

composer require drupal/basic_firewall
drush en basic_firewall

Then configure the private file system, if you haven't already:

$settings['file_private_path'] = dirname($app_root) . '/private';

Keep it outside the web root, then run drush basic-firewall:rebuild and visit Administration → Configuration → Security → Basic Firewall.

Getting started safely

A firewall can lock you out of your own site. The module is built so that can't happen by accident:

  1. It installs in log-only mode with no rules. Nothing is blocked, and nothing matches, until you say so.
  2. Add your own address to an allow rule first, at a low weight. Allow rules run before everything else, and a match ends evaluation immediately.
  3. Add the rules you actually want, and leave the mode on log-only.
  4. Read the log for a few days and look for anything legitimate.
  5. Switch the mode to Block once the log is clean.

If you do lock yourself out, either route works, and neither needs the interface:

drush basic-firewall:unblock 203.0.113.10   # remove one client
drush basic-firewall:clear-blocked          # remove all of them

Or set $settings['basic_firewall_enabled'] = FALSE; in settings.php, which takes effect immediately and needs no database access at all.

How this differs from similar modules

Modules such as Ban and Perimeter act once Drupal is running. Basic Firewall evaluates before routing, sessions, authentication and the page cache, so rejected traffic never reaches them, and it can optionally evaluate before the container is built at all.

It isn't a replacement for a WAF at the edge. If you have a CDN or reverse proxy that can filter traffic, filter it there first. This module is for the rules that need to live with the site, travel with its configuration, and be managed by the people who administer it.

Maintainers

Maintained by Kanopi Studios. Issues and merge requests are welcome in the issue queue.

Ask your assistant about Basic Firewall

Check compatibility with your Drupal and PHP version, maintenance and security coverage, from current release data. How it works

Depends on

Dependencies of the latest stable release

No dependencies recorded for this project.

Required by

Tracked projects that depend on this one

No tracked projects depend on this one yet.

Activity

Tracked releases
34
Tracked since
Aug 2026
Latest release
3 weeks ago
Releases (12 mo)
34 ▲ from 0
Maintenance
Active

Release Timeline

Releases

Version Type Core PHP Release date
2.0.0-beta1 Pre-release 11 Aug 3, 2026
1.0.0-beta1 Pre-release 10–11 Aug 3, 2026
2.x-dev Dev 11 Aug 3, 2026
1.x-dev Dev 10–11 Aug 3, 2026