feat: granular access tokens — scoped, least-privilege tokens for bots/CI/integrations #761

Open
opened 2026-07-12 20:01:46 +00:00 by jmiller · 1 comment
Owner

Summary

Add support for granular access tokens — tokens scoped to specific permissions (and ideally specific repos/orgs) instead of broad full-account access. Bots, CI, deploy automation, and MCP integrations should run with least privilege.

Motivation

The forge's automation increasingly relies on long-lived tokens:

  • moko-deploy / the Actions bot (push + force-push on dev/rc — see the org branch-protection standard)
  • MOKOGITEA_TOKEN (registry + API, org secret, used by every deploy/release workflow)
  • MCP integration tokens (mcp-mokogitea-api), GitHub-mirror tokens, package-publish tokens

Today these are coarse: a leaked/misused token can act as the full account across every repo. Granular tokens limit blast radius — a deploy bot that can only push to named branches, a read-only mirror token that can't write, an MCP token scoped to the operations it actually performs.

Scope (to design)

  • Verify current granularity — confirm what scoped-PAT support the fork exposes today (upstream Gitea has category scopes: read:/write: for repo, issue, package, admin, org, notification, user, etc.) in both the UI and the token-create API; expose the full scope surface if any is missing.
  • Per-repo / per-org restriction — consider limiting a token to named repositories/orgs (beyond Gitea's category scopes), since much automation is repo-specific.
  • Token metadata — description, expiry, last-used, scope list surfaced in UI + GET .../tokens API for auditing.
  • Migration — once granular tokens land, audit and re-issue the existing bot/CI/MCP tokens scoped-down to least privilege; rotate the ones exposed in plaintext (e.g. MOKOGITEA_TOKEN in compose).

Open questions

  1. Does upstream Gitea's scoped-PAT model cover the need, or do we need fork-specific per-repo/per-org restriction?
  2. UI + API surface for creating and managing scoped tokens (and viewing a token's effective scope).
  3. Backward-compat for existing full-access tokens during the transition.

Related: org branch-protection standard (bots/actions whitelist), plaintext MOKOGITEA_TOKEN rotation follow-up.

## Summary Add support for **granular access tokens** — tokens scoped to specific permissions (and ideally specific repos/orgs) instead of broad full-account access. Bots, CI, deploy automation, and MCP integrations should run with least privilege. ## Motivation The forge's automation increasingly relies on long-lived tokens: - `moko-deploy` / the Actions bot (push + force-push on dev/rc — see the org branch-protection standard) - `MOKOGITEA_TOKEN` (registry + API, org secret, used by every deploy/release workflow) - MCP integration tokens (mcp-mokogitea-api), GitHub-mirror tokens, package-publish tokens Today these are coarse: a leaked/misused token can act as the full account across every repo. Granular tokens limit blast radius — a deploy bot that can only push to named branches, a read-only mirror token that can't write, an MCP token scoped to the operations it actually performs. ## Scope (to design) - **Verify current granularity** — confirm what scoped-PAT support the fork exposes today (upstream Gitea has category scopes: `read:/write:` for repo, issue, package, admin, org, notification, user, etc.) in both the **UI** and the **token-create API**; expose the full scope surface if any is missing. - **Per-repo / per-org restriction** — consider limiting a token to named repositories/orgs (beyond Gitea's category scopes), since much automation is repo-specific. - **Token metadata** — description, expiry, last-used, scope list surfaced in UI + `GET .../tokens` API for auditing. - **Migration** — once granular tokens land, audit and re-issue the existing bot/CI/MCP tokens scoped-down to least privilege; rotate the ones exposed in plaintext (e.g. `MOKOGITEA_TOKEN` in compose). ## Open questions 1. Does upstream Gitea's scoped-PAT model cover the need, or do we need fork-specific per-repo/per-org restriction? 2. UI + API surface for creating and managing scoped tokens (and viewing a token's effective scope). 3. Backward-compat for existing full-access tokens during the transition. Related: org branch-protection standard (bots/actions whitelist), plaintext `MOKOGITEA_TOKEN` rotation follow-up.
Author
Owner

Investigated — sharpening the scope

Category-scoped tokens already exist (Gitea 1.26, models/auth/access_token_scope.go): read:/write: for repository, issue, package, organization, admin, user, notification, activitypub, misc + a fork-specific licensing category, plus all and public-only. So token granularity by operation category is done.

The real gap = resource-level (per-repo / per-org) restriction. Gitea scopes are category-wide: write:repository grants write to every repo the user can access — there is no way to bind a token to specific repositories or orgs. That is the least-privilege gap (e.g. a deploy bot that should push to exactly one repo, an MCP token limited to the repos it touches).

Refined scope

  1. Data model: add a per-token repository/org allowlist to AccessToken (+ migration). Empty = unrestricted (back-compat).
  2. Enforcement: at the auth choke point (git transport + API reqToken), deny if the target repo/org is not in the token's allowlist — combined with (not replacing) the existing category scopes. A token must pass BOTH its category scope AND its resource allowlist.
  3. Surface: UI (token-create form) + API (POST /users/{u}/tokens accepts a repo/org list; GET .../tokens returns it).
  4. Migration: once shipped, re-issue the bot/CI/MCP tokens scoped to their actual repos.

Category scopes stay as-is; resource restriction is orthogonal. So this issue is effectively "add resource-level access-token restriction." Suggest a first PR = data model + migration + enforcement (the security-critical core), with UI/API following.

## Investigated — sharpening the scope **Category-scoped tokens already exist** (Gitea 1.26, `models/auth/access_token_scope.go`): `read:`/`write:` for `repository`, `issue`, `package`, `organization`, `admin`, `user`, `notification`, `activitypub`, `misc` + a fork-specific **`licensing`** category, plus `all` and `public-only`. So token granularity by **operation category** is done. **The real gap = resource-level (per-repo / per-org) restriction.** Gitea scopes are category-wide: `write:repository` grants write to *every* repo the user can access — there is no way to bind a token to specific repositories or orgs. That is the least-privilege gap (e.g. a deploy bot that should push to exactly one repo, an MCP token limited to the repos it touches). ### Refined scope 1. **Data model:** add a per-token repository/org allowlist to `AccessToken` (+ migration). Empty = unrestricted (back-compat). 2. **Enforcement:** at the auth choke point (git transport + API `reqToken`), deny if the target repo/org is not in the token's allowlist — combined with (not replacing) the existing category scopes. A token must pass BOTH its category scope AND its resource allowlist. 3. **Surface:** UI (token-create form) + API (`POST /users/{u}/tokens` accepts a repo/org list; `GET .../tokens` returns it). 4. **Migration:** once shipped, re-issue the bot/CI/MCP tokens scoped to their actual repos. Category scopes stay as-is; resource restriction is orthogonal. So this issue is effectively **"add resource-level access-token restriction."** Suggest a first PR = data model + migration + enforcement (the security-critical core), with UI/API following.
Sign in to join this conversation.