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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ A route without a guard argument is scanned but never blocked; that is Rocket's

## Pipeline and config surface

`GuardFairing::from_security_config(&config)` (and `from_security_config_with(&config, &WiringOptions)` for the injected telemetry agent) builds the whole pipeline through the facade's `Wiring`, the one `SecurityConfig` mapping the Rust adapters share; the adapter installs each part on its builder and owns only the seams the wiring leaves on `Wiring::config` (`fail_secure`, `trusted_proxies`, `exclude_paths`, `auth_verifier`, `body_read_timeout`). `route_resolution_strict` is not applied (Rocket resolves routes after `on_request`).
`GuardFairing::from_security_config(&config)` (and `from_security_config_with(&config, &WiringOptions)` for the injected telemetry agent) builds the whole pipeline through the facade's `Wiring`, the one `SecurityConfig` mapping the Rust adapters share; the adapter installs each part on its builder and owns only the seams the wiring leaves on `Wiring::config` (`fail_secure`, `trusted_proxies`, `exclude_paths`, `auth_verifier`, `body_read_timeout`, `route_resolution_strict`). The config is validated first (`GuardConfigError::Config`). The stages a dynamic rule can change are read per request from a `LiveCell` the fairing builds at `on_ignite` (`Wiring::live`, stored in `GuardEngine::live`); a builder-built fairing keeps static stages. With the `agent` feature the agent is built from the `agent_*` fields when none is injected (`src/agent.rs`, degrade or `GuardConfigError::Agent` per `agent_strict`); the `redis` feature forwards `guard-core-rs/redis`.

- **Client identity**: resolved once per request in `on_request` from the connecting peer (`Request::remote()`) and `X-Forwarded-For` through the trusted proxies, stashed in a request-local slot and read by every stage, the guards and the response pass. Rocket's `client_ip()` is never used (it trusts `X-Real-IP`). `fail_secure` rejects a peer-less request with the `client_address_unresolved` block.
- **`exclude_paths`**: the engine's `PathExclusions` matcher; an excluded request is scoped (route resolution, bans, the global IP rules, the rate limit), carries the public `GuardExclusionScoped` marker and gets the scoped response pass. A route that bypasses `all` emits `security_bypass` and gets only the modifier.
- **Response pass**: the processor from the wiring (shared ban manager) plus the fairing-level modifier, which runs even without a processor. `return_pattern` body rules buffer the response body in `on_response` (bounded by `body_read_timeout`, a failure answers `500`) and re-emit it intact.
- **Strict routes, request-body timeout and usage rules**: `route_resolution_strict` is decided in `on_response` (`Request::route()` is `None` only when the router matched nothing, so no handler ran; the answer becomes `500 Route resolution failed`, log-only under `passive_mode`). `GuardBody` bounds its read with `body_read_timeout` and answers `408` (Rocket itself peeks 14 bytes before routing, outside this bound). The route usage rules run after the pipeline passed: from `BlockGuard`/`GuardBody` on success, or once in `on_response` for an unguarded route (`scan::usage_rules_verdict`).
- **Refusals**: Rocket cannot answer from `on_request`, so every block, including the new ones, is a verdict stashed in request-local state, enforced by the guards and rendered by the catchers or the `404` rewrite.

## Engine Integration
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ The parity release: the reference `exclude_paths`, client and response-pass sema
- The `security_bypass` event for an all-checks-bypass route (`all_checks_bypassed`), whose request forwards with only the response modifier applied (no metrics, headers or return rules) and continues through the pipeline under `passive_mode`
- Body `return_pattern` rules now read the response body: the response body is buffered when a rule needs it, bounded in time by `body_read_timeout`, re-emitted intact, and a read error or timeout answers the fail-secure `500`

- Reference parity: `SecurityConfig::validate` runs at construction (`GuardConfigError::Config`), the stages a dynamic rule can change (geo, cloud, user agent, emergency mode, the penetration toggle, the rate limit and ban knobs) are read from the live config on every request (`LiveCell`, re-exported with `LiveContext` and `LiveStages`), `route_resolution_strict` answers `500 Route resolution failed` for a request that matched no route (decided in `on_response`, log-only under `passive_mode`, emits `route_unresolved` and fires `on_block`, `GuardFairing::with_route_resolution_strict`), `GuardBody` answers `408 Request body read timed out` when the body does not arrive within `body_read_timeout`, and the cargo features `redis` (forwards `guard-core-rs/redis`) and `agent` (builds the guard-agent-rs agent from the `agent_*` fields, `GuardConfigError::Agent`, degraded state in `agent_stats()`)

### Changed

