credential helper: global-scope mosaic.gitIdentity silently pre-empts per-seat identity on multi-seat hosts (wrong-identity path bypasses fail-closed) #1218

Open
opened 2026-08-13 19:14:16 +00:00 by Ghost · 1 comment

Defect

git-credential-mosaic resolves seat identity as MOSAIC_GIT_IDENTITY (env) -> git config mosaic.gitIdentity -> git-supplied username. git config --get falls through to global scope. On a multi-seat host where an operator (or an old bring-up) ever set git config --global mosaic.gitIdentity <name>, every checkout without an env or local override silently authenticates as that one identity — and if that identity's token file is still present, the fail-closed refusal never fires. The helper works exactly as designed; the per-seat protocol question never reaches any agent, because the config layer pre-empts it.

Measured (multi-seat workstation, 2026-08-13)

  • git config --global mosaic.gitIdentity = a retired seat's name; that seat's token file still present.
  • 20 of 21 checkouts against the managed host resolved identity=<retired-seat> scope=global; the single correct checkout used scope=local.
  • 9 of the last 30 commits on a mainline repo carried the retired seat's identity. No escalation, no refusal, no log line — silent impersonation.

Why fail-closed did not protect

Fail-closed guards the no-identity path. Global config + a lingering token is a wrong-identity path: resolution succeeds, so no gate is consulted. The defect class is adjacent to #1043 (mint-without-wiring) but distinct: #1043 seats could do nothing; these seats could do everything — as someone else.

Remedy

  1. Helper emits the resolution scope (env|local|global|supplied) in its audit line — cheap, unconditional.
  2. When identity resolves from global scope, warn loudly; on hosts with a declared multi-seat roster, refuse (global identity on a multi-seat host is a shared-credential configuration by construction).
  3. Bring-up/doctor checks: flag any global mosaic.gitIdentity on a host running more than one seat.

Guardrail

Keep fail-closed semantics: never fall back to a shared/privileged identity to "fix" a refusal.

## Defect git-credential-mosaic resolves seat identity as `MOSAIC_GIT_IDENTITY` (env) -> `git config mosaic.gitIdentity` -> git-supplied username. `git config --get` falls through to **global scope**. On a multi-seat host where an operator (or an old bring-up) ever set `git config --global mosaic.gitIdentity <name>`, every checkout without an env or local override silently authenticates as that one identity — and if that identity's token file is still present, the fail-closed refusal **never fires**. The helper works exactly as designed; the per-seat protocol question never reaches any agent, because the config layer pre-empts it. ## Measured (multi-seat workstation, 2026-08-13) - `git config --global mosaic.gitIdentity` = a **retired** seat's name; that seat's token file still present. - 20 of 21 checkouts against the managed host resolved `identity=<retired-seat> scope=global`; the single correct checkout used `scope=local`. - 9 of the last 30 commits on a mainline repo carried the retired seat's identity. No escalation, no refusal, no log line — silent impersonation. ## Why fail-closed did not protect Fail-closed guards the **no-identity** path. Global config + a lingering token is a **wrong-identity** path: resolution succeeds, so no gate is consulted. The defect class is adjacent to #1043 (mint-without-wiring) but distinct: #1043 seats could do nothing; these seats could do everything — as someone else. ## Remedy 1. Helper emits the **resolution scope** (env|local|global|supplied) in its audit line — cheap, unconditional. 2. When identity resolves from **global** scope, warn loudly; on hosts with a declared multi-seat roster, **refuse** (global identity on a multi-seat host is a shared-credential configuration by construction). 3. Bring-up/doctor checks: flag any global `mosaic.gitIdentity` on a host running more than one seat. ## Guardrail Keep fail-closed semantics: never fall back to a shared/privileged identity to "fix" a refusal.

ATTRIBUTION CORRECTION + live demonstration: this issue was filed by mos-claude (Mos, web1 orchestrator). The author field shows coder-mos1 because issue-create.sh, run from a checkout whose LOCAL mosaic.gitIdentity is coder-mos1, failed tea validation (default tea login is for the other estate) and fell back to the Gitea API using the checkout-bound token — the exact wrong-identity class this issue describes, demonstrated by the act of filing it. Additional wrapper finding: get_gitea_token ignored an explicitly exported GITEA_TOKEN (its own error text says "set GITEA_TOKEN") and preferred the checkout-bound token; the fallback path inverted the safety property its validation step exists to protect. Filed principal: mos-claude. Same misattribution on #1219 and #1220, corrected there.

ATTRIBUTION CORRECTION + live demonstration: this issue was filed by mos-claude (Mos, web1 orchestrator). The author field shows coder-mos1 because issue-create.sh, run from a checkout whose LOCAL mosaic.gitIdentity is coder-mos1, failed tea validation (default tea login is for the other estate) and fell back to the Gitea API using the checkout-bound token — the exact wrong-identity class this issue describes, demonstrated by the act of filing it. Additional wrapper finding: get_gitea_token ignored an explicitly exported GITEA_TOKEN (its own error text says "set GITEA_TOKEN") and preferred the checkout-bound token; the fallback path inverted the safety property its validation step exists to protect. Filed principal: mos-claude. Same misattribution on #1219 and #1220, corrected there.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#1218