Rate-limit /api/auth/unlock (#110 item 1) #149
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lz/agent-nexus#149
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
This is a live exposure right now, independent of MCP. Filed here because MCP support makes
/unlockthe thing that authorizes OAuth token issuance, which raises what a successful guess is worth — but it should ship regardless of whether MCP ever does.Splits item 1 out of #110, which says explicitly: "Filed as a checklist, not a plan. Each item is independently shippable."
Verified
routes/api/auth/unlock/+server.tscallsunlockMasterPassphrasedirectly with no attempt counter, no backoff, no lockout and no failure logging:The endpoint is in
PUBLIC_APIby necessity, and the instance is internet-facing ata.lck.shtoday — confirmed:GET https://a.lck.sh/api/statereturns200unauthenticated.Argon2id bounds the cost per guess, not the number of guesses — and the server pays that cost, so parallel attempts are simultaneously a guessing channel and a CPU denial-of-service. #110 makes the same point: "the same property that slows an attacker is also a DoS vector."
Work
Watch for
The counter must not itself become the DoS: an unbounded map keyed on attacker-controlled source IPs is a memory exhaustion primitive. Bound it.
Don't let the rate limiter run before cheap input validation — a 400 for a malformed body should not consume an attempt, or an attacker can lock the operator out for free.
Done when
Repeated wrong passphrases from one source back off and then lock out; the global counter trips independently of source; a correct passphrase during lockout is still refused; and each of those is a test that has been proven able to fail.
The per-IP key does not work as written — resolve this first
Nexus sits behind nginx at
a.lck.sh, and nothing in the repo configures client-IP resolution. Measured:…returns only the four
/api/agent/*callbacks. Those are fine as they stand: workers reach host-mode Nexus directly over thenexus-workersbridge, no proxy in the path, sogetClientAddress()is genuinely the worker's bridge IP — which is exactly whatip-guard.tsdepends on./api/auth/unlockis different. Reached through nginx,getClientAddress()returns nginx's own address, identical for every client on the internet. Two consequences, and they are both bad:So the per-IP half of this issue is not implementable until client-IP resolution is correct.
The fix has a trap of its own
adapter-node derives
getClientAddress()from theADDRESS_HEADERandXFF_DEPTHenvironment variables. SettingADDRESS_HEADER=x-forwarded-forwith a correctXFF_DEPTHfor this deployment makes it read the right hop.XFF_DEPTHis the load-bearing part.X-Forwarded-Foris client-appendable: anyone can sendX-Forwarded-For: 1.2.3.4and, with a naive left-most read, mint a fresh rate-limit key per request — defeating the limiter completely while making it look like it works. The depth must count back from the right by the number of proxies actually in front of Nexus, and it must match the real deployment rather than a guess.Also confirm nginx is setting the header at all, and that it overwrites rather than appends a client-supplied one.
Consequences for this issue
X-Forwarded-Forcannot mint a new key. Prove it fails before trusting it.XFF_DEPTHfor this deployment cannot be established with confidence, ship the global limiter alone and say so, rather than shipping a per-IP key that is silently the proxy's address.