Repository navigation
Hosting: RotateRegistryKey — instance keys rotated through the Hosting plugin, never by hand (first verb of the record-driven deployment process) #2802
Description
Tracking issue for the maintainer directive of 2026-08-30: a clean deployment process, administered through the Hosting plugin, with instance records in Systemorph/Memex — starting with the verb the day's incident demands, registry-key rotation.
Why now
Two live mwi_ instance keys (memex, memex-cloud) need rotating. Today that cannot be done cleanly: MeshWeaverInstanceService.ReissueKey exists and has no caller (no endpoint, no UI, no action), the key is stored as a literal in memex's helm values and as a cluster-only patch on memex-cloud (helm get values / helm get manifest: 0, live deployment: 1 — a helm upgrade would drop it), and the operator's hosting-kv-ensure marks PluginCatalog-RegistryToken as REQUIRED = "supplied out-of-band". Rotation by hand would be a workaround; the verb is the fix.
Measured state (2026-08-30)
| fact | value |
|---|---|
| Deployment records | committed in Systemorph/Memex mesh/Deployments/{memex,memex-cloud,client-b,client-i}.json (Memex #146, core #2686, Plugins #918 — all merged) |
| …in the registry mesh | zero — nodeType:Hosting/Deployment returns 0, no GitSync node references Systemorph/Memex; the Hosting plugin cannot act on records it cannot see |
| record secret model | both records still use the LEGACY secretMounts hand-made SPC; keyVaultSecrets (typed) is null/empty, so the registry token is declared nowhere |
vault Systemorph |
access-policy model (enableRbacAuthorization=false); the operator identity's Key Vault Secrets Officer role assignment was inert; policy get/set/list granted today to hosting-operator (oid bd994331…) and read back |
| operator ↔ Azure | Jobs run as SA hosting-operator with workload identity (azure.workload.identity/use), image azure-cli:2.77 |
| operator ↔ mesh | one-way: the mesh reads ::hosting:: marker lines out of the pod log (OperatorOutput, the audit line); the Job holds NO mesh credential |
The verb: InstanceActionKind.RotateRegistryKey
Designed so the raw key never leaves the operator job, and only its hash crosses back — the same channel the backup verifier and the audit already use.
- Operator script
hosting-kv-rotate(coredeploy/aks/operator/bin/, thehosting-kv-ensureidiom): generatemwi_+ base64url(32 random bytes) (InstanceKeysscheme),az keyvault secret set --vault <record.keyVault> --name <prefix>PluginCatalog-RegistryTokenfrom a variable that is never echoed,unset, thenhosting::say key_hash <sha256 hex>. Wait for the synced Secret to carry the new value (compare hashes, never values), then the existingRestart the portal+Wait for rolloutsteps, thenhosting-verify. - Control plane (Plugins
InstanceActionControlPlane.Track): when the streamed output carries::hosting:: key_hash=, adopt it before the job reaches the restart step — writeKeyHash/KeyIssuedAton the instance node and theMeshWeaverInstance/{hashPrefix}index — via a new core methodMeshWeaverInstanceService.AdoptKeyHash(instancePath, hash)(aReissueKeythat takes the hash instead of minting the key; reusesWriteIndex). The old index entry is deleted in the same write. - Plan (
InstanceActionPlan.Steps):RotateRegistryKey => RotateSteps(vault, prefix, ns); request validation joinsRoll/Restart(name-the-instance confirmation, no backup question);Describegains the verb. - Records: both records declare the token under typed
keyVaultSecrets(key: PluginCatalog__RegistryToken→ object<prefix>PluginCatalog-RegistryToken), soHelmValuesrenders the SPC and the env issecretKeyRef, retiring the literal and the cluster-only patch (Memex#148's most serious row).DeploymentContentgainsRegistryInstanceId(defaults to the deployment id). - Records reach the mesh:
Systemorph/Memexmesh/GitSynced into the registry's operational space, two-way (the control plane already assumes "the record space is two-way GitSynced").
Order, and what each step unblocks
- vault policy — done (this issue's date)
- core:
AdoptKeyHash+hosting-kv-rotate+RegistryInstanceIdplumbing - Plugins: the verb, plan, Track adoption, in-mesh tests
- Memex: records declare
keyVaultSecrets; GitSync so they exist in the mesh - run
RotateRegistryKeyformemexandmemex-cloud; verify by hash and by the portal authenticating — never by printing a value
Not in scope here
Everything else the directive covers (the standard baked chart, RAM/CPU knobs, BYO helm, ACA) is the wider program in memory; this issue is the first verb, chosen because it is forced.
Activity
rbuergi commentedon Aug 30, 2026
Progress
hosting-operator(get/set/list onSystemorph)IInstanceKeyRegistry+MeshWeaverInstanceService.AdoptKeyHash+hosting-kv-rotateInstanceKeyAdoptionTest2/2 on the real mesh; script shellcheck-cleanRotateRegistryKeyverb, plan, mid-run adoption, tests, DIfeat/hosting-rotate-registry-keypushed; PR opens once #2803 is on main (it compiles against core, which does not yet carry the contract)keyVaultSecrets; GitSync into the meshmemexandmemex-cloudTwo facts learned on the way that the design now rests on:
::hosting:: key_hash=line streams (Track), before the job's CSI-sync wait and restart. Ordering is pinned by a plan test.Systemorphuses access policies, not RBAC, so Secrets Officer granted nothing;hosting-kv-ensurecould never have satisfied its ownREQUIREDtoken on this vault. Worth a line in Memex'senvs.jsonor docs so the next environment does not rediscover it.Merge pull request #3015 from Systemorph/docs/program-state-of-record
rbuergi commentedon Sep 1, 2026
Closed by #3015 (merged as
8a31eb40), which converts this program to a committed state-of-record page.The PR body said "Closes #1707, #2193, #2550 and #2802" — GitHub only parses the keyword before the first number, so only #1707 auto-closed. Closing the rest by hand; the work is landed either way.
Where this now lives:
Both ship with the platform under
Doc/Architecture/, so the next session finds them without reading an issue thread.🚨 The remaining work is named in those pages, not lost. The sharpest items:
Modules:RequirePrebuiltis real, load-bearing, and set in ZERO config files — so "never fall back to compiling" is provided for and enforced nowhere.Carried out in the same PR: deleted the dead
CompilationLock(no caller, andasync/Task.Delay-based, which the house rules forbid); taught the operator gate the direction that rots (it could only fail when a script was deleted, never when one shipped unclaimed); and gavehosting-kv-rotatebehaviour tests including the one property whose failure is unrecoverable — it must never print the minted key, mutation-proven.test(#2802): the stand-in registry runs without awk, inside the opera…
fix(#2802): commit and revoke refuse while the Deployment sets the ke…
fix(#2802): adoption retires a staged key; tokens bind to the key tha…
docs(#2802): list the rotation page in the topic map; the new tests t…
fix(#2802): a key transition reads the owner, and an identical index …
rbuergi commentedon Oct 11, 2026
(re-posted 2026-10-11; originally by @rbuergi on 2026-08-30)
Step 4b done — the fleet Space exists in the registry mesh and is GitSynced from
Systemorph/MemexBefore:
nodeType:Hosting/Deployment→ 0 on the registry; no Space namedDeployments; no_GitSyncreferencing the config repo. The Hosting plugin could not act on records it could not see.Created on the registry (memex.meshweaver.cloud), via MCP:
then
git_hub_sync op=update space=Deployments(activityDeployments/_Activity/d44ca55a). After:Private-repo
updatewas already proven on this registry — everySystemorph/Educationcourse Space syncs from a private repo the same way — so no new credential was needed.Two things to note:
keyVaultSecretsdeclarations are in Memex fix(blazor): cure the render-storm circuit wedge (re-bind guard) + surface bind errors #151, still open, and reach the mesh on the nextupdateafter it merges. That is the intended order: nothing in the mesh may reference vault objects the vault does not hold yet.get) repeatedly answered "reached no verdict within 10s" during this work whilesearchanswered fine — the live face of OwnerDisposing NACK is dropped exactly when the parent is disposing hosted hubs — the caller hangs for its full budget #2778. It did not block the writes or the sync; it did block reading back the diff.Step 5 (running the verb) waits on core #2803 → Plugins verb PR → Memex #151.