Skip to content

v2.33.0 — everything in YAML

Choose a tag to compare

@sean-e-dietrich sean-e-dietrich released this 27 Sep 15:52
d176647

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, --lift with 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-doctor and BlockList::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, CacheRateLimitStorage and reverse DNS's verify_cache all 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_cache took 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 cache adaptor:, a database backend's connection:. Two things needed PHP before. Handlers that take a client, such as Monolog's RedisHandler, 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 --lint never 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 calls setTrustedProxies(), the YAML is ignored, with one warning. 0.0.0.0/0 and ::/0 are refused at startup, since trusting every address trusts every client's own X-Forwarded-For. firewall-doctor can now verify this form from a terminal, which the bootstrap form never allowed.

  • verify_offline (#391)

    KANOPI_FIREWALL_SOURCES_OFFLINE switched 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: false now verifies while rule sources stay offline, and a rule the constant switched off warns at startup and in firewall-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 — made IpAddress fail with a TypeError. Worse, every such request shared the block-list key "", so one address-less client tripping a rule refused every other, on every path. Now IpAddress does not match, a matching rule still refuses but writes no ban it cannot attribute, and a rate limit counting by client_ip does not pool them.
  • The GeoIP docs' Redis cache example never worked. It passed a DSN where RedisAdapter needs a client, so caching was silently off. It now uses the DSN form (#394).
  • CacheRateLimitStorage constructed 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.33

Nothing to configure. Three changes are worth knowing about:

  • Requests with no REMOTE_ADDR are no longer banned (#403). They are still refused by a matching rule, but not written to the block list. If a custom StorageInterface returns '' from getKey() 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_OFFLINE now logs a warning at startup (#391). Behaviour is unchanged; set verify_offline to false to verify, or true to keep it off and silence the warning.
  • CacheRateLimitStorage naming a class that is not a PSR-6 pool now appears in Firewall::getDegradedBackends() (#394). It never worked; now it says so.

Full changelog: v2.32.0...v2.33.0