feat(operator): HMAC-signed queue message verification (forgery defense) - #137
Merged
Conversation
todie
added a commit
that referenced
this pull request
Aug 16, 2026
… rotation, fail-closed startup Review findings applied (8-angle fan-out on PR #137): - Envelope v1 now MAC-covers audience (queue name) + timestamp: sig = HMAC(key, "v1\n" + audience + "\n" + ts + "\n" + body). Domain separation kills cross-queue replay; a 10-min freshness window bounds same-queue replay. Both halves ship together, so the format change is free now and breaking later. - Fixed-set failure reasons — attacker-controlled field values are never interpolated into logs (128KB forged `v` field cannot flood stderr). - Sig format check (64 hex chars) BEFORE hashing: O(1) reject for malformed sigs, no HMAC pass over max-size forged bodies, and trailing-garbage sigs (silent Buffer.from hex truncation) now fail instead of verifying; dead "sig not hex" catch removed. - Key resolved ONCE at startup (injectable via OperatorOptions.hmac, same pattern as queueEnv) — posture cannot flip mid-run; SET-but-empty key refuses startup (present-but-empty ESO failure, estate secret rule); unsigned posture logged AT STARTUP (idle queue no longer hides it); /readyz reports hmac: off|warn|enforce. - LINEARCTL_QUEUE_HMAC_MODE=warn migration mode: processes non-verifying messages with an UNVERIFIED log line — closes the rollout-window data-loss trap (enforce + unsigned receiver = reject+ack = destroyed events). LINEARCTL_QUEUE_HMAC_KEY_PREV accepted during key rotation. - Control socket hardened to 0600 (dir 0700) — the /delegate path's trust boundary is filesystem perms; umask alone doesn't guarantee it. - Chart: queueHmacKeyPrev + queueHmacMode env; values.yaml documents the 4-step rollout order + ESO sequencing. Runbook: step-3 scoping caveat reconciled, step-4 env + envelope format, 6 new troubleshooting rows. Replay WITHIN the freshness window by a queue-read-capable principal remains possible and is documented as the v1 scope limit.
CF API tokens for Queues may scope account-wide (per-queue resource
scoping unverified — runbook step 3 carries the caveat), so a producer
token could forge AgentSessionEvents into linear-agent-events. Defense:
the webhook receiver signs every body at enqueue; the operator verifies
before processing, so queue-level write access alone cannot reach the
event loop.
- src/core/hmac-envelope.ts: v1 envelope {v, alg, ts, sig, body}; sig =
HMAC-SHA256(key, "v1\n"+audience+"\n"+ts+"\n"+body). Audience (queue
name) binding kills cross-queue replay; the MAC-covered timestamp
bounds same-queue replay to a 10-min window. Fixed-set failure reasons
(no attacker-controlled interpolation), 64-hex sig format check before
any hashing, constant-time digest compare. Replay WITHIN the window by
a queue-read-capable principal remains possible — documented v1 limit.
- Poller: key/prev-key/mode resolved ONCE at startup (injectable via
OperatorOptions.hmac); SET-but-empty key refuses startup; posture
logged at startup and surfaced in /readyz (hmac: off|warn|enforce);
"warn" migration mode processes non-verifying messages with an
UNVERIFIED log line (closes the rollout-window reject+ack data-loss
trap); LINEARCTL_QUEUE_HMAC_KEY_PREV accepted during rotation.
- Control socket hardened to 0600 (dir 0700) — the /delegate trust
boundary is filesystem perms, not umask luck.
- Chart: queueHmacKey/queueHmacKeyPrev/queueHmacMode env (guarded);
values.yaml documents the 4-step rollout order + rotation. Runbook:
step-3 scoping caveat, step-4 env + envelope format, troubleshooting
rows for every rejection reason.
Rebuilt as one commit on post-#136 main (supersedes 85867b2+a4959db;
review findings from the 8-angle pass are incorporated).
todie
force-pushed
the
feat/queue-hmac-verify
branch
from
August 16, 2026 11:59
a4959db to
2ac8e33
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Operator ruling 2026-08-16 (ecosystem program): implement HMAC-signed queue bodies as the forgery defense on the session-event queue, regardless of whether per-queue CF token scoping proves real.
What
src/core/hmac-envelope.ts— v1 envelope{v, alg, sig, body}, HMAC-SHA256 over the exact raw body string, constant-time verification.signBody/makeEnvelopeare the receiver-side reference implementation.LINEARCTL_QUEUE_HMAC_KEYset, unsigned/invalid messages are rejected+acked with a reason-only log line; unset = one startup warning + legacy processing (backward compatible until the receiver half deploys).env.queueHmacKeysecretKeyRef (linearctl-cf-queue/hmac-key), template-guarded.Verification
bun run typecheckclean;bun test487 pass / 0 fail (new hmac-envelope suite: round-trip, wrong-key, tamper, legacy-body reject, reason hygiene, short-sig safety).helm lintclean.Out of scope
Receiver-side signing (
unsigned/ggservices/linear-webhook) — separate repo/lane; this consumer side is inert until the key is configured.