Skip to content

v2.10.0

Choose a tag to compare

@sean-e-dietrich sean-e-dietrich released this 29 Jul 23:44
134c43b

One change with a config surface: global.behind_proxy lets an operator assert whether a proxy sits in front of the deployment, which is the fact the library cannot detect for itself. If you run without a proxy, this is the release that lets you stop the per-request trusted-proxies warning honestly.

No behaviour changes unless you set the new key. Existing configuration keeps working, and with behind_proxy unset the trusted-proxies check does exactly what 2.9.0 did — same level, same message text plus one sentence pointing at the new setting.

PHP 8.1–8.5 supported.

Added

  • global.behind_proxy (#101, fixes #99). Every plugin reads $request->getClientIp(), and Symfony only honours proxy headers once you have called Request::setTrustedProxies(...). The firewall warns when that list is empty, because a deployment that is behind a proxy without it can have its source IP spoofed past IP allowlists and per-IP rate limits.

    What was missing was any way to say "there is no proxy here". Two settings covered three real postures, so a site with nothing in front of it logged a warning on every request with no way to stop it — create() runs per request, and the only escape was passing setTrustedProxies() a value that was not true.

    global:
      behind_proxy: false   # asserted: nothing in front of this deployment
    behind_proxy Trusted proxies empty ⇒
    unset (default) warning — the posture is unknown, unchanged from 2.9.0
    false silent — nothing can spoof a forwarding header through a proxy that does not exist
    true error — an asserted proxy that is not wired up is a definite misconfiguration, not an open question

    Two details are load-bearing enough to state here rather than leave in the docs:

    • behind_proxy: false wins over require_trusted_proxies: true. An explicit assertion that there is no proxy makes the requirement moot. Throwing anyway would leave an operator who told the truth about their deployment with no way to start the firewall at all.

    • An uninterpretable value fails safe. behind_proxy: with nothing after it parses to NULL, and an %env()% token for an unset variable yields an empty string; PHP's filter_var() reads both as FALSE. Taken literally, a half-written key would silence a security warning. Both are treated as "posture unknown" instead, so the warning stands. Silencing a security warning is the dangerous direction to guess in.

    require_trusted_proxies is unchanged and still escalates whichever case applies into a startup ConfigurationException.

Fixes

  • The create() docblock promised a silencer that did not exist (#101, fixes #99). It read "Operators who know they're not behind a proxy can silence the warning with global.require_trusted_proxies: false (the default)" — and false is precisely the branch that emits the warning. The method's own docblock and docs/configuration/global.md were both already correct, which is how one wrong comment survived between two accurate ones. Anyone who followed it went looking for a setting that was not there, which is what #99 reported.

Changed

  • The default trusted-proxies message gained one sentence naming global.behind_proxy=false as the way to assert no proxy. The rest of the text, and its warning level, are unchanged.

  • docs/configuration/global.md Trusted Proxies section rewritten to cover the two settings separately: behind_proxy for the deployment fact, require_trusted_proxies for how loud the unresolved cases are, with the precedence rule between them stated explicitly.

Upgrade notes

  • Nothing is required. composer update kanopi/firewall changes no behaviour on its own. With behind_proxy unset the check behaves exactly as it did in 2.9.0.

  • If nothing sits in front of your site, set behind_proxy: false. This is the fix for the per-request warning reported in #99. It is the only supported way to silence it — there is deliberately no way to suppress it by accident.

  • If you are behind a proxy / CDN / load balancer, say so. behind_proxy: true upgrades a missing setTrustedProxies() from warning to error, because once you have declared the proxy the omission is a defect rather than an unknown. Pair it with require_trusted_proxies: true to fail the deploy instead:

    global:
      behind_proxy: true
      require_trusted_proxies: true
  • The new error level cannot affect an existing deployment. It applies only to behind_proxy: true, a key that did not exist before this release.

Internal

  • behind_proxy is resolved before create() runs array_filter() over the global config, mirroring how require_config is already handled. array_filter() strips falsy values, so a behind_proxy: false read any later is deleted outright and becomes indistinguishable from unset — the setting's entire purpose silently gone. The test suite caught this rather than review; the first implementation read it inside the check and the "silences the warning" test failed immediately.

  • 11 new unit tests cover each of the three postures, the precedence rule against require_trusted_proxies, the boolean-ish string forms ("false", "0", "no", "off") that arrive from %env()% tokens and quoted YAML, and the empty / unparseable values that must not be read as a false assertion. The three pre-existing #58 trusted-proxies regressions are unmodified and still pass, which is what pins the unchanged default behaviour.

  • Per-process log deduplication was considered and rejected. It was one of the options in #99. It would add process-global mutable state to a security component in order to reduce the volume of a message that can now be switched off properly. The remaining per-request warning is the genuinely-unknown case, and that should keep asking until it is answered — the difference in this release is that it is now answerable from config.

  • Docs deployment now publishes with ghp-import rather than mkdocs gh-deploy (#98), and pymdown-extensions was bumped to 11.0 (#97). Both are build-tooling changes with no effect on the released package.

  • Suite: 845 unit tests and 49 integration tests pass. PHPCS, PHPStan at level max and Rector are clean. The 2 reported deprecations are a pre-existing ReflectionProperty::setValue() call in LoggingFactoryTest, unrelated to this release.