kanopi/firewall
Kanopi Firewall is a lightweight PHP/Laravel package for controlling access by IP address. It offers configurable allow/deny rules and middleware integration to protect routes, block unwanted traffic, and manage firewall behavior from application configuration.
The firewall package sits between Laravel's HTTP kernel and your application: it inspects each request, applies allow/block/bypass rules, and (for rules that verify crawlers) performs reverse-DNS and forward-confirmation lookups before letting a claimed Googlebot/Bingbot through.
Minimal path:
Install and publish config.
composer require kanopi/firewall
php artisan vendor:publish --tag=firewall-config
You get a YAML config (typically config/firewall.yml or a firewall/ directory) plus the service provider registration.
Sanity-check the install before you tune anything. Two commands are the fastest way to learn the package:
php artisan firewall-check --lint
php artisan firewall-doctor
--lint validates config syntax, unknown provider names, and unresolvable resolver classes. firewall-doctor tells you, per verifying rule, where its lookups actually go — which after v2.38.0 is the first thing to check when verification behaves unexpectedly.
First real use case: verify crawlers instead of trusting user agents. Most teams start by protecting against fake Googlebot. Enable verification on the relevant rule and, if you want predictable latency, opt into a DNS-over-HTTPS provider:
global:
reverse_dns:
provider: cloudflare # or google — this is the opt-in
timeout_ms: 300
Nothing is sent to a third party until you name a provider; with no global.reverse_dns, PHP's own gethostbyaddr()/dns_get_record() are used exactly as before.
Where to look first in the docs: the Reverse DNS page for provider privacy/terms and latency sizing, then the config reference for merge semantics (see Gotchas — this matters if you compose configs).
Second real use case: lockdown. Put the app behind a maintenance/IP-allowlist mode using lockdown_allow, and drive it from an env-specific config file rather than editing the base config.
Compose configs per environment, but respect the new merge rules. A common workflow is a base config plus a site/env overlay:
Firewall::create([
base_path('firewall/base.yml'),
base_path('firewall/'.app()->environment().'.yml'),
]);
As of v2.38.0, lists in a later config replace the earlier one's list rather than appending — matching how configs: includes behave. So production.yml setting trusted_proxies replaces the base list. Keep each list in exactly one file, or repeat the full list in the overlay.
Use includes for shared fragments. Includes are the idiomatic way to share rule sets. An included file's lockdown_allow: [] now genuinely clears the list (previously ignored). Use this deliberately: an include with [] means "empty this list", not "leave it alone".
Per-rule overrides for mixed traffic. Global provider + per-rule tuning is the pattern for sites where different rule groups have different latency budgets:
rules:
- name: verified-crawlers
verify: true
verify_provider: google
verify_timeout_ms: 400
Wire commands into deploy and CI. firewall-check --lint belongs in your deploy script or a CI job — it fails on unknown providers, provider + resolver both set, or an unbuildable resolver class, which otherwise stops the firewall from starting. Add firewall-doctor output to your post-deploy smoke check so you can see lookup destinations per rule.
Bring your own resolver for private infrastructure. If you run an internal DoH resolver, either declare it under global.reverse_dns.providers or point resolver at a class implementing Interface. Note the contract: a resolver returns DNS data only — never a verdict. Hostname validation, domain checks, and forward confirmation remain in the firewall, so a custom resolver can't widen an allow rule beyond what DNS proves.
Test verification paths without network calls. Keep reverse-DNS-heavy rules out of the default test config, or inject a stub resolver; assert rule outcomes (allowed/blocked/breaker state) rather than DNS specifics. For feature tests that must exercise a verifying rule, use a local stub resolver so suite runtime doesn't depend on a third party.
Tune timeout_ms against real numbers, not guesses. Measured on 25 workers with a non-answering provider: first worker gives up at ~306–308 ms, the rest are refused in under 1 ms; an established connection costs 19–40 ms per verification, the first 66–93 ms including TLS. Size timeout_ms comfortably above the provider's normal latency — the limit covers connect plus TLS handshake — and align verify_claim_wait_ms with it so concurrent claimers aren't starved.
Privacy/ops review as a code change. Naming a provider is a data-processing decision: visitors' IPs used by crawler-claiming requests get sent to Cloudflare or Google. Treat the config diff as something legal/compliance signs off on, and document it in your privacy notes. Defaults send nothing.
Firewall::create() and relied on two files adding to the same list (trusted_proxies, lockdown_allow, verify_suffixes, …), you silently lose entries. The later config now wins wholesale. Single-config sites are unaffected. Audit any site where a proxy list was split across files — a dropped trusted proxy changes client IP resolution for everything upstream.plugins: and a legacy rule's config: continue to append. A later config's priority and enable: false still win. Don't assume "replace" is universal.[] over a map is a no-op. Empty lists clear lists; empty maps change nothing. The asymmetry is intentional and easy to misread when reviewing includes.block:/bypass: list merging changed too. [b] over [a, c] used to yield [b, c]; it's now [b]. If a legacy rule looked like it was preserving entries, it wasn't.provider and resolver set, or a resolver that can't be constructed stops the firewall booting. Catch this in CI with --lint rather than discovering it in production.DnsOverHttpResolver requires PHP's curl extension. PHP's native lookups need nothing extra. If you opt into a provider without curl, you'll fail at startup — check the extension in your base image.verify_negative_ttl. Timeouts and server failures won't refuse a legitimate crawler for a day. Don't try to "tune" this via the negative TTL; it's deliberately short.googlebot.com names to make every verification slow. If you're debugging why a host with many PTRs isn't fully checked, this is why — it's a ceiling, not a bug.firewall-doctor (where do lookups go?) → firewall-check --lint (is the config even valid?) → check timeout_ms against measured provider latency → check whether the PTR/forward confirmation chain genuinely completes, since verification requires both directions.global.reverse_dns.providers, custom resolver classes via resolver/resolver_options, root plugins: for behaviour, and per-rule verify_provider/verify_timeout_ms for scoping. Resolvers deliberately cannot return verdicts — build policy in rules, not in the resolver.How can I help you explore Laravel packages today?