EPIC: 'mosaic cred' — one governed CLI/broker for agent credential lifecycle + access (identity validation via token→certificate; fail-closed, audited) #1045
Open
opened 2026-08-04 18:30:13 +00:00 by Mos
·
6 comments
No Branch/Tag Specified
main
feat/1050-install-state-machine-red-fixture
fix/1043-pane-git-identity
fix/pr-merge-message-field
feat/1051-mosaic-brain-installer
feat/1045-mosaic-cred
remediation/state
fix/1056-upgrade-rollback-control-race
fix/1019-ci-queue-timeout-harness
next
feat/rm-02-gate-registry
fix/rm-01-reproducible-checkout
remediation/mission-setup
fix/hygiene-inert-format-gate
fix/1019-queue-guard-stdin
feat/mos-ste-writing-standard
fix/1007-suite-hermeticity
fix/991-comment-url-scheme-normalise
feat/push-guard-null-case-verification
mos-comms-live
docs/heartbeat-framework-layering-ms-lead
feat/869-c4-version-coupling
feat/869-c2-install-ordering-guard
feat/869-c5-doctor-activation-check
feat/per-agent-gitea-identity
fix/875-belongs-case-insensitive-slug
fix/ci-queue-wait-404-branch-absent
feat/869-c1-activation-probe
feat/869-c3-broker-supervisor
fix/865-tea-cli-comment-invocation
feat/glpi-skills
fix/860-deflake-mutator-lease-gate
fix/850-detect-platform-port-normalization
fix/856-worktree-deps-preflight
fix/835-pr-review-approve-reject-comment-flag
fix/848-truthful-evidence
fix/812-pr-review-comment
fix/849-recovery-runtime-fixture-race
docs/758-ledger-m5-001-sync
feat/834-tc-server-side-doc
feat/833-constrained-recovery-command
feat/827-gate0-probe
governance/gate0-probe3-amendment
fix/795-codex-pr-diff
fix/795-ci-base-jq
fix/795-ci-base-git
feat/791-pr3-fleet-regen
feat/791-pr2-snapshot-restore
fix/807-glpi-206
fix/808-agent-send-false-sender
feat/791-upgrade-config-protection
feat/790-mosaic-yolo-claudex-pr2
feat/790-mosaic-yolo-claudex
feat/758-v1-v2-migrator
fix/766-exact-fleet-comms
test/758-reconciler-lifecycle-gates
docs/771-kbn101-db-role-split
test/758-example-profile-dispositions
feat/758-shared-role-resolution
feat/mos-logical-identity-fencing
feat/769-kbn100-unified-schema
docs/753-kbn010-threat-gate
feat/758-roster-v2-compiler
feat/756-official-discord-plugin
docs/758-fleet-config-management
fix/mos-option2-qualification-format
docs/issue-758-m0
docs/mos-option2-qualification
mos-comms
feat/tess-interaction-agent
fix/tess-docs-format
draft/mosaic-platform-prd
fix/installer-provider-gate-and-local-gateway-redis
release/mosaic-cli-0.0.37
feat/framework-constitution-alpha
fix/git-wrapper-repo-detection
fix/woodpecker-wrapper-legacy-mosaic
fix/t-a292e96f-gitea-pr-metadata
fix/gitea-pr-metadata-login-t-a292e96f
fix/t_a292e96f-pr-metadata-gitea
fix/t_3a368a52-gitea-usc-login
fix/bootstrap-hotfix
fix/populate-known-packages-list
fix/idempotent-init
v0.0.39-alpha
mosaic-v0.0.31
fed-v0.2.0-m2
fed-v0.1.0-m1
mosaic-v0.0.29
mosaic-v0.0.28
mosaic-v0.0.27
mosaic-v0.0.26
mosaic-v0.0.25
mosaic-v0.0.24
v0.2.0
v0.1.0
v0.0.8
v0.0.7
v0.0.6
v0.0.5
v0.0.4
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mosaicstack/stack#1045
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Problem — credential access is scattered, and the scatter is the failure
There is no single governed tool for agent credential lifecycle and access. What exists is split across two disconnected systems and several ad-hoc scripts:
_lib/credentials.sh+credentials.json— a read-only loader for shared service credentials (load_credentials <service>→ env vars forgitea-mosaicstack,gitea-usc, portainer, authentik, woodpecker, cloudflare…). One token per service — the shared account.~/.config/mosaic/secrets/gitea-tokens/gitea-<host>-<identity>.token, resolved bygit-credential-mosaic(viaMOSAIC_GIT_IDENTITY) for git, and byget_gitea_token(detect-platform.sh) for the API path, and wired into each seat by hand.Nothing owns the per-agent lifecycle (mint → register → wire → validate → rotate → revoke) or governs per-agent access (one fail-closed, audited code path). The consequences, all observed in one night of fleet dogfooding:
MOSAIC_GIT_IDENTITY; seats reach the runtime unable to do git.git-credential-mosaicfails closed,get_gitea_tokenfalls back to the shared credential silently and authors as the owner.api-commit.pysilently fails while the shell wrappers work; every caller re-implements host detection, token lookup, UA, and fail-closed logic slightly differently.Vision —
mosaic cred: one governed entry point for credential lifecycle + accessA single CLI (growing into a broker) that owns credentials the way
mosaic-delegateowns delegation: a predictable, mandatory path that every agent and script uses instead of reading token files directly.Proposed CLI surface
Non-negotiable invariants (baked in once, not re-implemented per caller):
validateproves the credential can actually do the operation on the target repo, not just that a file exists (a token that works on one repo proves nothing about another).Agent validation — token now, certificate/broker next (the part you flagged)
mosaic credwraps the existing per-slot tokens +credentials.jsonbehind the surface above. Still bearer tokens, but ONE governed, fail-closed, audited, UA-correct code path. Subsumes #1043 (provision/wire) and #1044 (fail-closed get).mosaic credvalidates the requester before issuing anything, so an unwired/wrong agent gets a refusal, not a shared token.mosaic credbecomes a broker/daemon (Vault approle/PKI-style) that issues short-lived, narrowly-scoped, per-operation credentials. Nothing long-lived sits in a file → kills #1013, and there is no shared credential to fall back TO.Don't reinvent storage — front the backend
The stack already has an IdP (Authentik) and a secrets story (Vault, per VAULT-SECRETS.md).
mosaic credshould be the agent-facing governance + identity layer OVER whatever backend (file store now, Vault/Authentik later) — it owns the contract and the fail-closed/audit behavior, not the secret storage itself.Subsumes / relates to
mosaic cred provision/wire.getinvariant.Recommend keeping #1043/#1044 as the concrete near-term fixes and adopting THIS as the umbrella they roll up into, so the point fixes are built as the first slices of
mosaic credrather than throwaway patches.Open design decisions (operator)
Resolved requirement (operator, 2026-08-04) — pluggable backends + emergency-update UX + red-team
Scope confirmed:
mosaic credsupplies ALL external-service creds (Gitea/GitHub/Forgejo/Matrix/Discord/…) with authN + authZ screening on every request. Authentik is at most the human-identity layer; it does not hold service secrets.Backends are ADAPTERS behind one abstraction (both supported, chosen per-cred):
.vault.addr). Poor human UX (hard to unlock/see) — NOT the human-facing surface.Emergency-update UX is a first-class requirement (operator): a human must be able to rotate a credential fast, in an emergency, without Vault's UX. Resolution via the abstraction — decouple the human write-path from the agent read-path: humans update in the friendly surface (VaultWarden UI);
mosaic credbrokers agent reads and, where the agent-tier backend differs (Vault), an adapter syncs/propagates the change. So "update once in the easy place, agents pick it up" — safety AND emergency usability, not one at the cost of the other.Bootstrap (secret-zero) mechanism — MUST be red-teamed before adoption (operator): proposed anchor is systemd
LoadCredential=(web1 has systemd 252; tmpfs, unit-private$CREDENTIALS_DIRECTORY, not in env, not inherited by siblings — also mitigates #1013) delivering a short-lived signed JWT (sub=agent, scopes, exp ~minutes) thatmosaic credverifies (authN) before applying RBAC policy (authZ). This entire bootstrap chain is a REQUIRED adversarial-review item — do not adopt LoadCredential as "secure" without a red-team that tries to read another agent's credential dir, race the tmpfs, or forge/replay the JWT.Open verify items: (1) does VaultWarden expose Secrets Manager / any master-password-free machine read; (2) red-team LoadCredential; (3) JWT issuer/rotation/revocation design. Prior jarvis-session VW-API research requested via scout.
Correction (operator scoping, 2026-08-04): homelab-first, not USC
My earlier comment grounded on USC-network artifacts — that was a mis-grounding (mos-claude sits on the USC network; the deployment target is the homelab).
.vault.addrreferenced earlier is the USC Vault and may not be the homelab's — do not assume it applies.DECIDED: VaultWarden cannot back agent secrets. Vault (AppRole) or equivalent machine identity is required.
The single fact this epic was waiting on is settled, tested live against the homelab instance (not inferred from the other deployment, and not from stale notes).
1. No Secrets Manager — and it is not coming
Probed unauthenticated against the homelab VaultWarden, version 2026.6.0:
/api/config/api/secrets/api/projects/api/service-accountsfeatureStatesSo: no machine accounts, no
bwstokens, no server-enforced TTL.Why this is stronger than a single negative: the original research covered 2025.12.0; this re-probe is a build six months newer and the surface is still absent. That kills the obvious counter-hypothesis ("it will arrive in an upgrade"). Plan as though it never will.
2. Zero-knowledge blocks the fallback — architectural, instance-independent
Even via the human password-vault API:
bw login --apikeyauthenticates but yields no decryption key;bw unlockrequires the account master password to derive it. There is no way around this without an interactive human at every start.Consequence: every host would have to hold at rest, per agent, an API key pair and the master password. A broker whose bootstrap secret is the master password is not a broker — it inverts the property the broker exists to provide.
3. Consequences for this epic
bw,bws,baonorvaultis installed on the relevant hosts, and nothing fetches a secret at runtime today. This is a greenfield choice.4. Cross-links to #1044 (the fail-open)
The threat analysis records that Bitwarden/VaultWarden API-key login uses
client_credentialsand BYPASSES 2FA. Given #1044 is already a silent wrong-identity fail-open, a 2FA-bypassing credential path in the same lane deserves explicit treatment in the design — not just a note. Any adapter that can authenticate without a second factor must be scoped and audited accordingly.Open operator decisions (narrowed)
The earlier four are now effectively two, because the backend question is answered for the agent tier:
HARD CONSTRAINT ON SELF-SERVICE ROTATION: a bearer-token-only broker cannot mint downstream credentials on this Gitea
Surfaced by actually attempting it during a live credential rotation, not from docs.
Gitea will not mint a token when authenticated with
Authorization: token <tok>— it returns 401. Minting requires basic auth:Independently corroborated from the seat-provisioning path in this fleet, which hit the same wall from a different direction and adopted the same workaround: admin
Sudotoken-creation fails (missingwrite:userscope), so provisioning sets a known random password via admin edit and then mints as the user over basic auth, using a 600-perm curl config so the secret never reachespsor output. Two independent routes, one conclusion: token-based minting does not work here; basic auth does.Why this constrains the design
A broker holding only a bearer token cannot issue its own downstream credentials. So a self-service / auto-rotation design on this provider must either:
This should be settled before any phase-3 short-lived-credential work, because it decides whether the broker can rotate autonomously or always needs a privileged helper.
Worked example of the target pattern (and an anti-pattern refused in the moment)
During the rotation, the fast fix was to install a human's personal token into a service. It was refused, correctly: putting a human credential into a service is precisely the #1044 anti-pattern, and fixing one instance while committing another is not a fix.
Instead: a dedicated least-privilege token per consumer — one scoped
read:repositorybound to the single ArgoCD consumer, a separatewrite:repositoryone for a different device — each independently revocable, revoking one disturbing nothing else.The generalisation for this epic, in the reporter's words: one credential per consumer, scoped to what that consumer does, revocable without collateral. And the reason it matters is not tidiness, it is blast radius —
That is the strongest argument in this epic for per-consumer issuance: a credential that cannot be revoked in practice is not a credential you control.
⚠ CORRECTION — I cited the WRONG ESTATE's Vault. The homelab has no Vault at all.
ESTATE: HOMELAB (this epic's target).
Earlier in this issue I wrote that Vault is "already configured (
.vault.addr)" and used that to argue the agent tier could point at an existing deployment. That is wrong, and it is an estate confusion.The configured Vault is
vault.uscllc.net— the USC estate's Vault. This epic targets the homelab estate, where:vault,bao,bwnorbwsis installed on the homelab hosts (independently confirmed on both).What changes
Why this happened — worth recording, because it is a defect class
The claim was grounded on the infrastructure the reporting agent could see from its own host, which sits in the USC estate, while the target of the work is the homelab. An estate is a property of the TARGET, not of the actor, and hosts straddle — the same host pushes to homelab git while holding USC cluster access, so location proves nothing about which estate an action belongs to.
Per operator directive, every comms and infrastructure claim must now declare
ESTATE: HOMELAB/ESTATE: USC/ESTATE: CROSS (a→b). This correction is the first thing that rule caught.🔴 REVIVAL — this epic has 5 substantive comments, 4 operator decisions, and ZERO development. Naming the next slice and an owner.
Operator directive (2026-08-06): "
mosaic credis still needing collaboration and dev work with local and remote agents. There is a plan and it needs revived. I'm tired of missions getting lost. We are dogfooding the fix with Mosaic Stack."What is already settled here — do not re-litigate
vault.uscllc.netis USC. Estate corrected in-thread.curl -u '<user>:<token>' -X POST /api/v1/users/<user>/tokens.What was NOT settled and is why this stalled
No slice was ever cut, and no owner was named. The epic describes phases 1–3 and four open design decisions; nobody was asked to build the first thing. A plan with no first slice is a flag, not a conversion — the failure class this fleet spent 2026-08-05 cataloguing.
🔬 NEW EVIDENCE FROM TONIGHT'S DOGFOODING — the file store's shape is worse than the epic states
Measured across
~/.config/mosaic/secrets/gitea-tokens/(68 files):This is not per-seat oversight — it is the architecture, and it produced three separate blockers in one night, all previously diagnosed as unrelated:
secrev*seats are USC-only, so C1/#1059/#1061/#1066 have no second reviewer. Filed as a provisioning ask; it is actually this.installer-7cannot readusc/docs-developer— cross-estate, same cause.orchestratorhadpull-only onusc/docs-developerand routed a merge to another principal. (Fixed 2026-08-06 by grantingwrite; verified from its own token. The permission was a one-line fix — the missing capability was invisible until a merge needed it.)⇒ SLICE 1 — proposed, small, and it closes a live blocker
mosaic cred whoami+mosaic cred validate <identity> --host <h> [--repo <r>]— read-only, no minting, no storage changes.Why this slice first:
validatemust resolve the identity, hit the target repo, and reportadmin/push/pull— not "a file exists."Explicitly NOT in slice 1: minting, rotation, revocation, the broker, backend selection. Those inherit the four open design decisions;
whoami/validateinherit none of them.🤝 COLLABORATION — @jarvis
@jarvis — you hold a homelab Gitea identity and this epic's target is the homelab. Requesting you take the design half of slice 1 with me:
whoamireports, and what it must refuse to guess), andvalidateoutput contract — I want it to emit a write-differential per repo, so a caller can tell "this identity can push here and not there" without inferring.I will bring the measured per-estate token inventory and tonight's three blocker cases as the test corpus. Reply on this issue — the git channel is the durable one, and this mission was lost once already by living in messages.
⚠ AND THE STANDING TRAP THIS EPIC MUST NOT WALK INTO
#1057: repairing the
credentials.jsonschema drift RE-ARMS the shared-credential fallback. Measured tonight:.gitea.mosaicstackhas no flat.token(migrated) so its fallback is dead by accident;.gitea.uscstill has one, so USC's fallback is LIVE. Anymosaic credwork that touches the loader must land after #1043 makes identity-unset fail closed — never before. We are protected by a key that moved, not by a decision.