kanopi/firewall-symfony
Symfony integration for Kanopi Firewall that adds configurable request firewalls and access rules, letting you allow or deny traffic by IP, network, or custom criteria. Protect routes and enforce access policies directly from Symfony configuration.
This is a Symfony bundle, not a Laravel package. There is no Laravel service provider, no vendor:publish, and no php artisan command. If you are working in a Laravel app, treat this as a reference implementation for the underlying kanopi/firewall library rather than something you can drop into composer.json and expect to work. If you are working in a Symfony app—or maintaining one alongside Laravel services—this is the package you want for path/IP/ASN/rate-limit rules and ALTCHA challenges.
Minimum steps:
composer require kanopi/firewall-symfony:^1.3
This pulls kanopi/firewall ^2.36, up from ^2.33. The library bump is not optional; several fixes in this release only arrive with the new floor.config/bundles.php if it is not already registered, then add the bundle configuration. Use bin/console config:dump-reference and the README for the exact config key/path for your version.bin/console list kanopi:firewall
bin/console kanopi:firewall:check /
/wp-login.php, or a rate limit on an API route. Run the bundle in mode: exception so it writes every firewall response itself, and set blocked_response: http_exception if you want blocked requests to travel through Symfony’s normal error page.Upgrade priority: if the app sits behind Pantheon’s Fastly-based Global CDN or any CDN that loosely honours no-store, this release is important. Before 1.3.0, a challenge page could be cached at the edge with a single-use challenge, so only the first visitor could solve it and everyone else looped.
Configuration-driven rules are the default workflow. Define path, IP, ASN, and rate-limit rules in bundle config, then verify them with kanopi:firewall:check. Keep rules against normalised paths; as of the ^2.36 library floor, //wp-json/... and /%77p-json/... are normalised before rules see them.
Let the bundle own responses when using mode: exception. In that mode the bundle writes block, lockdown, challenge, redirect, and solved-challenge responses. Do not half-write your own response in a listener unless you are prepared to reproduce the full cache-header set. If you use blocked_response: http_exception, the headers travel with the exception, so your Symfony error page carries them too.
Use query_count for crawler and facet abuse. The new query_count rule variable, available with kanopi/firewall ^2.36, is useful for capping query-string crawling without blocking normal pagination. It fits well alongside rate limits on search/filter endpoints.
Gate CI with the check command and its real exit codes. The command now exits 64 when it cannot read configuration, instead of 2. The full set is:
0 allowed1 blocked2 challenged3 redirected64 unreadable configuration70 internal/script errorA CI assertion that a URL is challenged should fail on 64, not pass because it saw 2. If you run checks in a subdirectory install, use the now-declared --script-name option so the CLI check sees the same path shape as the app.
Surface challenge notices deliberately. challenge.notice in config and RequestChallenged::addNotice() from a Symfony listener both reach the challenge page. Use the config value for static messaging and the listener when the notice depends on request context, tenant, or rule that fired.
Test ASN and rate-limit rules after upgrading. ASN rules such as asn:16509 previously never fired. After moving to ^2.36, they will start matching. Similarly, rate-limit keys that name a field with a capital letter now count that field correctly instead of sharing one counter across every visitor. If you relied on the old shared-counter behaviour—even accidentally—expect traffic shaping to change.
Deployment pattern: change config → clear Symfony cache → run kanopi:firewall:check against representative URLs → run the doctor → spot-check response headers with curl -I. This is especially important on CDN-fronted environments.
Do not install this in a Laravel app expecting Laravel integration. It is a Symfony bundle. The underlying kanopi/firewall library and the rule concepts are portable, but the bundle’s commands, config, and response writing are Symfony-specific.
Preserve the full no-store header set if you write your own responses. The 1.3.0 fix sends:
Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0
Pragma: no-cache
Expires: 0
Surrogate-Control: no-store
CDN-Cache-Control: no-store
If you override or wrap firewall responses, do not reduce this to Cache-Control: no-store alone. That was the exact condition that let Pantheon’s CDN cache a single-use challenge.
Avoid global.path_source: script_name in Symfony. That setting is intended for WordPress’s directly served files. A Symfony app runs public/index.php, and in a subdirectory install script_name puts the subdirectory in front of every path. The doctor now warns about this; take the warning seriously rather than suppressing it.
The exit-code change can break CI in both directions. A pipeline that treated 2 as “challenged” may now see 64 for an unreadable config. That is the intended fix: a gate should not pass on an application with no usable rules. Update scripts and help text assumptions.
ASN rules becoming live is a behaviour change. After the upgrade, rules that were previously inert may start blocking or challenging traffic. Roll out with logging or a permissive phase first if ASN rules are in production config.
Capitalised rate-limit field names are now meaningful. A key naming a field with a capital letter counts that field instead of sharing one counter across all visitors. Prefer lowercase field names for predictability, and re-check limits after upgrade.
query_count requires the new library floor. Do not copy a query_count rule into an app still on kanopi/firewall ^2.33; it will not behave as expected.
Solved-challenge redirects are now constrained. The library fix in 2.35.0 prevents a solved challenge from redirecting off-site. If you had a custom flow depending on an external post-solve redirect, re-test it.
Debug with headers, not just status codes. Use curl -I against a challenged URL and confirm the full cache-header set is present. Use kanopi:firewall:check for rule outcomes, and the doctor for configuration warnings. For deeper integration, listen to RequestChallenged and add notices rather than replacing the bundle’s response.
How can I help you explore Laravel packages today?