Enforce broker replay protection (iat expiry window + jti cache) #494

Open
opened 2026-07-26 02:53:02 -04:00 by didericis-claude · 1 comment
Collaborator

Split out from the Host control server PRD (#468, PR #486) — replay protection was judged too heavy for the MVP of that work and belongs on its own track.

Gap

sign_request already emits jti and iat; verify_request reads neither — no expiry window, no jti cache (broker.py:88). Harmless in-process (a fresh token per submit), but once the broker contract runs over a wire a captured launch token replays indefinitely.

Proposed enforcement

Have verify_request start reading both claims, fail-closed:

  • Expiry window. Reject a token whose iat is outside a small window (proposed ±60s, one constant) — bounds how long a captured token is useful and how much clock skew is tolerated.
  • jti cache. Keep a set of seen jtis for at least the expiry window; reject a repeat. The cache only needs to remember tokens still within their window, so it self-trims by iat — no unbounded growth, no persistence needed (a token older than the window is already rejected by the expiry check, so a cache lost on restart cannot admit a replay).

A missing or malformed jti/iat is a rejection, not a pass — both are additions to the existing BrokerAuthError path.

Notes

  • Pure in-process change; no transport needed, so it can land independently of the host control server work.
  • Open question carried over: with one host controller per host the in-memory jti cache is sufficient; if lifecycle ever fans out, the cache and the expiry window need to be co-located with the single verifier.
Split out from the **Host control server** PRD (#468, PR #486) — replay protection was judged too heavy for the MVP of that work and belongs on its own track. ## Gap `sign_request` already emits `jti` and `iat`; `verify_request` reads neither — no expiry window, no `jti` cache ([`broker.py:88`](../src/branch/main/bot_bottle/orchestrator/broker.py)). Harmless in-process (a fresh token per `submit`), but once the broker contract runs over a wire a captured launch token replays indefinitely. ## Proposed enforcement Have `verify_request` start reading both claims, fail-closed: - **Expiry window.** Reject a token whose `iat` is outside a small window (proposed ±60s, one constant) — bounds how long a captured token is useful and how much clock skew is tolerated. - **`jti` cache.** Keep a set of seen `jti`s for at least the expiry window; reject a repeat. The cache only needs to remember tokens still within their window, so it self-trims by `iat` — no unbounded growth, no persistence needed (a token older than the window is already rejected by the expiry check, so a cache lost on restart cannot admit a replay). A missing or malformed `jti`/`iat` is a rejection, not a pass — both are additions to the existing `BrokerAuthError` path. ## Notes - Pure in-process change; no transport needed, so it can land independently of the host control server work. - Open question carried over: with one host controller per host the in-memory `jti` cache is sufficient; if lifecycle ever fans out, the cache and the expiry window need to be co-located with the single verifier.
gitea-actions bot added the Status/Needs Triage label 2026-07-26 02:53:09 -04:00
didericis added Kind/FeatureKind/Security
Priority
Low
4
and removed Status/Needs Triage labels 2026-07-26 02:58:03 -04:00
gitea-actions bot added the Status/Needs Triage label 2026-07-26 02:58:07 -04:00
Owner

Low priority given this is all currently traffic on a trusted host device.

Low priority given this is all currently traffic on a trusted host device.
didericis removed the Status/Needs Triage label 2026-07-26 03:01:46 -04:00
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: didericis/bot-bottle#494