- The route usage rules (`process_usage_rules`) run after the whole pipeline passed instead of before it: a request the pipeline or a guard refuses is no longer counted, and neither is a request with no client ip. Configs that were invalid but accepted before (for example `auto_ban_threshold = 0`) are now refused by `from_security_config`; `Wiring::build_unvalidated` is the unvalidated seam
- `exclude_paths` uses the engine's reference matcher instead of an exact raw-path comparison: `/static` covers `/static/app.js`, percent-encoding and dot segments are resolved, and `exclude_paths()` reports the normalized entries. An excluded request is no longer exempt from the pipeline: bans, the global IP lists, country and cloud rules and the rate limit apply, and the request is counted in the metrics. Tests that encoded the old exact-match bypass were updated
- The client identity no longer comes from Rocket's `client_ip()`, which honors the `X-Real-IP` header from any peer: it is the connecting peer, with `X-Forwarded-For` honored only through the trusted proxies
- `from_security_config` builds through `Wiring`, so the fields it used to drop now land: geo country rules and `geo_ip_handler`, `block_cloud_providers`, `endpoint_rate_limits`, `enable_rate_limit_auto_ban`, `custom_request_check`, `auth_verifier`, `custom_response_modifier`, `on_error`, `cors_expose_headers`, `trusted_proxies`, the telemetry and event bus, dynamic rules, and the redis stores when `guard-core-rs` has its `redis` feature. The response processor now shares the ban manager with the ban stage, so a return-rule `ban` is enforced by the next request. A config with `fail_secure = true` (the default) rejects a request that carries no peer address
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ A route without a guard argument is scanned but never blocked; that is Rocket's

## Pipeline and config surface

`GuardFairing::from_security_config(&config)` (and `from_security_config_with(&config, &WiringOptions)` for the injected telemetry agent) builds the whole pipeline through the facade's `Wiring`, the one `SecurityConfig` mapping the Rust adapters share; the adapter installs each part on its builder and owns only the seams the wiring leaves on `Wiring::config` (`fail_secure`, `trusted_proxies`, `exclude_paths`, `auth_verifier`, `body_read_timeout`). `route_resolution_strict` is not applied (Rocket resolves routes after `on_request`).
`GuardFairing::from_security_config(&config)` (and `from_security_config_with(&config, &WiringOptions)` for the injected telemetry agent) builds the whole pipeline through the facade's `Wiring`, the one `SecurityConfig` mapping the Rust adapters share; the adapter installs each part on its builder and owns only the seams the wiring leaves on `Wiring::config` (`fail_secure`, `trusted_proxies`, `exclude_paths`, `auth_verifier`, `body_read_timeout`, `route_resolution_strict`). The config is validated first (`GuardConfigError::Config`). The stages a dynamic rule can change are read per request from a `LiveCell` the fairing builds at `on_ignite` (`Wiring::live`, stored in `GuardEngine::live`); a builder-built fairing keeps static stages. With the `agent` feature the agent is built from the `agent_*` fields when none is injected (`src/agent.rs`, degrade or `GuardConfigError::Agent` per `agent_strict`); the `redis` feature forwards `guard-core-rs/redis`.

- **Client identity**: resolved once per request in `on_request` from the connecting peer (`Request::remote()`) and `X-Forwarded-For` through the trusted proxies, stashed in a request-local slot and read by every stage, the guards and the response pass. Rocket's `client_ip()` is never used (it trusts `X-Real-IP`). `fail_secure` rejects a peer-less request with the `client_address_unresolved` block.
- **`exclude_paths`**: the engine's `PathExclusions` matcher; an excluded request is scoped (route resolution, bans, the global IP rules, the rate limit), carries the public `GuardExclusionScoped` marker and gets the scoped response pass. A route that bypasses `all` emits `security_bypass` and gets only the modifier.
- **Response pass**: the processor from the wiring (shared ban manager) plus the fairing-level modifier, which runs even without a processor. `return_pattern` body rules buffer the response body in `on_response` (bounded by `body_read_timeout`, a failure answers `500`) and re-emit it intact.
- **Strict routes, request-body timeout and usage rules**: `route_resolution_strict` is decided in `on_response` (`Request::route()` is `None` only when the router matched nothing, so no handler ran; the answer becomes `500 Route resolution failed`, log-only under `passive_mode`). `GuardBody` bounds its read with `body_read_timeout` and answers `408` (Rocket itself peeks 14 bytes before routing, outside this bound). The route usage rules run after the pipeline passed: from `BlockGuard`/`GuardBody` on success, or once in `on_response` for an unguarded route (`scan::usage_rules_verdict`).
- **Refusals**: Rocket cannot answer from `on_request`, so every block, including the new ones, is a verdict stashed in request-local state, enforced by the guards and rendered by the catchers or the `404` rewrite.

## Engine Integration
Expand Down
Loading
Loading