Skip to content

fix(gateway): cap auth limiter entries#96224

Merged
eleqtrizit merged 1 commit into
openclaw:mainfrom
eleqtrizit:773
Jun 24, 2026
Merged

fix(gateway): cap auth limiter entries#96224
eleqtrizit merged 1 commit into
openclaw:mainfrom
eleqtrizit:773

Conversation

@eleqtrizit

Copy link
Copy Markdown
Contributor

Summary

Caps the Gateway auth rate limiter's tracked client entries so failed-auth bookkeeping cannot grow without bound between prune sweeps.

Changes

  • Adds a normalized internal maxEntries option with a default cap of 10,000 tracked identities.
  • Prunes expired entries before eviction, evicts the oldest unlocked entry when at capacity, and preserves active lockouts.
  • Fails closed with a temporary overflow lock when every tracked entry is actively locked, avoiding a fail-open saturated-table path.
  • Adds focused Gateway auth limiter tests for flood capping, lockout-preserving eviction, saturated overflow behavior, and numeric normalization.

Validation

  • corepack pnpm exec oxfmt --check --threads=1 src/gateway/auth-rate-limit.ts src/gateway/auth-rate-limit.test.ts
  • node scripts/run-vitest.mjs src/gateway/auth-rate-limit.test.ts (4 files, 136 tests)
  • .agents/skills/autoreview/scripts/autoreview --mode local (clean)

Notes

@eleqtrizit
eleqtrizit requested a review from a team as a code owner June 24, 2026 00:10
@openclaw-barnacle openclaw-barnacle Bot added gateway Gateway runtime size: S maintainer Maintainer-authored PR labels Jun 24, 2026
@eleqtrizit

Copy link
Copy Markdown
Contributor Author

[CODEX BEHAVIORAL PROOF]

I checked the live PR surfaces before posting this: there are no existing PR comments or review comments from ClawSweeper, and no existing eleqtrizit re-review comment to delete.

What the change proves:

  • The Gateway auth limiter now resolves an internal max-entry cap and uses it before inserting a fresh failed-auth identity: src/gateway/auth-rate-limit.ts:137, src/gateway/auth-rate-limit.ts:143, src/gateway/auth-rate-limit.ts:231.
  • At capacity it prunes expired entries, then evicts only the oldest non-locked entry: src/gateway/auth-rate-limit.ts:293.
  • If every tracked entry is actively locked, it fails closed with a temporary overflow lock instead of admitting an untracked fresh identity: src/gateway/auth-rate-limit.ts:231, src/gateway/auth-rate-limit.ts:271.
  • Production Gateway auth uses this limiter for the wrong shared-secret token/password path: src/gateway/auth.ts:109, src/gateway/auth.ts:405, src/gateway/auth.ts:427; the server creates the shared limiter through createAuthRateLimiter(rateLimitConfig ?? {}) at src/gateway/server.impl.ts:464.
  • The sibling control-plane limiter already has the same bounded-memory invariant for unique-key pressure: src/gateway/control-plane-rate-limit.ts:9.

Focused regression coverage in this PR:

  • Unique-client flood is capped: src/gateway/auth-rate-limit.test.ts:163.
  • Locked entries survive flood eviction: src/gateway/auth-rate-limit.test.ts:176.
  • Saturated all-locked state fails closed and recovers after lockout expiry: src/gateway/auth-rate-limit.test.ts:193.
  • Invalid or fractional maxEntries inputs normalize to a safe integer cap: src/gateway/auth-rate-limit.test.ts:222.

Behavioral probe I ran locally against the production authorization path:

corepack pnpm exec tsx - <<'EOF_PROOF'
import {
  AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET,
  createAuthRateLimiter,
} from "./src/gateway/auth-rate-limit.ts";
import { authorizeGatewayConnect } from "./src/gateway/auth.ts";

const limiter = createAuthRateLimiter({ pruneIntervalMs: 0 });
const attempts = 10_001;
const makeIp = (index: number) => `198.51.${Math.floor(index / 254)}.${(index % 254) + 1}`;

for (let i = 0; i < attempts; i += 1) {
  const result = await authorizeGatewayConnect({
    auth: { mode: "token", token: "expected-token", allowTailscale: false },
    connectAuth: { token: "wrong-token" },
    req: {
      socket: { remoteAddress: "127.0.0.1" },
      headers: { "x-forwarded-for": makeIp(i) },
    } as never,
    trustedProxies: ["127.0.0.1"],
    rateLimiter: limiter,
  });
  if (result.ok || result.reason !== "token_mismatch") {
    throw new Error(`unexpected auth result at ${i}: ${JSON.stringify(result)}`);
  }
}

