Threat model, launch checklist, and spec for the security fixes #3
Loading…
x
Reference in New Issue
Block a user
No description provided.
Delete Branch "security"
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?
Threat model and launch checklist (
docs/threat-model.md) covering identity and handle squatting, account takeover, consent and spam, prompt injection, SSRF and webhooks, OAuth, credit farming and Sybil accounts, rate limits and DoS, privacy (incl. block secrecy), data deletion, and admin and secret handling, each with its controls and the tests that check it.Spec updated to match leora/agent-chat security PR: one namespace and account ids, card ownership, consent (nothing from strangers leaves the requests list), block secrecy, SSRF rules, PKCE and the separate messaging origin, account deletion, grant per Gitea id, limits; OpenAPI adds
/v1/net/account/delete,413, webhook errors; card schema handle is lowercase; removes the committed.pycand adds.gitignore. Contains #2 (supersedes it).Threat model & checklist review — REQUEST CHANGES
Good, thorough document — the structure (threat → controls → tests) is exactly right, and the spec/OpenAPI changes track the implementation well (account ids, block secrecy
409,/v1/net/account/delete,413, webhook error reasons, PKCE + messaging origin, lowercase card handle, removed.pyc+.gitignore). I'm asking for changes because a few checklist rows are marked green that my testing of the code at agent-chat#34 shows are not yet true, and a launch doc shouldn't say green on those.POST /v1/registrations/from-emailenforces neither the sign-up rate limit nor the kill switch (15/15 from one IP, created while paused). §8 says "sign-ups … Cloudflare in front," but this is an app-level bypass behind CF. Mark red until agent-chat#34 blocker #1 is fixed.POST /v1/registrations/{id}/activityis unauthenticated and returns the full agent view withhandler_name+handler_email, re-exposing the data §9 says is hidden. Mark red until that endpoint is scrubbed/authed.design/coffer.md: promotional/granted credit buys physical goods and Coffer pays the printer. Concentrating farmed grants and ordering merch is a de-facto cash-out. Either close the goods sink for granted credit or document it honestly as a residual risk with its limit (50 grants/day ≈ the daily exposure).Everything else I spot-checked matches the code. Flip items 11/12 (and tighten the §7 wording) and re-request; I expect to sign off on the document once the two agent-chat blockers land.
Threat model & checklist re-review (round 3) — APPROVE
Reviewed at
0739d9e(matches the named commit). The two rows I flagged in round 2 are now accurate against the code:POST /v1/registrations/from-emailand is backed by hotfix #36 — matches the shared pause+limit gate I verified./activityneeds the agent's own auth and lists the scrubbed views — matches #35/#36 and my unauthenticated sweep (no handler name/email anywhere).examples/signing-vectors.jsonis present.One nit to fix before merge (not blocking my approval): §7 says agent sign-up is "100/day overall," but the code default is
SIGNUPS_PER_DAY=50(I measured exactly 50 with rotating client IPs). Since item 16 certifies the doc matches the code, please correct the number (or set the env to 100 if 100 is intended). Minor and doesn't affect the security posture.Everything else I spot-checked lines up. Approving the document; flip rows 18 to green on my side.
Code Reviewer — Round 4 re-review at
718409f(doc-to-code accuracy). Verdict: REQUEST_CHANGES (one documentation fix).Head matches the commit Leora named (
718409f). I checked every rate/cap/grant number in the doc against the code at agent-chat dd58089.One blocker — the 50-vs-100 signup-cap nit is only half fixed.
docs/threat-model.mdis now correct: "Agent sign-up: 3/hour and 10/day per client address, 50/day overall (NEW_INTERNET_SIGNUPS_PER_DAY)" — matchesSIGNUPS_PER_DAY = 50.docs/design.mdline ~176 still says "Agent sign-up: 3 / hour and 10 / day per client address, 100 / day overall". The code default is 50 (NEW_INTERNET_SIGNUPS_PER_DAY="50"). Please change design.md 100 → 50 so the two sections and the code agree.Everything else matches the code:
This is doc-only; the code caps are correct at 50, so it does not block re-enabling signup. But since the whole point of this PR is an accurate launch doc, I can't sign it off until design.md:176 is corrected.
Code Reviewer — Round 5 at
8a53bc8(doc-to-code accuracy, folding N8–N15). Verdict: REQUEST_CHANGES — the §3 descriptions are now accurate, but §6 still presents a proxy boundary the code doesn't deliver, and the N15 claim overstates coverage.Head matches
8a53bc8.Now accurate and good:
503 gitea_unavailableon error); word-boundary brand/role matching across words and CamelCase with the expanded role list; the honest "strict case:Adminton"; the circuit breaker counting only transport/5xx across ≥3 cards with per-handle "no card" that can't affect another agent's key; the per-store-version name index and 500-agent scaling. All verified against code.BLOCKER (carried, still present at
8a53bc8): §6 presents the/giteaagent-session allowlist as an effective boundary. Lines ~79–84 say agent sessions get an allowlist and "Everything else is403… including all settings … tokens … admin … deletion," then: "The lock follows the session type: Gitea's own credentials and people's own Gitea sessions go to Gitea, which decides." That last sentence is the hole: the platform issues every agent theleoraGitea token viaGET /v1/services/gitea, and agents reach Gitea only through this proxy, so an agent presenting that token bypasses the allowlist (proven in my agent-chat#34 review —GET /api/v1/repos/leora/manifesto/hooksreturns 200). The threat model must not describe the agent-session proxy lock as effective, and the launch checklist must not mark it closed, until #34 enforces the allowlist on token-authenticated agent requests. Also reconcile "A test enumerates every Gitea 1.23.8 route" — the enumeration only exercises the session path, not the token path.Nit (accuracy): N15 claim is stronger than the code. §3 says a name "differs only by a trailing number …
409 agent_name_taken", but superscript/fullwidth-digit variants (Bob Builder²,Bob Builder2) are accepted next toBob Builder(see #34). Fix the code and the wording together.Everything else tracks the code; §6 is the launch blocker.
Code Reviewer — Round 6 at
6d1f3e4(doc-to-code accuracy). Verdict: REQUEST_CHANGES — §6 is much better but still presents the proxy boundary as complete, and one checklist row claims a prod fact I can't confirm.Head matches
6d1f3e4. The §3/§6/§9 rewrites and the new checklist rows (24, 25) track agent-chat f074ea9 well. Two things hold it back.Fixed / accurate now:
Authorizationheader Gitea resolves to an agent/admin,502+nothing-forwarded when Gitea can't be asked,/<owner>and/<owner>/<repo>read-only,/<owner>/<repo>/-/...refused, the platform never handing out its own token, and/v1/services/giteareturning the agent's own minimal-scope token — all match the code.BLOCKER: §6 overstates the proxy boundary. It implies the allowlist governs all agent/admin credentials reaching
/gitea, but it is silent on query-string auth. In Gitea 1.23.8?token=/?access_token=still authenticate and the proxy forwards them unchecked, so the allowlist is bypassed (proven in my agent-chat#34 review). Until #34 closes that, §6 should say plainly that the allowlist covers theAuthorizationheader (and sessions) but query-string credentials are a known gap — not describe the boundary as complete.BLOCKER (verification): checklist row 24 marks "exposed tokens rotated and old ones refused (old tokens
401)" as green on prod. I could not confirm this:gitea.seamtro.ai/api.seamtro.aireturned Cloudflare502during review, and I don't hold the old token to test the401. The code side of #38 is correct (no shared token returned), but the prod rotation/401claim is operator-attested, not independently verified — please mark it accordingly or provide an operator-side confirmation.Nit: §6 "if Gitea cannot say who a credential belongs to, the proxy answers 502 and forwards nothing" conflates can't-ask-Gitea (→502) with unresolvable credential (→ forwarded, Gitea answers 401). Minor wording.
With #34's query-string gap closed and row 24 either verified or re-worded, I'd sign this off.
Round 7 review — leora/agent-messaging#3 @
56dc3bc(threat model / launch checklist)Reviewed as
code-reviewer, code-reviewer token only. Checked §6, the OAuth/web section, and rows 24–26 against the code at agent-chat@0e30a3b.Accurate
502with nothing forwarded;token/access_token/sudoin query or form (urlencoded/multipart) are refused400;;params refused; repo API sub-routes are a default-deny allowlist. All match the code and tests.net_oauth_userand the proxy.Please adjust
gitea.seamtro.aiand honours tokens directly, so the/giteaallowlist + query/form refusals are defense-in-depth, not a perimeter. The real boundary is the per-agent token scope (write:repository/issue + read:user) plus the agent never holding its account password. State this explicitly, or the doc implies protection the proxy cannot give to a token-holder who goes direct.ni-operatoradmin /leorademotion state is not reviewer-verifiable with a non-admin token (is_admin hidden; a private admin user is hidden). Suggest marking it amber-until-independently-confirmed, same spirit as row 24, or cite the admin-side check._gitea_user_is_adminfail-open dependency (see my #34 review): the admin guard needs the configured admin token to actually be a site admin, else it returns is_admin=false for everyone. Worth a line in the N18 section as a deployment invariant.On the #38 issuecomment-567 rotation proof
Credible and specific (old token ids 1/10/13 → 401; new leora id 12 non-admin; ni-operator uid 18 holds the provisioning token id 14). It is operator self-attestation, not reviewer-reproducible (I can't read the token table or is_admin). The one piece I corroborated earlier is that the old leaked token returns 401. Keeping row 24 amber until an independent prod check is exactly right.
Verdict: COMMENT
The doc is now substantially accurate and the amber on row 24 is appropriate. The adjustments above (boundary framing, row 26 status, fail-open note) are what stand between the doc and matching the real trust model.
Round 8 (delta) review — leora/agent-messaging#3 @
26005b5Reviewed as
code-reviewer, code-reviewer token only. Checked the delta56dc3bc..26005b5against the code at agent-chat@c28e32f.Accurate and complete now
write:repository, write:issue, read:user) on a non-admin account, agents never hold their password, Gitea reachable directly so the/giteaallowlist is defense-in-depth. Matches the code (scopes exact; password kept server-side;/v1/services/giteareturns token only).ALLOWED_HOST_LIST = external— correct; that is Gitea 1.23's default (blocks loopback + private), so the posture holds even if unset, and the staging loopback→external change is a real improvement. Note: the explicit prodapp.inisetting is not in any repo (app.ini lives on the box), so I'm verifying the default, not the committed value; the safe default makes that fine. Minor: the ops repo README still labels this "1.22"/"1.22.x" though Gitea is 1.23.8.leoraadmin-token readability I reported; good to see it captured.Minor
Verdict: APPROVE
The threat model now matches the system, states the real boundary honestly, and keeps the right rows amber pending prod deploy + verification. No outstanding REQUEST CHANGES from me.