Skip to content

feat(security): origin-side rate limiter — PR1 (shared-store limiter + tiering, flag-off) - #2263

Merged
jung-thomas merged 1 commit into
DEVfrom
feat-origin-abuse-protection
Sep 12, 2026
Merged

jung-thomas merged 1 commit into
DEVfrom
feat-origin-abuse-protection

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

What & why

First PR of the origin-side abuse-protection series (plan: assume Akamai Bot Manager removal, since its UA/behavioral checks 403 our agentic MCP/A2A clients). Moves the protection those edge rules provided onto our side, as defense-in-depth. Ships flag-OFF (RATE_LIMIT_ENABLED / ImsConfig flag.ratelimit, default false) — no behavior change until enabled on DEV.

Scope (PR1 of 4)

Cross-instance rate limiting on the anonymous/expensive surface + a zero-consumer-setup trusted-caller tier system + observability. (PR2 Fast-Purge, PR3 input validation, PR4 load-shedding to follow.)

New

  • srv/lib/rate-limit/shared-limiter.js — fixed-window counter keyed by (routeClass, tier, clientKey), backed by the shared cds-caching store (cross-instance in hybrid/prod). Window governed by windowStart inside the value; TTL is GC-only (a mid-window write can't slide the window). Fail-open: any store fault → per-instance in-memory Map; if that throws, request is allowed. Non-atomic RMW can under-count → effective limit is looser, never tighter (documented; not for exactness).
  • srv/lib/rate-limit/agent-identity.js — resolveTier(req):
    • trusted — valid HMAC-SHA256 header (keyId\nmethod\npath\nts, ±5-min replay window), secret via credstore (AGENT_HMAC_SECRET, rotatable in /admin-ui/#secrets).
    • auth — a bearer token / api-key is already present (keyed off a hash — zero new setup).
    • anon — everyone else (IP floor).
    • Identity is a tier, not a gate — unknown callers are limited, never blocked.
  • srv/lib/runtime-config/rate-limit-settings.js — ImsConfig-backed numeric thresholds (window, per-routeClass limits, tier multipliers, abuse threshold), 5s cache, defaults on any failure. Env-free.
  • srv/lib/rate-limit/register.js — middleware + metrics counters (ratelimit.hits, ratelimit.blocked[route=,tier=]) + debounced RateLimitAbuse alert. Mounted in cds.on('bootstrap') on /content, /build, /graph, /feedback, /homepage, /api, and the agentic /mcp*, /a2a, /chat/stream, /graphql/public.

Modified

  • srv/server.js — call registerRateLimit(app) in bootstrap. Legacy always-on /search limiter left intact (intentionally not superseded while this flag defaults OFF — avoids dropping an always-on floor).
  • srv/lib/feature-flags/registry.js — RATE_LIMIT_ENABLED entry (kind:db, category Security, dev-only).
  • package.json — RateLimitAbuse alert eventType (⚠️ needs manual ANS cockpit condition + subscription wiring per ops runbook before it delivers).

Tests

test/unit/rate-limit.test.js — 15 tests: window allow/block/reset + retryAfter, fail-open fallback counting, key isolation, IP derivation, tier resolution (anon/auth/trusted), HMAC replay + tamper + wrong-path rejection, config defaulting. Registry drift test passes. Full unit suite: only pre-existing env failures (missing island deps / srv-qa bundle / a JWT-exp flake), none related.

Notes

  • Fail-open everywhere; never throws into a request path.
  • srv-qa cp list unaffected — new modules aren't reachable from content-store.js.
  • Rollout: merge flag-OFF → enable on DEV via setFeatureFlag('RATE_LIMIT_ENABLED', true), observe ratelimit.* metrics → then PROD.

Adds a fail-open, DB-flag-gated (RATE_LIMIT_ENABLED / ImsConfig flag.ratelimit,
default OFF) rate limiter on the anonymous/expensive surface, backed by the
shared cds-caching store for cross-instance counters — origin-side defense-in-
depth assuming Akamai Bot Manager removal.

- shared-limiter.js: fixed-window counter keyed by (routeClass,tier,clientKey),
  window governed by windowStart (TTL is GC-only); fail-open to a per-instance
  in-memory Map on any store fault. Non-atomic RMW under-counts (looser, never
  tighter) — documented.
- agent-identity.js: tier resolver (trusted=valid HMAC-SHA256 w/ ±5min replay
  window, secret via credstore; auth=already-present bearer/api-key; anon=IP).
  Identity is a tier, not a gate — unknown callers are limited, never blocked.
- rate-limit-settings.js: ImsConfig-backed numeric thresholds, 5s cache,
  defaults on any read failure.
- register.js: middleware + metrics counters + debounced RateLimitAbuse alert;
  mounted in bootstrap on /content,/build,/graph,/mcp*,/a2a,etc. Legacy always-
  on /search limiter left intact (not superseded while this flag defaults OFF).
- registry RATE_LIMIT_ENABLED entry; package.json alerts eventType RateLimitAbuse.

Ships flag-OFF. srv-qa cp-list unaffected (not reachable from content-store.js).
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