Reconciler & quota hardening for open registration (fallback, non-LFS caps, token scope, org/CI bypass) #125

Stängd
öppnade 2026-07-19 06:58:02 +00:00 av supernaut · 4 kommentarer
Ägare

Severity: HIGH — pre-onboarding infra audit (2026-07-19). Reconciler/quota hardening for open registration; groups several related findings.

All references are ansible/roles/reconciler/templates/gitborg-reconciler.sh.j2 unless noted.

  • H1 — permissive fallback. Unknown/unmatched users get lfs-basic (:249-254, else tgt="$BASIC"), whose shared base rule grants unlimited size:repos:all + size:assets:all (only LFS capped). A failed tier_participant assignment at signup (but created JIT account) silently yields unlimited non-LFS storage. → Change fallback to the zero-quota participant group; require positive tier membership before base.
  • H2 — no instance-wide non-LFS cap. Every non-participant tier has size:repos:all=-1, size:assets:all=-1 (:92-105). One basic user can fill the 60 GB volume with ordinary git push/packages → shared-volume outage. Only reactive DiskUsageCritical (90%) exists. → Introduce bounded per-tier size:repos:all/size:assets:all (mind Forgejo's most-permissive merge rule).
  • H3 — partial-scope token silently no-ops CI enforcement. set_repo_actions (:171-193) WARN-and-return 0 on non-200; a token missing read:user/write:repository means Actions default-deny is never enforced and the run still exits 0 → non-entitled users keep CI (real OpenStack VM cost) with no alert. → Verify live token scopes; make scope failure fatal or emit an alertable metric.
  • M4 — per-cycle / org Actions bypass. DEFAULT_REPO_UNITS only affects new repos; a user re-enabling the actions unit gets ~15 min of runner jobs per cycle. Org repos are never reconciled → permanent CI bypass. → Reconcile org repos; consider a sticky toggle.
  • M3 — no per-user repo/upload/file-size limits. app.ini.j2 [repository] has no MAX_CREATION_LIMIT, no [repository.upload] FILE_MAX_SIZE, no mirror caps. → Set them (compounds H2).
  • M8 — partial Kanidm read mis-tiers. A 200-with-empty-member (ACL-filtered) read (:137-147) downgrades entitled users / drops participants to BASIC (via H1) with no signal. → Add a sanity floor (abort if a should-be-nonempty group is empty); verify the SA token's read ACL (ADR-0017 open item).
**Severity: HIGH** — pre-onboarding infra audit (2026-07-19). Reconciler/quota hardening for open registration; groups several related findings. All references are `ansible/roles/reconciler/templates/gitborg-reconciler.sh.j2` unless noted. - **H1 — permissive fallback.** Unknown/unmatched users get `lfs-basic` (`:249-254`, `else tgt="$BASIC"`), whose shared `base` rule grants **unlimited** `size:repos:all` + `size:assets:all` (only LFS capped). A failed `tier_participant` assignment at signup (but created JIT account) silently yields unlimited non-LFS storage. → Change fallback to the **zero-quota `participant`** group; require positive tier membership before `base`. - **H2 — no instance-wide non-LFS cap.** Every non-participant tier has `size:repos:all=-1`, `size:assets:all=-1` (`:92-105`). One basic user can fill the 60 GB volume with ordinary `git push`/packages → shared-volume outage. Only reactive `DiskUsageCritical` (90%) exists. → Introduce bounded per-tier `size:repos:all`/`size:assets:all` (mind Forgejo's most-permissive merge rule). - **H3 — partial-scope token silently no-ops CI enforcement.** `set_repo_actions` (`:171-193`) WARN-and-`return 0` on non-200; a token missing `read:user`/`write:repository` means Actions default-deny is never enforced and the run still exits 0 → non-entitled users keep CI (real OpenStack VM cost) with no alert. → Verify live token scopes; make scope failure fatal or emit an alertable metric. - **M4 — per-cycle / org Actions bypass.** `DEFAULT_REPO_UNITS` only affects *new* repos; a user re-enabling the `actions` unit gets ~15 min of runner jobs per cycle. Org repos are never reconciled → **permanent** CI bypass. → Reconcile org repos; consider a sticky toggle. - **M3 — no per-user repo/upload/file-size limits.** `app.ini.j2 [repository]` has no `MAX_CREATION_LIMIT`, no `[repository.upload] FILE_MAX_SIZE`, no mirror caps. → Set them (compounds H2). - **M8 — partial Kanidm read mis-tiers.** A 200-with-empty-`member` (ACL-filtered) read (`:137-147`) downgrades entitled users / drops participants to BASIC (via H1) with no signal. → Add a sanity floor (abort if a should-be-nonempty group is empty); verify the SA token's read ACL (ADR-0017 open item).
Upphovsperson
Ägare

H1 + H2 implemented on feat/125-reconciler-quota-hardening (commit fce8f64; push/PR pending — SSH agent locked overnight). Rendered + bash -n verified; syntax-check + ansible-lint clean. Not applied — needs review + a dry-run pass on prod first (the timer applies within ~5 min of the role apply, so review reconciler_user_exempt before shipping).

  • H1: no tier signal → zero-quota participant (was lfs-basic = unlimited non-LFS). tier_basic is now read as the positive paid-seat signal. First-party identities (forgejo_users + service accounts + reconciler_user_exempt_extra, currently supernaut — ⚠️ confirm that's the only out-of-band admin) are exempted into the unlimited group, since fail-closed would otherwise zero-quota the operator. A WARN is logged on every fail-closed fallback, which also makes an ACL-filtered empty tier read (M8) visible.
  • H2: the shared unlimited base rule is gone (detached from every group + rule deleted). Each tier group now carries <group>-store = size:repos:all+size:assets:all capped at 10 GiB (per the pricing decision — 10 GiB shared, no repo-count cap; LFS add-ons bump only LFS). Subjects don't overlap with the LFS rule, so the most-permissive merge can't defeat either cap.
  • M3 note: MAX_CREATION_LIMIT deliberately NOT set — the pricing decision explicitly chose "no repo-count cap, bounded by storage quota". Upload/file-size caps remain open here.

Still open on this issue: H3 (token-scope no-op → fatal or alertable metric), M4 (per-cycle/org Actions bypass), M8 (sanity floor on group reads), remainder of M3 (upload caps).

**H1 + H2 implemented** on `feat/125-reconciler-quota-hardening` (commit `fce8f64`; push/PR pending — SSH agent locked overnight). Rendered + `bash -n` verified; syntax-check + ansible-lint clean. **Not applied** — needs review + a dry-run pass on prod first (the timer applies within ~5 min of the role apply, so review `reconciler_user_exempt` before shipping). - **H1:** no tier signal → **zero-quota `participant`** (was `lfs-basic` = unlimited non-LFS). `tier_basic` is now read as the positive paid-seat signal. First-party identities (forgejo_users + service accounts + `reconciler_user_exempt_extra`, currently `supernaut` — ⚠️ confirm that's the only out-of-band admin) are exempted into the unlimited group, since fail-closed would otherwise zero-quota the operator. A WARN is logged on every fail-closed fallback, which also makes an ACL-filtered empty tier read (M8) visible. - **H2:** the shared unlimited `base` rule is **gone** (detached from every group + rule deleted). Each tier group now carries `<group>-store` = `size:repos:all`+`size:assets:all` capped at **10 GiB** (per the pricing decision — 10 GiB shared, no repo-count cap; LFS add-ons bump only LFS). Subjects don't overlap with the LFS rule, so the most-permissive merge can't defeat either cap. - **M3 note:** `MAX_CREATION_LIMIT` deliberately NOT set — the pricing decision explicitly chose "no repo-count cap, bounded by storage quota". Upload/file-size caps remain open here. **Still open on this issue:** H3 (token-scope no-op → fatal or alertable metric), M4 (per-cycle/org Actions bypass), M8 (sanity floor on group reads), remainder of M3 (upload caps).
Upphovsperson
Ägare

H1+H2 applied to prod 2026-07-20 (PR #164, + follow-up PR sourcing the exempt list from vault). Reconciler ran clean (APPLY=true, complete, no errors):

  • Tier groups now carry the bounded <group>-store cap: lfs-basic/-pro/-xl all non-LFS 10737418240 (10 GiB). Legacy unlimited base rule deleted.
  • Exemptions correct: supernaut + all service accounts → org-unlimited; kofish → lfs-xl.
  • Fail-closed works: a tierless user → participant with the WARN line.

⚠️ The apply surfaced a pre-existing HIGH bug → filed #166: the reconciler reads DIRECT group members, but the taxonomy nests tiers into ent_* groups (transitive memberOf). So tier_pro users don't resolve (ent_lfs.member=[tier_pro], not persons) and now fail-closed to zero-quota participant — Pro seats get less than Basic. Observed: alexanderkjall (in tier_pro, renovate cap 100) → participant. Acute impact nil (owns no repos). Fix implemented on feat/166-nested-group-expansion (committed; push pending on SSH agent).

Remaining on this issue: H3, M4, M8 (M8 partly subsumed by #166).

**H1+H2 applied to prod 2026-07-20** (PR #164, + follow-up PR sourcing the exempt list from vault). Reconciler ran clean (`APPLY=true`, complete, no errors): - Tier groups now carry the bounded `<group>-store` cap: `lfs-basic/-pro/-xl` all `non-LFS 10737418240` (10 GiB). Legacy unlimited `base` rule deleted. - Exemptions correct: `supernaut` + all service accounts → `org-unlimited`; `kofish` → `lfs-xl`. - Fail-closed works: a tierless user → `participant` with the WARN line. ⚠️ **The apply surfaced a pre-existing HIGH bug → filed #166:** the reconciler reads DIRECT group members, but the taxonomy nests tiers into ent_* groups (transitive memberOf). So **tier_pro users don't resolve** (ent_lfs.member=[tier_pro], not persons) and now fail-closed to zero-quota `participant` — Pro seats get less than Basic. Observed: `alexanderkjall` (in tier_pro, renovate cap 100) → participant. Acute impact nil (owns no repos). Fix implemented on `feat/166-nested-group-expansion` (committed; push pending on SSH agent). Remaining on this issue: H3, M4, M8 (M8 partly subsumed by #166).
Upphovsperson
Ägare

Reconciled scope: H1/H2 applied (#164), M4 shipped (#169), M8 subsumed by #166. H3 (token-scope no-op) split out to #183. Remaining here: M3 — per-user upload / file-size caps ([repository.upload] FILE_MAX_SIZE, mirror caps); MAX_CREATION_LIMIT deliberately unset per the pricing decision (10 GiB shared, no repo-count cap).

Reconciled scope: H1/H2 applied (#164), M4 shipped (#169), M8 subsumed by #166. H3 (token-scope no-op) split out to #183. Remaining here: **M3** — per-user upload / file-size caps (`[repository.upload] FILE_MAX_SIZE`, mirror caps); `MAX_CREATION_LIMIT` deliberately unset per the pricing decision (10 GiB shared, no repo-count cap).
Upphovsperson
Ägare

Closing. Most of this was superseded by the ADR 0035 reconciler rewrite, which retired the bash script these line references point at.

  • H1, H2: fixed in the rewrite (the runbook records H2 as resolved).
  • M4: fixed in fbd3bb0 (org repos are reconciled).
  • M3: still missing in app.ini.j2. Filed as #495.
  • H3, M8: to be verified against the rewritten reconciler. Tracked with the reconciler.
Closing. Most of this was superseded by the ADR 0035 reconciler rewrite, which retired the bash script these line references point at. - H1, H2: fixed in the rewrite (the runbook records H2 as resolved). - M4: fixed in fbd3bb0 (org repos are reconciled). - M3: still missing in `app.ini.j2`. Filed as #495. - H3, M8: to be verified against the rewritten reconciler. Tracked with the reconciler.
Logga in för att delta i denna konversation.
Ingen milstolpe
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-infra#125
Ingen beskrivning angiven.