feat(kanidm): detect OAuth2 scope-map drift #281

Sammanfogat
supernaut sammanfogade 1 incheckning från feat/275-oauth2-drift-gate in i main 2026-07-31 11:47:21 +00:00
Ägare

Part of #275 — the hardening that would have caught #278 shipping half-fixed.

The problem it closes

kanidm-provision applies scope maps additively with no removal logic (#279). So a group removed from a client's declaration stays live forever, and the converge still reports success. #278 added forgejo_admins to bitborg-web-dev, left forgejo_users in place, and the dev client stayed visible to every user — with a clean apply and a PR body claiming the opposite.

Scope maps decide who sees an application on their Kanidm landing page and which groups can obtain tokens for it. An additive-only mismatch is a visibility/authorisation defect, and nothing surfaced it.

Verified against production, both paths

Detection — reports the real leak, stays silent on the client that matches:

TASK [kanidm : Report OAuth2 scope-map drift (#275)]
skipping: [gitborg-prod] => (item=bitborg-web)
ok:       [gitborg-prod] => (item=bitborg-web-dev) =>
  "OAuth2 scope-map DRIFT on 'bitborg-web-dev':
   live=['forgejo_admins', 'forgejo_users'] declared=['forgejo_admins'].
   Extra live groups cannot be removed declaratively — kanidm-provision is additive for scope maps.
   Fix with: kanidm system oauth2 delete-scope-map bitborg-web-dev <group> --name idm_admin"

Enforcement — with -e kanidm_oauth2_drift_fail=true, exit 2:

[ERROR] OAuth2 scope-map drift on 'bitborg-web-dev': live=[…] declared=[…].
        Enforcement is on (kanidm_oauth2_drift_fail).

Why it warns rather than fails by default

The drift it detects is currently real and unfixed. A hard gate would abort every unrelated apply until someone runs delete-scope-map by hand — the same reasoning as the account-policy gate in #277: a gate that breaks unrelated applies is worse than no gate.

Recommended sequence: run the manual delete-scope-map for bitborg-web-dev/forgejo_users, confirm this task goes quiet, then set kanidm_oauth2_drift_fail: true so any future leak aborts instead of printing.

Properties

  • Read-only reconciler token — cannot mutate identity state.
  • check_mode: false, so a dry-run exercises the gate instead of skipping it.
  • Parses the live group@domain: {scopes} rendering by taking everything before the first @.
  • Reports declared-but-missing groups too, which would mean provisioning hasn't applied yet.

Not covered

Claim-map drift at group level inside a declared claim — relevant to #280, where ent_lfs/ent_lfs_large may survive. Same shape of problem; worth extending this gate once #280's behaviour is observed on a real apply.

Part of #275 — the hardening that would have caught #278 shipping half-fixed. ## The problem it closes kanidm-provision applies scope maps **additively** with no removal logic (#279). So a group removed from a client's declaration stays live forever, and the converge still reports success. #278 added `forgejo_admins` to `bitborg-web-dev`, left `forgejo_users` in place, and the dev client stayed visible to every user — with a clean apply and a PR body claiming the opposite. Scope maps decide who sees an application on their Kanidm landing page and which groups can obtain tokens for it. An additive-only mismatch is a visibility/authorisation defect, and nothing surfaced it. ## Verified against production, both paths Detection — reports the real leak, stays silent on the client that matches: ``` TASK [kanidm : Report OAuth2 scope-map drift (#275)] skipping: [gitborg-prod] => (item=bitborg-web) ok: [gitborg-prod] => (item=bitborg-web-dev) => "OAuth2 scope-map DRIFT on 'bitborg-web-dev': live=['forgejo_admins', 'forgejo_users'] declared=['forgejo_admins']. Extra live groups cannot be removed declaratively — kanidm-provision is additive for scope maps. Fix with: kanidm system oauth2 delete-scope-map bitborg-web-dev <group> --name idm_admin" ``` Enforcement — with `-e kanidm_oauth2_drift_fail=true`, exit 2: ``` [ERROR] OAuth2 scope-map drift on 'bitborg-web-dev': live=[…] declared=[…]. Enforcement is on (kanidm_oauth2_drift_fail). ``` ## Why it warns rather than fails by default The drift it detects is **currently real and unfixed**. A hard gate would abort every unrelated apply until someone runs `delete-scope-map` by hand — the same reasoning as the account-policy gate in #277: a gate that breaks unrelated applies is worse than no gate. **Recommended sequence:** run the manual `delete-scope-map` for `bitborg-web-dev`/`forgejo_users`, confirm this task goes quiet, then set `kanidm_oauth2_drift_fail: true` so any future leak aborts instead of printing. ## Properties - Read-only reconciler token — cannot mutate identity state. - `check_mode: false`, so a dry-run exercises the gate instead of skipping it. - Parses the live `group@domain: {scopes}` rendering by taking everything before the first `@`. - Reports declared-but-missing groups too, which would mean provisioning hasn't applied yet. ## Not covered Claim-map drift at **group** level inside a declared claim — relevant to #280, where `ent_lfs`/`ent_lfs_large` may survive. Same shape of problem; worth extending this gate once #280's behaviour is observed on a real apply.
supernaut lade till 1 incheckning 2026-07-31 11:15:26 +00:00
feat(kanidm): detect OAuth2 scope-map drift
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m32s
76297b9aae
Part of #275, and the hardening that would have caught #278 shipping half-fixed.

kanidm-provision applies scope maps ADDITIVELY with no removal logic, so a group
removed from a client's declaration stays live forever while the converge reports
success. #278 added forgejo_admins to gitborg-web-dev, left forgejo_users in
place, and the dev client stayed visible to every user on the service — with a
clean apply and a PR body claiming otherwise.

Scope maps decide who sees an application on their Kanidm landing page and which
groups can obtain tokens for it, so an additive-only mismatch is a
visibility/authorisation defect, not cosmetic. Nothing surfaced it.

Compares live scope-map groups against the declaration, per declared client, and
reports the difference. Verified against production: it reports the real
gitborg-web-dev leak (live=[forgejo_admins, forgejo_users] declared=
[forgejo_admins]) and stays silent on gitborg-web, which matches.

WARNS by default. The drift it detects is currently real and unfixed, so a hard
gate would abort every unrelated apply until someone runs delete-scope-map by
hand — the same "a gate that breaks unrelated applies is worse than no gate"
reasoning as the account-policy gate. kanidm_oauth2_drift_fail: true turns it into
an abort once live is clean; verified that path fails with the expected message.

Reads with the read-only reconciler token and runs under check_mode: false, so a
dry-run exercises it rather than skipping it.
supernaut tvångsskickade feat/275-oauth2-drift-gate från 76297b9aae
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m32s
till 6985c82a7d
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m33s
2026-07-31 11:39:40 +00:00
Jämför
supernaut tvångsskickade feat/275-oauth2-drift-gate från 6985c82a7d
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m33s
till 054a7ad1d3
Alla kontroller lyckades
ci / ci (pull_request) Successful in 1m41s
2026-07-31 11:44:30 +00:00
Jämför
supernaut sammanfogade incheckning e9cbad0b2a till main 2026-07-31 11:47:21 +00:00
supernaut tog bort grenen feat/275-oauth2-drift-gate 2026-07-31 11:47:21 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-07-31 11:47:21 +00:00
supernaut refererade denna ändringsförfrågan från en incheckning 2026-08-03 09:41:35 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
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!281
Ingen beskrivning angiven.