Repository navigation
v2.33.0 — everything in YAML
v2.33.0
Everything in YAML.
Four things a site could only set up from PHP now have a configuration key: a Memcached or Redis cache, a connection handed to something that takes a client, decision listeners and the StatsD exporter, and trusted proxies. A site whose only integration point is a config file can now use all of them. Each is scoped so it cannot reach past the firewall.
Memcached also ships as a block list. And two things the firewall had been getting quietly wrong are fixed.
PHP 8.1–8.5 supported. Every new key is opt-in; the behaviour changes on upgrade are listed under Upgrading.
Added
-
Memcached block storage (#392)
storage: type: "Kanopi\\Firewall\\Storage\\MemcachedStorage" config: memcached: { host: cache.internal, port: 11211, prefix: "firewall:" }
For a fleet that has Memcached and not Redis. Blocks are JSON with a server-side expiry, and bans longer than 30 days are sent as absolute timestamps; Memcached reads anything over 30 days as a timestamp, so a 90-day ban sent as seconds would have vanished on write. A server that fails is dropped for the rest of the request, so one of several being down does not take the block list with it.
Memcached cannot list its keys, so range searches (
firewall-block --find,--list,--liftwith a range) run from an index of 64 shards, written under compare-and-swap so two workers blocking at once cannot drop each other's entry. The index is best effort, and says so. An evicted shard is detected, and until everything it could have held has expired,firewall-block,firewall-doctorandBlockList::backend()['gap']warn that range searches may be incomplete. A single address never uses the index, so "why is this customer blocked?" is answered correctly even then. When un-blocking by range has to be exact, use Redis or the database. -
Cache pools from a DSN (#394)
metadata: cache: "memcached://cache.internal:11211" # or redis://, rediss://
The user-agent cache, the GeoIP cache,
CacheRateLimitStorageand reverse DNS'sverify_cacheall take a DSN now. Memcached and Redis adapters need a connected client, which a YAML value cannot hold, so these were PHP-only before;verify_cachetook nothing but an injected pool. One resolver replaces three hand-written copies.A DSN pool gets bounded timeouts, a Memcached reachability probe, and a marshaller that refuses objects: a value planted on a shared server comes back as a miss, never as an instance of a class the writer chose. DSN credentials are redacted from every log line.
-
Named connections (#395)
connections: redis: "redis://redis.internal:6379" logger: - class: "Monolog\\Handler\\RedisHandler" args: ["%connection(redis)%", "firewall:log"]
Declared once, and the connection itself handed to whatever needs one: a log handler argument, a storage's
instance:, a cacheadaptor:, a database backend'sconnection:. Two things needed PHP before. Handlers that take a client, such as Monolog'sRedisHandler, had no YAML at all, and there was no way to share one connection between consumers.%config()%still shares a connection's description, and is enough most of the time.The token is resolved where components are built, not when config is loaded, so
firewall-check --lintnever opens a connection. There is one registry per firewall, so two firewalls in one process never share by accident. A Redis or Memcached server that is down degrades rather than stopping the firewall starting. -
Decision listeners and StatsD from YAML (#396)
events: listeners: - class: "App\\Firewall\\NotifyOnPermanentBan" events: [RequestBlocked] metrics: statsd: { host: 127.0.0.1, port: 8125, prefix: "site1." }
The same events a PHP dispatcher receives, and the same StatsD wiring the metrics guide shows in PHP. A dispatcher passed to
Firewall::create()still works, and both are told, the host's first. Listeners are read-only and isolated: one that throws is logged, recorded as degraded, and the next still runs. One that cannot be built is refused at startup. Prometheus stays PHP. -
Trusted proxies from YAML (#397)
global: trusted_proxies: ["10.0.0.0/8", "173.245.48.0/20"] # or REMOTE_ADDR, PRIVATE_SUBNETS
Request::setTrustedProxies()is process-wide static state, so these are applied for one evaluation and put back afterwards; your application's own view of the client never changes. Your bootstrap wins: if it already callssetTrustedProxies(), the YAML is ignored, with one warning.0.0.0.0/0and::/0are refused at startup, since trusting every address trusts every client's ownX-Forwarded-For.firewall-doctorcan now verify this form from a terminal, which the bootstrap form never allowed. -
verify_offline(#391)KANOPI_FIREWALL_SOURCES_OFFLINEswitched reverse-DNS verification off along with rule-source fetches, with no way back and no warning, so a verified crawler allow rule matched nobody new. It still does by default.metadata.verify_offline: falsenow verifies while rule sources stay offline, and a rule the constant switched off warns at startup and infirewall-doctor.
Fixed
- A request with no client address (#403). One without
REMOTE_ADDR— built by a long-running runtime's bridge, a queue worker or a test — madeIpAddressfail with aTypeError. Worse, every such request shared the block-list key"", so one address-less client tripping a rule refused every other, on every path. NowIpAddressdoes not match, a matching rule still refuses but writes no ban it cannot attribute, and a rate limit counting byclient_ipdoes not pool them. - The GeoIP docs' Redis cache example never worked. It passed a DSN where
RedisAdapterneeds a client, so caching was silently off. It now uses the DSN form (#394). CacheRateLimitStorageconstructed any class that existed, pool or not, and failed on the first request. It is refused now, and reported as degraded (#394).
Upgrading
$ composer require kanopi/firewall:^2.33Nothing to configure. Three changes are worth knowing about:
- Requests with no
REMOTE_ADDRare no longer banned (#403). They are still refused by a matching rule, but not written to the block list. If a customStorageInterfacereturns''fromgetKey()for a real client, its blocks are now withheld; an empty key cannot tell clients apart anyway. - A reverse-DNS rule on a host that sets
KANOPI_FIREWALL_SOURCES_OFFLINEnow logs a warning at startup (#391). Behaviour is unchanged; setverify_offlinetofalseto verify, ortrueto keep it off and silence the warning. CacheRateLimitStoragenaming a class that is not a PSR-6 pool now appears inFirewall::getDegradedBackends()(#394). It never worked; now it says so.
Full changelog: v2.32.0...v2.33.0