Skip to content

fix(guard): disable admission limits by default - #5689

Merged
NathanFlurry merged 1 commit into
mainfrom
fix/guard-unbounded-admission-defaults-oss
Sep 10, 2026
Merged

fix(guard): disable admission limits by default#5689
NathanFlurry merged 1 commit into
mainfrom
fix/guard-unbounded-admission-defaults-oss

Conversation

@NathanFlurry

Copy link
Copy Markdown
Member
  • Disable per-IP request-rate and in-flight admission limits by default while preserving opt-in controls.
  • Expose Guard retry, timeout, connection-pool, and admission-state settings through the configuration schema.
  • Report request-rate and maximum-in-flight rejections as distinct errors with configured thresholds.
  • Add focused coverage and document the self-hosted admission defaults.

@railway-app

railway-app Bot commented Sep 10, 2026

Copy link
Copy Markdown

🚅 Deployed to the actors-pr-5689 environment in rivet-frontend

Service Status Web Updated
frontend-inspector 😴 Sleeping (View Logs) Web Sep 10, 2026 at 1:02 am UTC
kitchen-sink 😴 Sleeping (View Logs) Web Sep 10, 2026 at 1:01 am UTC
frontend-cloud 😴 Sleeping (View Logs) Web Sep 10, 2026 at 12:58 am UTC
ladle ✅ Success (View Logs) Web Sep 10, 2026 at 12:54 am UTC
mcp-hub ✅ Success (View Logs) Web Sep 10, 2026 at 12:52 am UTC
website ❌ Build Failed (View Logs) Web Sep 10, 2026 at 12:52 am UTC

@railway-app
railway-app Bot temporarily deployed to rivet-frontend / actors-pr-5689 September 10, 2026 00:52 Destroyed
@claude

claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Review: fix(guard): disable admission limits by default

Makes Guard's per-IP rate limiting and max-in-flight admission control opt-in (previously hardcoded to always-on 10k req/min and 2k concurrent), and exposes a batch of previously-hardcoded proxy timeout/retry/pool-idle settings through guard.* config. Solid refactor overall: ClientState/InFlightPermit cleanly move to Option-based fields so the disabled path has no cache/lock overhead, Guard::validate() follows the existing Root::validate_and_set_defaults() pattern, and the new unit tests (guard.rs, proxy_service.rs, utils/tests.rs) directly cover the new default-off, independent-rejection, and zero-value-validation behavior.

Bug: sub-second upstream_request_timeout_ms renders as "0 seconds" in the timeout error

upstream_request_timeout_ms is now configurable down to 1ms (schemars range min = 1 in engine/packages/config/src/config/guard.rs), but errors::RequestTimeout still reports timeout_seconds via timeout_duration.as_secs() (proxy_service.rs, in the handle_http_request timeout branch), and its message template is "Request timed out during PHASE after N seconds." An operator who sets guard.upstream_request_timeout_ms to 500 will see "0 seconds" reported on every timeout, which is misleading. Before this PR the timeout was hardcoded to a whole 30s, so as_secs() was always accurate; now that it's ms-configurable, the error should report milliseconds instead (or at least not silently truncate to 0).

Worth confirming: this flips the default DoS posture at the edge

Previously every Guard instance enforced a built-in rate limit (10k req/min) and in-flight cap (2k) per source IP with no way to disable them. After this change both are off unless an operator explicitly sets guard.rate_limit / guard.max_in_flight. Per this repo's trust-boundary conventions, the client-to-engine path is untrusted and Guard sits on that boundary, so self-hosted deployments that don't proactively configure these now have no built-in protection against a single IP flooding requests or holding many concurrent connections. That may well be the right call if the old fixed defaults were causing false-positive throttling in practice (the "fix" framing suggests this was the motivation), but it's worth making sure this default flip is called out clearly for upgraders (release notes / changelog) beyond the limits.mdx table update, since it's a meaningful change in out-of-the-box security posture.

Minor / FYI

  • engine/packages/guard-core/tests/proxy.rs, tests/websocket.rs, and tests/common/mod.rs already fail to compile against current src/ independent of this PR: they import RoutingTimeout, RateLimitConfig, and MaxInFlightConfig from proxy_service:: paths that don't exist on main either (confirmed against main). Not something to fix here, but it means cargo test -p rivet-guard-core can't currently exercise the integration suite for this change; only --lib picks up the new inline tests, so the proxy/websocket admission path is effectively uncovered end-to-end right now. Possibly worth a follow-up to repair or delete the stale integration tests.
  • errors::RateLimit's message now includes the configured requests/period_ms, and the new errors::MaxInFlight follows the same convention; the JSON artifact at engine/artifacts/errors/guard.max_in_flight.json matches the code/group/message shape used by sibling files.

Nice targeted test coverage for the new behavior (admission_limits_are_disabled_by_default, configured_admission_limits_are_enforced_independently, client_state_reports_rate_limit_separately, etc.) using the Option-based ClientState refactor to verify the hot path stays a no-op when both controls are disabled.

🤖 Generated with Claude Code

@NathanFlurry

Copy link
Copy Markdown
Member Author

Engine preview

Official multi-architecture debug images built from PR head 1625f467e6c3016facc3427c0f7ae476ab0c6398, directly on Potato's connection-pool fix 73475cfff56011d54f4529aa008ce17232b894ea:

docker pull rivetdev/engine:full-1625f46
docker pull rivetdev/engine:slim-1625f46

Immutable multi-architecture manifests:

  • rivetdev/engine@sha256:bddbd34088942e00ae2eaae77b238f25efb3871b63c8c2a277d54ede12a6282f (full)
  • rivetdev/engine@sha256:a4fed4293a71bf25b85c59264844dbdc8727681cad87f8ccf2446cf17c148782 (slim)

Publish workflow: https://github.com/rivet-dev/actors/actions/runs/34423140884

@NathanFlurry
NathanFlurry marked this pull request as ready for review September 10, 2026 19:33
@NathanFlurry
NathanFlurry merged commit 0c91da4 into main Sep 10, 2026
32 of 35 checks passed
@NathanFlurry
NathanFlurry deleted the fix/guard-unbounded-admission-defaults-oss branch September 10, 2026 19:33

@the-company-company the-company-company Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 1 medium-severity finding

Reviewed commit 1625f46.

Comment on lines +103 to +107
if guard_config.rate_limit().is_some() || guard_config.max_in_flight().is_some() {
Some(
Cache::builder()
.max_capacity(guard_config.admission_client_state_cache_capacity())
.time_to_idle(guard_config.admission_client_state_cache_idle_timeout())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Medium · Keep admission state alive while requests hold permits

This cache can evict an IP while its ClientState is still held by an InFlightPermit. With admission_client_state_cache_capacity: 1, admitting a request from a second IP can evict the first IP; a subsequent request from that first IP creates a new counter and is admitted even when max_in_flight: 1 is already occupied. The same state split resets an active fixed-window rate limit. time_to_idle has the same failure mode for a long-running request after the configured idle period.

Retain live admission entries independently of the bounded/idle cache, or make eviction conditional on there being no in-flight permits. The capacity policy must not create a second ClientState for an IP with an active permit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant