The live-only config audit — nothing lives only on the cluster

The rule this machinery enforces: nothing lives only on the cluster. Every value is a typed field on the Hosting/Deployment record, rendered by the chart (HelmValues); secrets are Key Vault NAMES on the record, never values anywhere. Anything else — a kubectl set env, a live-edited ConfigMap key, a hand-applied CronJob — is configuration only the cluster knows about, and it does not fail a deploy: helm upgrade preserves a live-edited ConfigMap key and never touches an object it does not own. It fails later, as an outage. On 2026-08-30 the memex portal crashed at boot because its Email section existed TWICE — the chart's ConfigMap defaults and a cluster-only secret patched onto the Deployment as env — and .NET picks the winner at random per pod start. The audit run by hand that morning found 25 env patches, 10 live-edited ConfigMap keys, sidecar containers and a volume on one namespace alone, plus a plain-text registry token in the pod spec. That measured inventory is Systemorph/Memex#148; this page is the machinery that keeps it measured.

Run one

Create a Hosting/InstanceAction with requestedAction: Audit. It is read-only: no backup question, no confirmation, nothing disturbed — safe to run against any instance at any time.

{ "id": "audit-memex", "namespace": "Ops/Actions", "name": "Audit memex",
  "nodeType": "Hosting/InstanceAction",
  "content": { "$type": "InstanceActionContent",
    "deployment": "memex", "requestedAction": "Audit" } }

The run launches the operator's hosting-audit (one step), which takes the truth from helm get manifest + helm get hooks on the instance's release and diffs the live namespace against it. The run node shows the verdict (auditVerdict) and links the report (auditPath).

What is detected

All of it by name, never by value — some of the things being hunted ARE secrets, so a report that quoted a value would be the finding one level up.

Category What it means
env live-only / manifest-only inline env on the portal container that no manifest renders (kubectl set env patches) — and, from the other side, env the chart renders that the pod lacks
envFrom / volumes / mounts / containers pod-spec patches: extra secret or ConfigMap sources, hand-mounted volumes, sidecar containers
pod-spec fields replicas (unless an autoscaler owns it), nodeSelector, tolerations, affinity, serviceAccountName, termination grace, the portal container's resources, lifecycle and startup-probe budget
ConfigMap keys keys of the chart's ConfigMaps that exist only live, only in the manifest, or whose value differs (compared on the cluster; only the key name leaves it)
unmanaged objects live objects the chart does not own, grouped by kind — hand-applied CronJobs, Deployments, Ingresses, SecretProviderClasses, PVCs. Owned objects, helm release records, SA tokens, CSI-synced secrets and Kubernetes' own furniture are excluded
unmanaged secrets Secrets the live pod reads (envFrom, secretKeyRef, secret volumes) that are neither chart-managed nor CSI-synced
🚨 plain secret-shaped values entries whose NAME matches Token\|Secret\|Key\|Password carried as a PLAIN value — inline env on any container, or a key in a chart ConfigMap. A flag that merely ends in Key is a tolerable false positive; a token in a pod spec is an incident
volumes below record a PersistentVolumeClaim whose live capacity is BELOW the size the record declares for it (the record's volumes[] reach helm as the release's persistence values; the audit reads them back from the release). The one record-versus-live comparison: the portal's claims on this fleet are not helm-managed, so a bigger size on the record changes nothing until Reconcile (or Provision) runs hosting-pv-resize — measured 2026-09-08, memex-data declared 16Gi and FULL. Carries the volume, the claim and both sizes; absent when no such claim exists

How to read the report

The report is a Hosting/ConfigAudit node at {space}/Audit/{deployment} (the operational space — Ops unless Hosting:OperationalSpace says otherwise, resolved identically by writer and reader) — one per instance, replaced on every audit. Its page leads with the verdict and lists every finding as a (category, object, items) table; the Fleet area on any audit node shows every instance's verdict side by side; and the instance's own Deployment page carries the latest verdict as a strip under its status line.

Three trust rules, enforced in code: the verdict is derived from the lists by the mesh (LiveOnlyConfigReport.Normalize), never believed from the script; a run whose output carries no report fails rather than recording anything (silence is not clean); and an audit that cannot read a kind (RBAC) or find the release refuses rather than reporting what it never measured.

Scheduling — the standing run

The operator has no scheduler of its own; runs are Hosting/InstanceAction nodes, and anything that can create one can schedule the audit. The intended standing hook lives in the fleet's config repo (Systemorph/Memex), beside its deploy-drift.yml sentinel — the two are complementary: deploy-drift answers "is every merged overlay value LIVE?", this audit answers "is everything live RENDERED?". The workflow to add there (do not hand-roll a second detector — the detection stays in hosting-audit):

# .github/workflows/config-audit.yml (Systemorph/Memex) — sketch
on:
  schedule:
    - cron: "45 6 * * *"     # daily, after chart-drift's 06:15 pass
  workflow_dispatch:
inputs/env:  AUDIT_MCP_URL   = https://<control-instance>/mcp
             AUDIT_API_TOKEN = a mesh API token for a fleet operator account (Key Vault, not a repo secret value)
steps:  for each environment in deployments/aks/envs.json, call the control instance's MCP
        endpoint (tools/call → create) to file an Ops/Actions/audit-{ns}-{run} node with
        requestedAction: Audit — then poll the same node (tools/call → get) until state is
        Done/Failed and fail the job on Failed or on verdict drift, so the Actions tab carries
        the standing red the same way deploy-drift does.

Until that lands, the audit is on-demand: run one after every deploy, every break-glass kubectl, and before declaring any Memex#148 row closed — its definition of done ends with "the audit reports zero live-only items for that namespace".

The moving parts

Piece Where
hosting-audit + _audit.jq (the detection) MeshWeaver repo, deploy/aks/operator/bin/ — fixture-tested in deploy/aks/operator/test/
Audit verb, phases, recording Hosting/InstanceAction (InstanceActionPlan.AuditSteps, InstanceActionControlPlane.WriteAuditRecord)
the report type + pages Hosting/ConfigAudit (LiveOnlyConfigReport, Content/Fleet areas)
the record strip the Deployment page (DeploymentLayoutAreas.AuditStrip)
the operator's RBAC for the read deploy/aks/manifests/hosting-operator/operator-rbac.yaml (read-only list/get on the audited kinds — re-apply it when adopting this feature)
the inventory being worked off Systemorph/Memex#148
Reconnecting…
The server was updated. Reloading the page to pick up the latest version.