const firstClient = limiter.check(makeIp(0), AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
const lastClient = limiter.check(makeIp(attempts - 1), AUTH_RATE_LIMIT_SCOPE_SHARED_SECRET);
const proof = {
  attempts,
  retainedEntries: limiter.size(),
  firstClientRemaining: firstClient.remaining,
  lastClientRemaining: lastClient.remaining,
  firstClientAllowed: firstClient.allowed,
  lastClientAllowed: lastClient.allowed,
};
if (proof.retainedEntries !== 10_000 || proof.firstClientRemaining !== 10 || proof.lastClientRemaining !== 9) {
  throw new Error(`proof assertion failed: ${JSON.stringify(proof)}`);
}
console.log(JSON.stringify(proof, null, 2));
limiter.dispose();
EOF_PROOF

Output:

{
  "attempts": 10001,
  "retainedEntries": 10000,
  "firstClientRemaining": 10,
  "lastClientRemaining": 9,
  "firstClientAllowed": true,
  "lastClientAllowed": true
}

This exercises the real authorizeGatewayConnect() wrong-token path with trusted-proxy client IP resolution. The 10,001st unique failed-auth identity does not grow the retained map beyond 10,000; the first identity is evicted, and the last identity remains tracked under the shared-secret scope.

Validation run now:

  • node scripts/run-vitest.mjs src/gateway/auth-rate-limit.test.ts -> 4 files passed, 136 tests passed.
  • corepack pnpm exec oxfmt --check --threads=1 src/gateway/auth-rate-limit.ts src/gateway/auth-rate-limit.test.ts -> all matched files use the correct format.
  • PR CI already has Real behavior proof passing, plus the security-sensitive and dependency guard jobs passing. Some broad matrix checks were still pending when I checked.

@eleqtrizit

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@clawsweeper

clawsweeper Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 23, 2026, 11:09 PM ET / 03:09 UTC.

Summary
The branch adds a normalized maxEntries cap to createAuthRateLimiter, fails closed when all retained entries are locked, and adds Gateway auth limiter regression tests.

PR surface: Source +59, Tests +82. Total +141 across 2 files.

Reproducibility: yes. Source inspection of current main and v2026.6.9 shows recordFailure inserts one map entry per unique non-exempt failed-auth key and only timer/manual prune reclaims entries.

Review metrics: 1 noteworthy metric.

  • Limiter API And Default Surface: 1 SDK-facing option added, 1 default cap added, 0 public config fields added. The PR changes exported limiter behavior and plugin-SDK type shape while intentionally leaving gateway.auth.rateLimit operator config unchanged.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #77986
Summary: The current PR is a candidate fix for the canonical Gateway auth limiter hard-cap issue, with one older overlapping candidate PR still open.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • none.

Risk before merge

  • [P1] The default limiter behavior changes from prune-only retention to a 10,000-entry cap that evicts the oldest non-locked failed-auth identities before prune would have removed them.
  • [P1] When every retained identity is actively locked, the PR intentionally fails closed for new untracked identities, so a legitimate new client can receive a temporary 429 during a fully saturated lockout table.
  • [P1] This PR and fix(gateway): cap auth-rate-limit entries map under unique-IP flood #77987 overlap on the same canonical issue; maintainers should choose one canonical branch before merge.

Maintainer options:

  1. Adopt This Branch As Canonical
    Maintainers can accept the new SDK-facing cap/default and fail-closed saturated-table behavior, then close or supersede the older overlapping PR after merge.
  2. Expose Operator Tuning First
    If the cap should be operator-controlled, wire maxEntries through public gateway config types, strict schema, docs, and upgrade tests before merge.
  3. Pause One Duplicate Path
    If the older PR remains the preferred vehicle, pause or close this branch so only one candidate owns the canonical issue.

Next step before merge

  • [P1] Protected-label handling, the fail-closed/default tradeoff, and the older overlapping PR require maintainer choice rather than a ClawSweeper repair job.

Security
Cleared: No concrete security or supply-chain regression was found; the diff hardens an auth-adjacent availability path without changing secrets, dependencies, workflows, or persisted data.

Review details

Best possible solution:

Merge one canonical normalized Gateway auth limiter cap after maintainer confirmation that this branch supersedes the older overlapping PR and that the fail-closed saturated-table behavior is the desired availability/security tradeoff.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection of current main and v2026.6.9 shows recordFailure inserts one map entry per unique non-exempt failed-auth key and only timer/manual prune reclaims entries.

Is this the best way to solve the issue?

Yes. The insert-time cap is the narrow Gateway-owned fix for the reported flood; keeping maxEntries out of public gateway config avoids a broader operator-facing surface unless maintainers explicitly want tuning.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against d15e89a83e61.

Label changes

Label changes:

  • add P2: This is a focused Gateway auth availability hardening PR with limited blast radius and no evidence of an active outage.
  • add merge-risk: 🚨 compatibility: The PR changes the exported limiter type and default retention behavior for high-cardinality failed-auth traffic.
  • add merge-risk: 🚨 availability: The intentional all-locked overflow path can temporarily deny new untracked clients with 429 responses under saturation.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR comment includes after-fix terminal output from the production authorizeGatewayConnect() wrong-token path proving the retained map stays capped at 10,000 entries.
  • add rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • add status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR comment includes after-fix terminal output from the production authorizeGatewayConnect() wrong-token path proving the retained map stays capped at 10,000 entries.

Label justifications:

  • P2: This is a focused Gateway auth availability hardening PR with limited blast radius and no evidence of an active outage.
  • merge-risk: 🚨 compatibility: The PR changes the exported limiter type and default retention behavior for high-cardinality failed-auth traffic.
  • merge-risk: 🚨 availability: The intentional all-locked overflow path can temporarily deny new untracked clients with 429 responses under saturation.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR comment includes after-fix terminal output from the production authorizeGatewayConnect() wrong-token path proving the retained map stays capped at 10,000 entries.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR comment includes after-fix terminal output from the production authorizeGatewayConnect() wrong-token path proving the retained map stays capped at 10,000 entries.
Evidence reviewed

PR surface:

Source +59, Tests +82. Total +141 across 2 files.

View PR surface stats
Area Files Added Removed Net
Source 1 64 5 +59
Tests 1 82 0 +82
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 2 146 5 +141

Acceptance criteria:

  • [P1] node scripts/run-vitest.mjs src/gateway/auth-rate-limit.test.ts.
  • [P1] corepack pnpm exec oxfmt --check --threads=1 src/gateway/auth-rate-limit.ts src/gateway/auth-rate-limit.test.ts.

What I checked:

  • Root policy read: Root AGENTS.md was read fully and applied; it treats auth/config/default and fail-closed changes as compatibility-sensitive review surfaces. (AGENTS.md:1, d15e89a83e61)
  • Gateway policy read: Gateway-scoped AGENTS.md was read fully and applied to this Gateway auth/test review. (src/gateway/AGENTS.md:1, d15e89a83e61)
  • Current uncapped insert path: Current main inserts a new rate-limit entry for each fresh non-exempt key in recordFailure without enforcing a hard entry cap. (src/gateway/auth-rate-limit.ts:222, d15e89a83e61)
  • Gateway auth caller: Wrong token/password paths call recordFailure, and the pre-check returns rate_limited when limiter check() denies an attempt. (src/gateway/auth.ts:405, d15e89a83e61)
  • PR head implementation: The PR resolves maxEntries with resolveIntegerOption(..., { min: 1 }), enforces capacity before inserting a fresh key, prunes expired entries, evicts the oldest unlocked entry, and returns false when all retained entries are actively locked. (src/gateway/auth-rate-limit.ts:143, 72b3124f14aa)
  • PR head tests: The PR adds coverage for unique-client flood capping, lockout-preserving eviction, all-locked overflow behavior, and invalid/fractional maxEntries normalization. (src/gateway/auth-rate-limit.test.ts:163, 72b3124f14aa)

Likely related people:

  • buerbaumer: Auth limiter PR 15035 and commit 30b6ecc introduced the Gateway auth rate limiter and adjacent tests/config type. (role: introduced behavior; confidence: high; commits: 30b6eccae5af; files: src/gateway/auth-rate-limit.ts, src/gateway/auth-rate-limit.test.ts, src/config/types.gateway.ts)
  • steipete: Merged PR 15035 and authored follow-up Gateway auth rate-limit serialization and hook key normalization work in the same area. (role: merger and recent adjacent contributor; confidence: high; commits: 30b6eccae5af, 032dbf0ec6cd, 3284d2eb227e; files: src/gateway/auth-rate-limit.ts, src/gateway/auth.ts, src/gateway/rate-limit-attempt-serialization.ts)
  • fede-kamel: Authored the older overlapping candidate PR and has merged adjacent bootstrap-token rate-limit hardening on Gateway auth paths. (role: recent adjacent auth-hardening contributor; confidence: medium; commits: ecbd97e9682d, c5bcb62fa101; files: src/gateway/auth-rate-limit.ts, src/gateway/server/ws-connection/auth-context.ts, src/gateway/server.preauth-bootstrap-token-rate-limit.test.ts)
  • Menglin Li: Added the sibling Gateway control-plane bucket cap used as the nearest bounded-memory precedent for unique-key pressure. (role: adjacent bounded-memory contributor; confidence: medium; commits: 36c3a54b51ec; files: src/gateway/control-plane-rate-limit.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. labels Jun 24, 2026
@eleqtrizit
eleqtrizit merged commit bcbd521 into openclaw:main Jun 24, 2026
158 of 168 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 25, 2026
@eleqtrizit
eleqtrizit deleted the 773 branch June 30, 2026 19:27
QiuYuang pushed a commit to QiuYuang/openclaw that referenced this pull request Jul 1, 2026
wheakerd pushed a commit to wheakerd/clawdbot that referenced this pull request Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime maintainer Maintainer-authored PR merge-risk: 🚨 availability 🚨 May cause crashes, hangs, restart loops, stalls, or process outages. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: gateway auth rate limiter entries map has no hard cap under unique-IP flood

1 participant