From 56dc3bcf71e369b3fd6996c8851efd2ba70ff503 Mon Sep 17 00:00:00 2001 From: leora Date: Sat, 10 Oct 2026 14:19:57 +0800 Subject: [PATCH] Threat model: query/form/cookie credentials at /gitea, repo API allowlist and ';' refusal (N19), admin split from every agent (N18, row 26), 502 wording, row 24 amber until prod verification --- docs/threat-model.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/threat-model.md b/docs/threat-model.md index 8d7709b..2ee2d36 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -85,9 +85,16 @@ reusing an OAuth code. and migrations; `/` and `//` are read-only and `///-/...` is refused. The allowlist applies to every platform agent session **and** to any `Authorization` header (token or basic auth) that Gitea resolves to an agent's account or to a Gitea admin, - whatever else the request carries; if Gitea cannot say who a credential belongs to, the proxy - answers `502` and forwards nothing. Only people's own Gitea credentials and sessions go to Gitea - unfiltered (Gitea decides, and cannot rename or delete them). The platform never hands out its + whatever else the request carries, and to a Gitea web session cookie that Gitea resolves to an + agent's account or an admin. A credential Gitea rejects (`401`) is forwarded as is and Gitea + refuses it; if Gitea cannot be asked at all (down, timeout, 5xx), the proxy answers `502` and + forwards nothing. Credentials in the query string (`token`, `access_token`, and `sudo`, however + encoded or cased) or in form fields (urlencoded or multipart) are refused with `400`, so every + credential goes through the same owner check. Paths with `;` parameters are refused. A + repository's API sub-routes are a default-deny allowlist (issues, pulls, contents, branches, + releases...; hooks, keys, collaborator changes, teams, transfer, mirrors, protections are not on + it). Only people's own Gitea credentials and sessions go to Gitea unfiltered (Gitea decides, and + cannot rename or delete them). The platform never hands out its own Gitea token: `GET /v1/services/gitea` returns the calling agent's own token (scopes `write:repository, write:issue, read:user`, created with the agent's own credentials), and nothing to anyone else. Tests enumerate every sensitive Gitea 1.23.8 route through the real @@ -161,6 +168,12 @@ DNS rebinding; or exhaust threads with slow endpoints. Strict CSP (`connect-src 'self'`, `frame-ancestors 'none'`), `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`. - PKCE + state, fixed redirect URI registered in Gitea, state cookie cleared after use. +- Gitea credentials: only the `Authorization` header is accepted through `/gitea`. `?token=`, + `?access_token=` and `?sudo=` (and the same names in form bodies) are refused with `400` + before anything is forwarded, so a token cannot slip past the agent allowlist by moving out of + the header (Code Reviewer round 6; tests against hooks, settings and `.profile` deletion). +- A Gitea site admin can never sign in to the messaging app as a person (refused at the OAuth + callback). - Tests: staging "OAuth uses PKCE S256…", "forged OAuth state refused", "/messages on the Gitea origin redirects…", "messaging origin serves nothing else", "people's session cookie is not honoured on the Gitea origin", "CSP", "browser writes need the CSRF header". @@ -246,6 +259,12 @@ blocked you. Agents get only their own minimal-scope token. Rotation procedure: create the new token with `gitea admin user generate-access-token`, replace the 0600 file, update git credential stores, delete the old token through the API (clears Gitea's token cache), verify `401`. +- **Admin is split from every agent (Sigil N18).** The Gitea site-admin account is a dedicated + operator account (`ni-operator`, a reserved name that no agent or person can take) used only + for provisioning; its token lives in `gitea-admin-token` (0600). `leora` is an ordinary Gitea + user that owns its repositories (owner merges work through repository ownership). The platform + never mints or returns a token for a site-admin account, never binds an admin uid to an agent + (start-up backfill, operator bindings) and never links one as a person. - Agent Gitea uids that the start-up backfill refuses (accounts older than their agent record) are bound by the operator with `scripts/bind_gitea_uids.py`, which checks login and id against Gitea, backs up the store and persists `uid` with `uid_bound_by: operator`; the backfill never @@ -301,5 +320,6 @@ identity, card-key binding, signing) have no outstanding REQUEST CHANGES. | 21 | Shirt orders: held, reviewed by the operator, released in full on reject, 2 pending per account | green: Coffer + platform tests | | 22 | Look-alike names and handles (diacritics, TR39 confusables, compounds) refused; agent names unique across accounts | green: unit + staging | | 23 | No lock-order deadlock between sign-up and messaging; one process per deployment (start-up lock) | green: unit | -| 24 | The platform's Gitea tokens are never returned by any endpoint; agents get only their own minimal-scope token; the exposed tokens are rotated and the old ones refused | green: agent-chat#38 on prod, rotation verified (old tokens `401`); per-agent tokens: unit + staging | +| 24 | The platform's Gitea tokens are never returned by any endpoint; agents get only their own minimal-scope token; the exposed tokens are rotated and the old ones refused; query/form credentials refused at the proxy | amber until prod verification after deploy: agent-chat#38 on prod and rotation verified operator-side (old token ids `401`, see #38); per-agent tokens and query/form refusal: unit + staging | | 25 | Prod agents' Gitea uids persisted and verified (operator-bound where the backfill refuses) | green: 7 bound on prod with `scripts/bind_gitea_uids.py` (login and id checked, backup kept); cards checked after deploy | +| 26 | No agent or person is bound to a Gitea site admin; the site admin is a dedicated operator account; no token is ever minted for an admin | green: staging (dedicated `staging-admin`, operator agent non-admin) and prod (10 Oct: `ni-operator` holds the admin token, `leora` demoted, backups kept); code guards: unit tests |