Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/advanced/discriminator-normalizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ This ensures:
| `ip-limit` | `ip-limit` |
| `my rule with spaces` | `my_rule_with_spaces` |
| (empty string) | `empty` |
| (very long name) | `first_107_chars...-a1b2c3d4e5f6` |
| (name over 120 chars) | `first_107_chars...-a1b2c3d4e5f6` |

| User Key | Cache Key Suffix |
|----------|-----------------|
Expand Down Expand Up @@ -210,6 +210,6 @@ The prefix is validated: it is trimmed (whitespace and a trailing `:` stripped),

2. **Normalize application-level keys yourself.** The discriminator normalizer handles global concerns (case, trim). Domain-specific normalization (like email deduplication) should happen in your key closure.

3. **Use consistent key structures.** When writing custom key closures, prefix your keys to avoid collisions between different rule types: `user:123` instead of just `123`.
3. **Use consistent key structures.** When writing custom key closures, prefix your keys to avoid collisions between different rule types: `user:123` instead of `123`.

4. **Avoid sensitive data in keys.** While user keys are SHA-256 hashed in cache, the raw key is still visible in event payloads (`TrackHit`, `ThrottleExceeded`, etc.). Use hashed or anonymized identifiers when possible.
13 changes: 8 additions & 5 deletions docs/advanced/dynamic-throttle.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# Dynamic Throttle & Advanced Rate Limiting

Phirewall's throttle system goes beyond simple fixed-window rate limiting. This page covers dynamic limits, sliding windows, multi-window throttling, and advanced patterns for building fine-grained rate limiting strategies.
Phirewall's throttle system does more than fixed-window rate limiting. This page covers dynamic limits, sliding windows, multi-window throttling, and patterns for fine-grained rate limiting.

For basic rate limiting setup, see [Rate Limiting](/features/rate-limiting).

Expand All @@ -18,7 +18,6 @@ Give different users different quotas based on a request header:

```php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Store\InMemoryCache;
use Psr\Http\Message\ServerRequestInterface;

Expand Down Expand Up @@ -323,7 +322,7 @@ $config->throttles->add('api-limit',
```

::: tip
For trusted traffic that should bypass **all** rules (not just throttles), use [safelists](/features/safelists-blocklists) instead. Safelisted requests skip the entire firewall pipeline, including blocklists, fail2ban, and track rules.
For trusted traffic that should bypass **all** rules (not only throttles), use [safelists](/features/safelists-blocklists) instead. Safelisted requests skip the entire firewall pipeline, including blocklists, fail2ban, and track rules.
:::

## Database-Driven Key Assignment
Expand All @@ -336,7 +335,7 @@ $userTiers = $db->fetchAll('SELECT user_id, plan FROM users');
$tierMap = array_column($userTiers, 'plan', 'user_id');

