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.
| Workflow | Actions secret | Backing vault item (blumeops) | Verdict |
|---|---|---|---|
argocd-deploy | ARGOCD_AUTH_TOKEN | w3663ffn…/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-container | the CI zot push key (originally an Actions secret; now a job-time op read of blumeops-ci/ci-zot) | w3663ffn… CI zot push key field | migrated → the blumeops-ci CI push key item, since renamed ci-zot (eblume/blumeops#1039 series) |
deploy-fly | FLY_DEPLOY_TOKEN | on5slfay…/deploy-token | migrated → blumeops-ci/fly-deploy |
build-blumeops | MAIN_PUSH_TOKEN | blumeops-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.yaml | RELEASE_FORGE_TOKEN | horkos-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-drift | FORGE_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 |
| all | GITHUB_TOKEN | forge-injected | n/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 editinblumeops-citakes effect on the next run; noprovision-indriround-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 intoblumeops-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-runseed-secretstask. 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 andprovision-ringtailstaysclass: 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 item | field | copied from (blumeops) | consumed by |
|---|---|---|---|
ci-argocd | token | re-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-zot | api-key | w3663ffn… CI zot push key field | build-container.yaml (talos + horkos release.yaml moved to per-repo ZOT_PUSH_API_KEY keys, eblume/blumeops#1039 series) |
fly-deploy | token | on5slfay…/deploy-token | deploy-fly.yaml |
forge-main-push | token | blumeops-main-push-token/token | build-blumeops.yaml (consumer retired) |
forge-main-push | hephaestus-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-script | kubeconfig | — 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.
Related
- warrant-approval-gated-runs — the program
- request-a-privileged-run — the request loop these workflows serve
- heph
01KZ0G21JS…— the tracking task