Repository navigation
Hosting operator: hosting-audit names what lives only on the cluster - #2734
Conversation
…luster The memex portal crashed at boot because its Email config existed twice: the chart ConfigMap defaults and a cluster-only secret patched onto the Deployment as env — .NET picks the winner at random per pod start, and a helm upgrade preserves live edits, so nothing ever went red (Systemorph/Memex#148 is the hand-measured inventory: 25 env patches, 10 live-edited ConfigMap keys, sidecars, unmanaged objects, a plain-text registry token in the pod spec). The operator image gains hosting-audit + _audit.jq: diff a namespace against 'helm get manifest' + 'helm get hooks' (the truth) and report, by NAME and never by value, the portal Deployment's live-only env/envFrom/volumes/mounts/ containers/pod-spec patches, the chart ConfigMaps' live-only and value-differing keys, objects the chart does not own, secrets the pod reads that nothing renders, and secret-shaped names carried as plain values. Verdict clean/drift rides '::hosting:: audit=<base64 JSON>' for the Hosting plugin's new Audit instance action to record; drift exits 0 (a finding, not a failed run), while 'could not measure' — no release, no live portal, a kind the ClusterRole may not list — refuses rather than reporting clean. Fixture-tested via helm/kubectl stubs (drift, clean, forbidden, missing-release, injection, value-leak guard) in test/run-tests.sh; the operator ClusterRole gains read-only list/get on the audited kinds; the image installs jq and the CI gate pins the file counts, the plan-command list and the jq program's parse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…joins the plan-command gate The tracked-file floor said 16 while 17 files were tracked, and the every-command-the-plan-can-emit list never carried hosting-pv-purge although the teardown plan emits it — a missing pv-purge would have failed INSIDE a live teardown, after the namespace delete, exactly what the gate exists to refuse. Now: 19 files (16 commands + run.sh + _common.sh + _audit.jq), 16-entry plan list. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds a new read-only hosting-audit operator command that detects “live-only” Kubernetes configuration drift by diffing a namespace’s live objects against the Helm release manifest/hooks, and wires it into documentation, RBAC, CI, and operator test fixtures.
Changes:
- Add
deploy/aks/operator/bin/hosting-auditplus its jq-based detection program (_audit.jq) to compute a typed drift report. - Extend operator tests with helm/kubectl stubs and fixtures to assert drift/clean/refusal behaviors and “no secret value leakage” guarantees.
- Update operator image dependencies (jq), RBAC read permissions for audited kinds, docs, and the hosting-operator CI gate.
Reviewed changes
Copilot reviewed 35 out of 40 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-the-cluster-can-no-longer-hide-configuration.md | What’s New entry describing hosting-audit and its drift/refusal semantics. |
| deploy/aks/operator/test/stubs/kubectl | Kubectl stub to serve fixture JSON for audit tests. |
| deploy/aks/operator/test/stubs/helm | Helm stub to serve fixture status/manifest/hooks for audit tests. |
| deploy/aks/operator/test/run-tests.sh | Adds hosting-audit behavior tests and report assertions. |
| deploy/aks/operator/test/fixtures/audit/missing/.keep | Placeholder for “missing release” fixture directory. |
| deploy/aks/operator/test/fixtures/audit/forbidden/status.json | Fixture Helm status JSON for “forbidden kind” scenario. |
| deploy/aks/operator/test/fixtures/audit/forbidden/manifest.json | Fixture manifest objects for “forbidden kind” scenario. |
| deploy/aks/operator/test/fixtures/audit/forbidden/live/role.forbidden | Marker file to force a Forbidden response for role listing. |
| deploy/aks/operator/test/fixtures/audit/forbidden/live/deployment.json | Fixture live Deployment list for “forbidden kind” scenario. |
| deploy/aks/operator/test/fixtures/audit/forbidden/live/configmap.json | Fixture live ConfigMap list for “forbidden kind” scenario. |
| deploy/aks/operator/test/fixtures/audit/drift/status.json | Fixture Helm status JSON for “drift” scenario. |
| deploy/aks/operator/test/fixtures/audit/drift/manifest.json | Fixture manifest objects for “drift” scenario. |
| deploy/aks/operator/test/fixtures/audit/drift/live/serviceaccount.json | Fixture live ServiceAccounts for drift detection/exclusions. |
| deploy/aks/operator/test/fixtures/audit/drift/live/service.json | Fixture live Services including unmanaged ones. |
| deploy/aks/operator/test/fixtures/audit/drift/live/secretproviderclass.json | Fixture live SecretProviderClass list for CSI context. |
| deploy/aks/operator/test/fixtures/audit/drift/live/secret.json | Fixture live Secrets list (chart-managed, CSI-managed, unmanaged, furniture). |
| deploy/aks/operator/test/fixtures/audit/drift/live/persistentvolumeclaim.json | Fixture live PVCs including unmanaged ones. |
| deploy/aks/operator/test/fixtures/audit/drift/live/job.json | Fixture live Jobs including owned and hook job cases. |
| deploy/aks/operator/test/fixtures/audit/drift/live/ingress.json | Fixture live Ingresses including unmanaged ones. |
| deploy/aks/operator/test/fixtures/audit/drift/live/deployment.json | Fixture live Deployments including drifted portal and unmanaged deployment. |
| deploy/aks/operator/test/fixtures/audit/drift/live/cronjob.json | Fixture live CronJobs including unmanaged ones. |
| deploy/aks/operator/test/fixtures/audit/drift/live/configmap.json | Fixture live ConfigMaps with edited/differing keys and unmanaged configmap. |
| deploy/aks/operator/test/fixtures/audit/drift/hooks.json | Fixture hook Job object to ensure hooks are treated as managed. |
| deploy/aks/operator/test/fixtures/audit/clean/status.json | Fixture Helm status JSON for “clean” scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/manifest.json | Fixture manifest objects for “clean” scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/live/serviceaccount.json | Fixture live ServiceAccounts for clean scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/live/service.json | Fixture live Services for clean scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/live/secret.json | Fixture live Secrets for clean scenario (including helm record, CSI secret). |
| deploy/aks/operator/test/fixtures/audit/clean/live/scaledobject.notype | Marker file to simulate an unserved CRD kind (skippedKinds). |
| deploy/aks/operator/test/fixtures/audit/clean/live/job.json | Fixture live Jobs for clean scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/live/ingress.json | Fixture live Ingresses for clean scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/live/deployment.json | Fixture live Deployment for clean scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/live/configmap.json | Fixture live ConfigMaps for clean scenario. |
| deploy/aks/operator/test/fixtures/audit/clean/hooks.json | Fixture hook Job object for clean scenario. |
| deploy/aks/operator/Dockerfile | Installs jq to support the audit’s jq-based detection. |
| deploy/aks/operator/bin/hosting-audit | New operator command: reads helm/kubectl, computes report, emits ::hosting:: lines. |
| deploy/aks/operator/bin/_audit.jq | New jq program implementing the drift detection and report shape. |
| deploy/aks/manifests/hosting-operator/operator-rbac.yaml | Adds read-only list/get permissions for additional audited kinds. |
| deploy/aks/INSTANCE-LIFECYCLE.md | Documents hosting-audit as part of the instance lifecycle flow. |
| .github/workflows/hosting-operator.yml | Updates gate expectations (tracked files, plan commands), parse-checks _audit.jq, requires jq in image checks. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…e, never a command line Every other compared pod-spec field carries placement and sizing (replicas, selectors, quantities, probe numbers), but a lifecycle hook holds COMMAND LINES, and a command line can embed anything — tokens included. The comparison keeps using the full value; the report now shows the hook's structure with every string scalar replaced by '…', which is enough to see THAT a hook was patched and WHERE without quoting what it runs. Pinned by a fixture case whose command string joins the must-never-print leak guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Test Results (shard 4) 10 files 10 suites 5m 48s ⏱️ Results for commit 9f52772. |
Test Results (shard 0)913 tests 913 ✅ 5m 30s ⏱️ Results for commit 9f52772. |
|
CI red on shard 2 was |
|
Second red on the re-run, a DIFFERENT test in the same shard: |
What
The operator image gains
hosting-audit— one read-only command that diffs an instance's namespace against its helm release's manifest (helm get manifest+helm get hooks, turned into objects bykubectl apply --dry-run=client) and reports everything that lives ONLY on the cluster, by name and never by value:kubectl patchreaches (replicas — unless an autoscaler owns them — nodeSelector, tolerations, affinity, SA, grace period, the portal container's resources/lifecycle/startup-probe budget);Token|Secret|Key|Passwordcarried as a plain value (inline env on any container, or a chart ConfigMap key).The verdict (
clean/drift) plus the full typed report ride::hosting:: audit=<base64 JSON>for the Hosting plugin's newAuditinstance action (MeshWeaver.Plugins PR, follows this one) to record on the mesh. Drift exits 0 — a finding, not a failed run. Could not measure refuses: no release, no live portal Deployment, or a kind the ClusterRole may not list each fail the run, because a kind the audit could not read must not be reported as clean.Why
The memex portal crashed at boot because its Email section existed twice — chart ConfigMap defaults vs a cluster-only secret patched as env — and .NET picks the winner at random per pod start.
helm upgradepreserves live-edited ConfigMap keys and never touches objects it does not own, so this class of drift never fails a deploy; it fails months later as an outage. Systemorph/Memex#148 is the hand-measured inventory (25 env patches, 10 live-edited keys, sidecars, a plain-text registry token in the pod spec); this is the machinery that keeps it measured. The rule enforced: nothing lives only on the cluster — every value is a typed field on theHosting/Deploymentrecord, rendered by the chart; secrets are Key Vault names.How it is proven
test/run-tests.shgains 26 audit cases run against helm/kubectl stubs over fixture manifests: a drifting namespace (every category found by name), a clean one (verdict clean, an unserved kind recorded as skipped — never silently absent), a missing release (refused), a Forbidden kind (refused naming the ClusterRole), injection-shaped names (refused before anything runs), and the one that matters most: no fixture secret value ever appears in the output. 65/65 green locally.hosting-audit), asserts_audit.jqships and parses (jq exit 3 = red), and the image check now requiresjq(present in the Azure Linux base; the Dockerfile pins it via tdnf anyway).🤖 Generated with Claude Code