Blumeops-CI Item Migration

The audit and migration plan for the blumeops-ci vault tier (warrant-approval-gated-runs Phase 2; heph 01KZ0G21JS…). The vault exists and BLUMEOPS_CI_OP_TOKEN is provisioned; this doc decides what moves in and — more importantly — what never does.

Status (2026-08-22): executed. All four migration steps landed in one PR. Later the same day, FORGE_ADMIN_TOKEN was retired (heph 01KZ5ESS2G…): horkos-forge-drift and agent-repo-access now use FORGE_REPO_WRITE_TOKEN, an eblume PAT scoped to write:repository,read:user (item forge-repo-write-token) — collaborator ops turned out to need only repo-admin permission plus the repository token scope (plus read:user for the drift job’s site-admin check), not the admin PAT. It stays an Actions secret for now; moving it into blumeops-ci is the open follow-up below.

The audit: who consumes what (2026-08-22, from main)

The 2026-08-03 audit predates the horkos extraction; talos and horkos release CI now consume ZOT_PUSH_API_KEY and RELEASE_FORGE_TOKEN too.

WorkflowActions secretBacking vault item (blumeops)Verdict
argocd-deployARGOCD_AUTH_TOKENw3663ffn…/argocd_token (the pre-rename ArgoCD account, get/sync/update)migrated (pilot) → a blumeops-ci item, since re-minted as blumeops-ci/ci-argocd for the ci-argocd account (eblume/blumeops#1163 series)
build-containerthe CI zot push key (originally an Actions secret; now a job-time op read of blumeops-ci/ci-zot)w3663ffn… CI zot push key fieldmigrated → the blumeops-ci CI push key item, since renamed ci-zot (eblume/blumeops#1039 series)
deploy-flyFLY_DEPLOY_TOKENon5slfay…/deploy-tokenmigrated → blumeops-ci/fly-deploy
build-blumeopsMAIN_PUSH_TOKENblumeops-main-push-token/token (eblume PAT, write:repository)migrated, eyes open — it pushed protected main → blumeops-ci/forge-main-push; now retired (eblume/blumeops#1628)
talos + horkos release.yamlRELEASE_FORGE_TOKENhorkos-forge-token/token (horkos-forge PAT, write on blumeops only — push non-protected branches, open PRs, and dispatch workflows (the permission workflow_dispatch requires); cannot merge — main is push+merge whitelisted to eblume)migrated 2026-08-26 → blumeops-ci/horkos-dispatch (eblume/talos#55, eblume/horkos#9). The rotation friction showed up: the run-script executor needed the same PAT CI-readable, which would have made three independently-rotating copies. Now two, reconciled by horkos-forge-provision.
agent-repo-access, horkos-forge-driftFORGE_REPO_WRITE_TOKEN (was FORGE_ADMIN_TOKEN)forge-repo-write-token/token (eblume PAT, write:repository,read:user; was w3663ffn…/api-token, the admin PAT)narrowed 2026-08-22 — no longer the admin PAT; migration into blumeops-ci can now proceed as a follow-up
allGITHUB_TOKENforge-injectedn/a

The one-CI-trust-tier decision (2026-08-21)

All CI in eblume/* repos is one trust tier — no per-repo stratification. Sharing BLUMEOPS_CI_OP_TOKEN (full blumeops-ci vault read) with talos/horkos release CI is by-design: it is CI reading the vault, not the services themselves, and workflow definitions reach main only through a human merge (bots can’t merge). The rule is broader: write on a repo == read of its Actions secrets; cross-repo release writes happen via horkos on a merge boundary (webhook), and cross-system writes go through a warrant. Consciously accepted: the tier transitively includes push-to-blumeops-main (auto-sync deploys + policy/workflow edits); the forge-main-push item’s consumers are now retired (eblume/blumeops#1628) and revoking its PATs is a human step there, not a blocker. Red-teaming this model is its own task (heph 01M0MX04Y8…).

What migration actually means

Not “copy items to another vault” — a consumption change: privileged workflows stop receiving per-secret Actions secrets and instead op read op://blumeops-ci/<item>/<field> at job time using BLUMEOPS_CI_OP_TOKEN (the only Actions secret they keep needing).

Won:

  • Rotation without provisioning — op item edit in blumeops-ci takes effect on the next run; no provision-indri round-trip, no more minikube-era-token rot (the run-703 failure mode).
  • Per-item audit — 1Password access logs show which run’s token read which item, per invariant 5.
  • Shrinking Actions-secret surface — repo-wide-readable secrets drop to one revocable token.
  • Untangling w3663ffn… — the migrated copies become per-purpose items, which is the fix for the multi-field-item overwrite hazard noted on the heph task.

Costs: runners need the op CLI (_1password-cli in the priv and nix-container-builder runners’ hostPackages on ringtail; homebrew-installed on indri), and jobs make a network call to 1Password at runtime. With OP_SERVICE_ACCOUNT_TOKEN set, op ignores the desktop-app integration, so the host-mode indri runner never trips a biometric prompt.

What never migrates: the provision-* verdict

Superseded (2026-10-13) — the decomposition path, not the verdict. The “each with its own deliberate item copy” plan below — seeding narrow per-role secrets into blumeops-ci — is superseded by the seed-file pattern (seed-secrets, eblume/blumeops#1524): no provision secret moves into blumeops-ci. The blumeops vault stays human-only, and the nix apply path (nixos-rebuild / darwin-rebuild) reads no vault at all — secrets are host-resident files the nix config references by path, seated by the human-run seed-secrets task. A decomposed role needs no item copy at all because its apply path is vault-free — the rebuild paths are auto-applied on merge (class: auto, auto-apply), while the full ansible plays stay non-requestable: indri’s full provision remains a human window and provision-ringtail stays class: deny, the indri play still reading the breadth of the vault and the ringtail play the two 1Password Connect fetches until those move.

provision-indri pre_tasks read the breadth of the blumeops vault — borgmatic/borgbase keys, every forgejo internal secret, OIDC client secrets for zot/jellyfin/…, the Gandi PAT; provision-ringtail pre_tasks read a narrower set — the runner registration tokens, the agents SA credential, and the two 1Password Connect fetches — until #1524 PR 2 retired the on-host seeding, leaving only those two fetches (the open follow-on in that issue). Migrating that set into a CI-readable vault would recreate the keys to the kingdom behind BLUMEOPS_CI_OP_TOKEN — the exact thing the vault split exists to prevent.

Verdict: wholesale provision-* stays non-requestable (warrant-policy.yaml) indefinitely — the full plays remain human-run or class: deny while their pre_tasks read the vault. The path to agent-requestable provisioning is decomposition: narrow, per-role actions (provision-indri --tags forgejo_runner needs only the runner identity items) added one at a time, each with its own policy entry — and since the supersession above, a decomposed role earns that entry by a vault-free apply path, not by a deliberate item copy.

The migration (executed 2026-08-22, one PR + human item-copies)

The item map — each a value-copy from the blumeops vault (same credential, per-purpose item; rotation to fresh values is the easy follow-up now that it needs no provisioning):

blumeops-ci itemfieldcopied from (blumeops)consumed by
ci-argocdtokenre-minted 2026-09-21 for the ci-argocd ArgoCD account (the pre-rename row copied w3663ffn…/argocd_token; retired by the eblume/blumeops#1163 rename)argocd-deploy.yaml, argocd-sync-apps.yaml
ci-zotapi-keyw3663ffn… CI zot push key fieldbuild-container.yaml (talos + horkos release.yaml moved to per-repo ZOT_PUSH_API_KEY keys, eblume/blumeops#1039 series)
fly-deploytokenon5slfay…/deploy-tokendeploy-fly.yaml
forge-main-pushtokenblumeops-main-push-token/tokenbuild-blumeops.yaml (consumer retired)
forge-main-pushhephaestus-token— minted hephaestus-scoped (write:repository, repo-restricted to eblume/hephaestus; the token field can’t write there)hephaestus release.yaml (declared as MAIN_PUSH_TOKEN) — consumer retired
k3s-run-scriptkubeconfig— minted from the live cluster (bound 12-month token for the horkos/run-script ServiceAccount, loopback-only server; no blumeops source)run-script.yaml cluster chores

FORGE_REPO_WRITE_TOKEN: not yet migrated to blumeops-ci — but the prerequisite is done (heph 01KZ5ESS2G…, 2026-08-22): it is now the write:repository,read:user-scoped forge-repo-write-token PAT, not the admin PAT. Moving it to a job-time op read like the rows above is the open follow-up.

Rotation after migration: the blumeops-vault originals stay the human-tier master copies (mise run fly-deploy still reads the fly one; the rest are provenance). Rotating a credential now means updating both copies — the service/master side as before, plus op item edit on the blumeops-ci item, which takes effect on the next CI run with no provisioning. Keep item titles unique in blumeops-ci: op read resolves by name, and a duplicate title breaks every consumer of that item (a duplicate-title incident on the pre-rename dispatch-identity item demonstrated this live).

Rollout order after the PR merges: run mise run provision-ringtail (puts op on both ringtail runners), then mise run forge-reconcile (its values half syncs BLUMEOPS_CI_OP_TOKEN to talos/horkos and no longer syncs the migrated secrets), verify one dispatch per workflow. The stale pre-migration secrets (ARGOCD_AUTH_TOKEN, ZOT_CI_API_KEY, FLY_DEPLOY_TOKEN, MAIN_PUSH_TOKEN) need no manual cleanup: the values half is authoritative — it PUTs declared secrets, DELETEs undeclared ones, and forge-reconcile --check reports name-level drift before anything is touched — so the deletion happens on the next human mise run forge-reconcile.