$config->throttles->add('db-tiered',
limit: fn(ServerRequestInterface $request) use ($tierMap): int =>
limit: fn(ServerRequestInterface $request): int =>
match ($tierMap[$request->getAttribute('userId') ?? ''] ?? 'anonymous') {
'enterprise' => 10000,
'pro' => 1000,
Expand Down Expand Up @@ -375,9 +374,13 @@ $firewall->resetThrottle('api:p60', '192.168.1.100');
$firewall->resetAll();
```

::: warning
`resetThrottle()` clears fixed-window, multi-window, and dynamic-period counters only. It does **not** reset sliding-window counters created via `throttles->sliding(...)`: sliding windows are stored under per-window keys (suffixed `.w.{windowStart}`) rather than the key `resetThrottle()` deletes. To clear a sliding-window counter, call `resetAll()` or let the windows expire (TTL = 2 x period).
:::

## Best Practices

1. **Use descriptive rule names.** Names appear in `X-RateLimit-*` headers, `ThrottleExceeded` events, and (when `enableResponseHeaders()` is active) `X-Phirewall-Matched` headers. Use `api-free-tier` instead of `rule1`.
1. **Use descriptive rule names.** Names appear in `ThrottleExceeded` events and, when `enableResponseHeaders()` is active, in the `X-Phirewall-Matched` response header. (The `X-RateLimit-*` headers carry only numeric limit/remaining/reset values, not the rule name.) Use `api-free-tier` instead of `rule1`.

2. **Return `null` to skip.** This is the primary mechanism for conditional rate limiting. When a key closure returns `null`, the rule is skipped with zero overhead.

Expand Down
3 changes: 1 addition & 2 deletions docs/advanced/infrastructure.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ new InfrastructureBanListener(
| `$blockOnFail2Ban` | `bool` | `true` | Mirror Fail2Ban bans |
| `$blockOnBlocklist` | `bool` | `false` | Mirror blocklist hits (request IP) |
| `$keyToIp` | `?callable` | identity | Map a Fail2Ban key to an IP (default: assumes key is an IP) |
| `$requestToIp` | `?callable` | `KeyExtractors::ip()` | Extract IP from a `ServerRequestInterface` |
| `$requestToIp` | `?callable` | `REMOTE_ADDR` (raw peer) | Extract IP from a `ServerRequestInterface` |

### Wiring with PSR-14

Expand Down Expand Up @@ -383,7 +383,6 @@ A full setup combining Fail2Ban, infrastructure mirroring, and rate limiting:
declare(strict_types=1);

use Flowd\Phirewall\Config;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\RedisCache;
use Flowd\Phirewall\Infrastructure\ApacheHtaccessAdapter;
Expand Down
6 changes: 3 additions & 3 deletions docs/advanced/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# Observability

Phirewall provides comprehensive observability through PSR-14 (PHP Standard Recommendation for Event Dispatching) events and built-in diagnostics counters. Every significant decision the firewall makes is observable, making it straightforward to integrate with any logging, metrics, or alerting system you already use.
Phirewall provides observability through PSR-14 (PHP Standard Recommendation for Event Dispatching) events and built-in diagnostics counters. Every significant decision the firewall makes is observable, so you can integrate it with any logging, metrics, or alerting system you already use.

## Enabling Events

Expand Down Expand Up @@ -263,7 +263,7 @@ Access the counters from the dispatcher at any time via `$dispatcher->counters()

### Minimal Dispatcher

The simplest possible dispatcher for quick debugging:
A minimal dispatcher for quick debugging:

```php
use Psr\EventDispatcher\EventDispatcherInterface;
Expand All @@ -279,7 +279,7 @@ $dispatcher = new class implements EventDispatcherInterface {

### Monolog Integration

Full-featured logging with different severity levels for different event types:
Logging with different severity levels for different event types:

```php
use Monolog\Logger;
Expand Down
10 changes: 7 additions & 3 deletions docs/advanced/portable-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ outline: deep

# Portable Config

`PortableConfig` expresses a firewall ruleset as plain, JSON-serializable data instead of PHP closures. Because a ruleset is just data, you can:
`PortableConfig` expresses a firewall ruleset as plain, JSON-serializable data instead of PHP closures. Because a ruleset is data, you can:

- **store it in a database** and pick up rule changes on the next request,
- **ship it through a config service** (etcd, Consul, S3, a settings table),
Expand Down Expand Up @@ -95,14 +95,18 @@ Everything `PortableConfig` can express today.

| Factory | Keys on |
|---------|---------|
| `keyIp()` | client IP (`REMOTE_ADDR`) |
| `keyIp()` | client IP resolved via the Config's IP resolver (else `REMOTE_ADDR`), like keyless rules and `filterIp()` |
| `keyMethod()` | HTTP method |
| `keyPath()` | request path |
| `keyHeader(name)` | raw value of header `name` |
| `keyHashedHeader(name)` | sha256 fingerprint of header `name`, preferred for credential-bearing headers (`Authorization`, `Cookie`, `X-Api-Key`) so the raw value never reaches the cache/ban registry |

::: tip
`keyIp()` keys on `REMOTE_ADDR`, which behind a CDN or load balancer is the proxy's address, not the client's. The IP resolver is a closure and therefore not portable; set it on the rebuilt `Config` with `setIpResolver(KeyExtractors::clientIp(new TrustedProxyResolver([...])))`. See [Client IP behind proxies](/getting-started#client-ip-behind-proxies).
`keyIp()` is resolver-aware: it materializes as a keyless rule and late-binds to the evaluating `Config`'s IP resolver at runtime, exactly like keyless counter rules and `filterIp()`. Behind a CDN or load balancer, set proxy trust once on the `Config` with `$config->setIpResolver((new TrustedProxyResolver([...]))->resolve(...))` and all `keyIp()`-keyed rules (plus keyless rules and `filterIp()`) will key on the resolved client IP automatically. Without a resolver, `REMOTE_ADDR` is used.
:::

::: warning
Never trust `X-Forwarded-For` or similar headers without configuring trusted proxies via `TrustedProxyResolver`. A `TrustedProxyResolver` only reads forwarded headers when the connecting peer matches a declared trusted proxy address; without that, a client can spoof any IP by sending their own `X-Forwarded-For` header. See [Client IP behind proxies](/getting-started#client-ip-behind-proxies).
:::

### Pattern kinds (`PortableConfig::patternEntry()`)
Expand Down
2 changes: 1 addition & 1 deletion docs/advanced/presets.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Resolve any preset by name with `Presets::get($name)` (a `PortableConfig`), pass

## Conventions and overrides

- The shipped presets target signals that are universal across applications (scanner User-Agents, missing browser headers, well-known sensitive paths), so they assume nothing about your routing. A preset you build yourself is just a `PortableConfig`, so it can key on whatever fits your environment, including routes your own apps standardize.
- The shipped presets target signals that are universal across applications (scanner User-Agents, missing browser headers, well-known sensitive paths), so they assume nothing about your routing. A preset you build yourself is a `PortableConfig`, so it can key on whatever fits your environment, including routes your own apps standardize.
- Override any rule by applying the preset with your own portable rules that redefine the rule by the same name (later layer wins), or by rebuilding the preset's schema.

> **Note:** `scannerBlocking()`'s `suspicious-headers` rule is the more aggressive of the two: some legitimate API clients, privacy tools, and embedded browsers also omit `Accept-*` headers. Drop or override it by name if your traffic includes non-browser clients.
Expand Down
2 changes: 0 additions & 2 deletions docs/advanced/psr17.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ A full example combining both approaches:
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Config\Response\Psr17BlocklistedResponseFactory;
use Flowd\Phirewall\Config\Response\Psr17ThrottledResponseFactory;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\InMemoryCache;
use Nyholm\Psr7\Factory\Psr17Factory;
Expand Down Expand Up @@ -421,7 +420,6 @@ class PhirewallFactory
```php [Laravel]
// In a service provider
use Flowd\Phirewall\Config;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\ApcuCache;
use Nyholm\Psr7\Factory\Psr17Factory;
Expand Down
20 changes: 9 additions & 11 deletions docs/advanced/request-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ $config->fail2ban->add('login',
);
```

With `RequestContext`, your handler verifies the credentials first, then signals a failure **only when authentication actually fails**. This gives you precise control over what counts as a failure.
With `RequestContext`, your handler verifies the credentials first, then signals a failure **only when authentication fails**. This gives you precise control over what counts as a failure.

## How It Works

Expand Down Expand Up @@ -51,7 +51,6 @@ Configure a fail2ban rule with a filter that **always returns `false`**. This me

```php
use Flowd\Phirewall\Config;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\InMemoryCache;
use Psr\Http\Message\ServerRequestInterface;
Expand Down Expand Up @@ -80,6 +79,7 @@ Retrieve the `RequestContext` from the request attribute and call `recordFailure

```php
use Flowd\Phirewall\Context\RequestContext;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
Expand All @@ -88,8 +88,9 @@ class LoginHandler implements RequestHandlerInterface
{
public function handle(ServerRequestInterface $request): ResponseInterface
{
$username = $request->getParsedBody()['username'] ?? '';
$password = $request->getParsedBody()['password'] ?? '';
$body = (array) $request->getParsedBody();
$username = $body['username'] ?? '';
$password = $body['password'] ?? '';

if (!$this->authenticate($username, $password)) {
// Retrieve the RequestContext attached by the middleware
Expand All @@ -100,10 +101,10 @@ class LoginHandler implements RequestHandlerInterface
// rule's own keyExtractor. Use the null-safe operator for safety.
$context?->recordFailure('login-failures');

return new JsonResponse(['error' => 'Invalid credentials'], 401);
return new Response(401, ['Content-Type' => 'application/json'], json_encode(['error' => 'Invalid credentials'], JSON_THROW_ON_ERROR));
Comment thread
sascha-egerer marked this conversation as resolved.
}

return new JsonResponse(['success' => true, 'user' => $username], 200);
return new Response(200, ['Content-Type' => 'application/json'], json_encode(['success' => true, 'user' => $username], JSON_THROW_ON_ERROR));
}
}
```
Expand All @@ -125,7 +126,6 @@ The first parameter to `recordFailure()` must **exactly** match the `name` you u
First, configure an allow2ban rule. To make the rule count *only* the events recorded by the handler (not every request), have the rule's `keyExtractor` return `null` pre-handler; the firewall then skips counting until the handler signals an explicit key via `recordHit()`:

```php
use Flowd\Phirewall\KeyExtractors;

$config->allow2ban->add(
'expensive-endpoint',
Expand Down Expand Up @@ -154,7 +154,7 @@ If the rule's `keyExtractor` returns a value pre-handler (the common case), the
$context?->recordHit('expensive-endpoint');
```

Note that when the rule's `keyExtractor` returns a value pre-handler, **both** the pre-handler counter and the handler's `recordHit()` increment the counter, so the threshold should account for the doubled count.
When the rule's `keyExtractor` returns a value pre-handler, **both** the pre-handler counter and the handler's `recordHit()` increment the counter, so the threshold should account for the doubled count.

Recorded failures and hits are processed together after your handler returns; retrieve them all with `getRecordedSignals()`.

Expand Down Expand Up @@ -212,7 +212,7 @@ if ($context !== null) {

$result->outcome->value; // 'pass', 'safelisted', etc.
$result->isPass(); // true if the request was allowed through
$result->rule; // Name of the matching rule (null if simply passed)
$result->rule; // Name of the matching rule (null if the request passed)
}
```

Expand Down Expand Up @@ -246,7 +246,6 @@ require __DIR__ . '/vendor/autoload.php';

use Flowd\Phirewall\Config;
use Flowd\Phirewall\Context\RequestContext;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\InMemoryCache;
use Nyholm\Psr7\Factory\Psr17Factory;
Expand Down Expand Up @@ -359,7 +358,6 @@ use Flowd\Phirewall\BanType;
use Flowd\Phirewall\Config;
use Flowd\Phirewall\Context\RequestContext;
use Flowd\Phirewall\Http\Firewall;
use Flowd\Phirewall\KeyExtractors;
use Flowd\Phirewall\Middleware;
use Flowd\Phirewall\Store\InMemoryCache;
use Nyholm\Psr7\Factory\Psr17Factory;
Expand Down
4 changes: 1 addition & 3 deletions docs/advanced/track-notifications.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Track rules provide **passive counting without blocking**. They are ideal for ob

## How Tracking Works

Track rules are evaluated **first** in the pipeline, before safelists and blocklists. They always run, even for requests that will be safelisted. This makes them reliable for comprehensive monitoring.
Track rules are evaluated **first** in the pipeline, before safelists and blocklists. They always run, even for requests that will be safelisted. This makes them reliable for monitoring all traffic.

```text
Request --> Track (passive) --> Safelist --> Blocklist --> Fail2Ban --> Throttle --> Allow2Ban --> Pass
Expand Down Expand Up @@ -62,7 +62,6 @@ $config->tracks
Monitor login attempts per IP for dashboards and anomaly detection:

```php
use Flowd\Phirewall\KeyExtractors;

$config->tracks->add('login-attempts',
period: 3600,
Expand All @@ -87,7 +86,6 @@ $config->tracks->add('api-usage',
Monitor access to admin or configuration pages:

```php
use Flowd\Phirewall\KeyExtractors;

$config->tracks->add('admin-access',
period: 600,
Expand Down
Loading
Loading