Skip to content

Secrets: write-only entry, split identities — operator verbs, design doc, CI guard - #5790

Merged
meshweaver-cloud[bot] merged 3 commits into
mainfrom
feat/secrets-write-only-split-identities
Sep 27, 2026
Merged

meshweaver-cloud[bot] merged 3 commits into
mainfrom
feat/secrets-write-only-split-identities

Conversation

@rbuergi

@rbuergi rbuergi commented Sep 27, 2026

Copy link
Copy Markdown
Contributor

What

Policy secrets-write-only-entry (registered as proposed in PolicyNotProse, with the owed items listed): nobody accesses Key Vault by hand.

  • Every secret is entered through a write-only GUI in the app that owns it.
  • ONE governed writer identity stores it. It can list, set, delete and recover, and never read a value.
  • The pods read through a reader identity that can only get.

Design page: Doc/Architecture/SecretsWriteOnlyEntry.

Operator (core half of the governed write path)

  • hosting-kv-set
    • stamps tags at write time: mw-fp (sha256:<12 hex>, or withheld:low-entropy below 128 estimated bits), mw-set-by, mw-set-at, mw-source;
    • --generate mints a value in the Job that nobody ever sees;
    • --set-by records the requester;
    • no longer reads a value back: it confirms the version through list-versions (metadata).
  • hosting-kv-status (new): status from list / list-deleted metadata only.
  • hosting-kv-state (new): enable, disable, delete, recover and purge. Purge is refused unless the object is already soft-deleted. It passes no tags, because set-attributes would wipe the fingerprint.
  • Behaviour tests: the stub az plays the writer and refuses secret show. 648/648 locally, and shellcheck is clean.
  • The new verbs are declared not-yet-wired in hosting-operator.yml; the plan half is the Plugins PR.
  • manifests/hosting-operator/secret-writer.yaml: the writer's ServiceAccount (federated to secret-writer) and its narrow ClusterRole.
    • The one residual, a synced-Secret get used for the hash wait, is stated there.
    • It is applied through the governed cluster-RBAC path, not by this PR.

CI guard

  • .github/scripts/check-manual-keyvault.py + .github/manual-keyvault.allow: refuses any tracked doc, runbook, skill or script that holds a Key Vault secret data-plane command. Exceptions are the governed verbs (allow file) and a procedure marked kv-break-glass. A stale allow entry is red.
  • It has a self-test, and it is wired into core's dotnet-test.yml guard block.
  • It is deliberately NOT yet in node-repo-validate.yml: MeshWeaver.Plugins still carries such commands, so wiring it now would red every Plugins PR. That wiring follows once the Plugins cleanup PR merges.

Docs

Manual vault commands are removed from: DependabotSecretStore, DeploymentOptions, InstanceIdentityAndSetup, MemexCloudDeployment, OnboardingNewEnvironment, OperatorCredentialsByName, SelfUpdateAnnouncementKey, and the /deployment and /new-deployment skills. Each now points at the Deployments page's Set Key Vault secrets… dialog.

Pairs-with: none — no public type or member removed.
Mirror-sync: none — no i18n catalog key added or changed.

Counterparts: the Systemorph/Memex PR (writer identity in infra/estate.bicep, docs/control-instance.md, runbooks) and the MeshWeaver.Plugins PR (status/lifecycle actions, writer ServiceAccount selection, GUI). Both follow this one.

🤖 Generated with Claude Code

…or verbs, doc, CI guard

Policy `secrets-write-only-entry`: nobody touches Key Vault by hand. Secrets are entered through
the write-only GUI of the app that owns them and written by ONE governed writer identity that can
list, set, delete and recover but never read a value; the pods read through a get-only reader.

- hosting-kv-set: stamps mw-fp (SHA-256 prefix, withheld for low-entropy values), mw-set-by,
  mw-set-at and mw-source tags at write time; --generate mints a value in the Job that nobody
  sees; confirms the write by version through list-versions and never reads a value back.
- hosting-kv-status (new): status from vault METADATA only (existence, enabled, dates, soft-delete,
  write-time tags).
- hosting-kv-state (new): enable/disable/delete/recover/purge; purge only when already soft-deleted.
- manifests/hosting-operator/secret-writer.yaml: the writer's ServiceAccount and narrow ClusterRole.
- Doc/Architecture/SecretsWriteOnlyEntry + PolicyNotProse row; manual vault commands removed from
  core docs and the deployment skills in favour of the Deployments page's secret dialog.
- .github/scripts/check-manual-keyvault.py: CI refuses a new hand-run vault command outside the
  governed verbs (allow file) or a marked kv-break-glass procedure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 27, 2026 07:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved critical issues affect the CI guard, workflow tests, RBAC scope, and lifecycle safety.

Review effort: Lite
Findings: 5 High severity · 2 Low severity

Open (7)
What changed in this PR

Adds a proposed write-only Key Vault workflow with split writer/reader identities, operator verbs, documentation, RBAC, tests, and CI enforcement.

Changes:

  • Adds metadata-only status and lifecycle operations with fingerprinted writes.
  • Adds writer identity manifests, behavior tests, and workflow declarations.
  • Updates deployment guidance and guards against manual Key Vault commands.
File Summary
src/​MeshWeaver.Documentation/​Data/​Architecture/​SelfUpdateAnnouncementKey.md Updates secret-entry guidance.
src/​MeshWeaver.Documentation/​Data/​Architecture/​SecretsWriteOnlyEntry.md Documents the write-only design and migration.
src/​MeshWeaver.Documentation/​Data/​Architecture/​PolicyNotProse.md Registers the proposed policy.
src/​MeshWeaver.Documentation/​Data/​Architecture/​OperatorCredentialsByName.md Documents migration limitations.
src/​MeshWeaver.Documentation/​Data/​Architecture/​OnboardingNewEnvironment.md Replaces manual vault procedures.
src/​MeshWeaver.Documentation/​Data/​Architecture/​MemexCloudDeployment.md Updates secret rotation guidance.
src/​MeshWeaver.Documentation/​Data/​Architecture/​InstanceIdentityAndSetup.md Removes manual vault references.
src/​MeshWeaver.Documentation/​Data/​Architecture/​DeploymentOptions.md Updates secret setup instructions.
src/​MeshWeaver.Documentation/​Data/​Architecture/​DependabotSecretStore.md Removes manual vault handling.
src/​MeshWeaver.Documentation/​Data/​Architecture.md Links the new architecture page.
deploy/​aks/​operator/​test/​stubs/​kv-status/​az Adds the status-command test stub.
deploy/​aks/​operator/​test/​stubs/​kv-set/​az Updates write-command test behavior.
deploy/​aks/​operator/​test/​run-tests.sh Adds operator behavior tests.
deploy/​aks/​operator/​bin/​hosting-kv-status Implements metadata-only status reporting.
deploy/​aks/​operator/​bin/​hosting-kv-state Implements lifecycle operations.
deploy/​aks/​operator/​bin/​hosting-kv-set Implements write-only secret setting.
deploy/​aks/​operator/​bin/​_common.sh Provides fingerprint and actor helpers.
deploy/​aks/​manifests/​hosting-operator/​secret-writer.yaml Adds the writer identity and RBAC.
deploy/​aks/​manifests/​hosting-operator/​README.md Documents the writer identity.
.github/​workflows/​hosting-operator.yml Declares commands and validates stubs.
.github/​workflows/​dotnet-test.yml Integrates the manual-command guard.
.github/​scripts/​check-manual-keyvault.py Scans for prohibited vault commands.
.github/​manual-keyvault.allow Defines command exceptions.
.claude/​skills/​new-deployment/​SKILL.md Updates deployment secret instructions.
.claude/​skills/​deployment/​SKILL.md Updates operational guidance.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/manual-keyvault.allow Outdated
# matches a file holding a vault command is stale and reds the gate. A doc or runbook NEVER goes
# here — a documented break-glass procedure carries the `kv-break-glass` marker instead.

deploy/aks/operator/bin/* — the GOVERNED implementation: the operator verbs an InstanceAction runs (hosting-kv-set/-status/-state as the writer identity; the provisioning verbs still on hosting-operator are the migration's step 3)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb. The allow file no longer has a glob. It names each governed file with its reason, and the value readers (kv-ensure, kv-copy, kv-rotate, registry-register, signin-app, image-mirror, pull-secret, hosting-deploy, _common.sh's pg_password) are marked TRANSITIONAL. The migration's step 2 removes each read, and its line goes with it. The hand-command fallback strings in kv-ensure, kv-copy, signin-app and registry-register now point at the write-only GUI instead of an az keyvault secret set, and the behaviour tests assert the new text.

Comment on lines +52 to +54
PATTERN = re.compile(
r"az\s+keyvault\s+(?:secret\s+(?:set-attributes|set|show|download|delete|purge|recover|backup|restore)\b"
r"|set-policy\b[^\n]*--secret-permissions)")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb. logical_lines() joins lines that end in a trailing backslash before matching. Two self-test cases cover a command split after secret and one split after keyvault, and both fire.

Comment on lines +108 to +110
window = lines[max(0, i - MARKER_WINDOW): i + 1]
if any(MARKER in w for w in window):
continue

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb. The marker now exempts a command only in the form kv-break-glass: <reason> with at least 20 characters of reason. A bare marker and a too-short reason each have a self-test case that fires.

Comment thread .github/workflows/hosting-operator.yml Outdated
run: |
set -euo pipefail
for f in deploy/aks/operator/test/stubs/pull-secret/az deploy/aks/operator/test/stubs/pull-secret/kubectl deploy/aks/operator/test/stubs/pv-resize/kubectl deploy/aks/operator/test/stubs/inline-env/kubectl deploy/aks/operator/test/stubs/kv-rotate/az deploy/aks/operator/test/stubs/kv-ensure/az deploy/aks/operator/test/stubs/kv-copy/az deploy/aks/operator/test/stubs/registry-register/az deploy/aks/operator/test/stubs/signin-app/az deploy/aks/operator/test/stubs/image-mirror/az deploy/aks/operator/test/stubs/image-mirror/crane deploy/aks/operator/test/stubs/deploy/az deploy/aks/operator/test/stubs/kv-set/az deploy/aks/operator/test/stubs/kv-set/kubectl deploy/aks/operator/test/stubs/db-release/kubectl deploy/aks/operator/test/stubs/db-release/helm deploy/aks/operator/test/stubs/pg/az deploy/aks/operator/test/stubs/pg/pg_dump deploy/aks/operator/test/stubs/pg/psql deploy/aks/operator/test/stubs/pg/pg_restore deploy/aks/operator/test/stubs/aks-upgrade/az deploy/aks/operator/test/stubs/aks-upgrade/kubectl deploy/aks/operator/test/stubs/aks-upgrade/curl; do
for f in deploy/aks/operator/test/stubs/pull-secret/az deploy/aks/operator/test/stubs/pull-secret/kubectl deploy/aks/operator/test/stubs/pv-resize/kubectl deploy/aks/operator/test/stubs/inline-env/kubectl deploy/aks/operator/test/stubs/kv-rotate/az deploy/aks/operator/test/stubs/kv-ensure/az deploy/aks/operator/test/stubs/kv-copy/az deploy/aks/operator/test/stubs/registry-register/az deploy/aks/operator/test/stubs/signin-app/az deploy/aks/operator/test/stubs/image-mirror/az deploy/aks/operator/test/stubs/image-mirror/crane deploy/aks/operator/test/stubs/deploy/az deploy/aks/operator/test/stubs/kv-set/az deploy/aks/operator/test/stubs/kv-set/kubectl deploy/aks/operator/test/stubs/kv-status/azdeploy/aks/operator/test/stubs/db-release/kubectl deploy/aks/operator/test/stubs/db-release/helm deploy/aks/operator/test/stubs/pg/az deploy/aks/operator/test/stubs/pg/pg_dump deploy/aks/operator/test/stubs/pg/psql deploy/aks/operator/test/stubs/pg/pg_restore deploy/aks/operator/test/stubs/aks-upgrade/az deploy/aks/operator/test/stubs/aks-upgrade/kubectl deploy/aks/operator/test/stubs/aks-upgrade/curl; do

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb: the separator is restored. The bin/ tracked-file floor is also raised to 34 for the two new verbs.

Comment on lines +43 to +45
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb. The Secret rule is gone. hosting-kv-set --wait now decides from secretproviderclasspodstatuses VERSIONS, which are metadata: every pod mount must report the version the write just created. The writer's ClusterRole holds no Secret read anywhere. The kubectl stub refuses get secret, so a regression is red. operator-rbac.yaml gains a read-only rule for the pod statuses, for as long as a secret write can still run as hosting-operator.

Comment on lines +60 to +61
exactly the reader this design removes. `MeshWeaver.Mesh.SecretFingerprint.Of` and the operator's
`hosting::fingerprint` compute the same string.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb. The page now calls the operator's hosting::fingerprint the reference implementation, and states that the portal-side SecretFingerprint.Of is OWED by the companion core change (the write-only secret control), which must match it byte for byte. The exact rule has been handed to that change's author.

Comment on lines +148 to +149
A stale allow entry fails too, so the list only shrinks. Core runs the guard on itself. Every node repo
gets it through the shared `node-repo-validate` lane, and Systemorph/Memex runs it in its own build.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 30840bb. The page now says core runs the guard on itself, and that node repos and Memex do not run it yet: wiring node-repo-validate waits for the Plugins cleanup, because doing it first would red every Plugins PR.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

460 tests  ±0   460 ✅ ±0   46s ⏱️ ±0s
  3 suites ±0     0 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

  1 files  ±0    1 suites  ±0   2m 45s ⏱️ +3s
348 tests ±0  348 ✅ ±0  0 💤 ±0  0 ❌ ±0 
352 runs  ±0  352 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 690 tests  ±0   1 690 ✅ ±0   4m 9s ⏱️ -3s
    2 suites ±0       0 💤 ±0 
    2 files   ±0       0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

    3 files  ±0      3 suites  ±0   6m 0s ⏱️ -24s
2 171 tests ±0  2 171 ✅ ±0  0 💤 ±0  0 ❌ ±0 
2 172 runs  ±0  2 172 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

♻️ This comment has been updated with latest results.

@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

762 tests  ±0   571 ✅ ±0   7m 8s ⏱️ ±0s
  3 suites ±0   191 💤 ±0 
  3 files   ±0     0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

♻️ This comment has been updated with latest results.

rbuergi and others added 2 commits September 27, 2026 09:51
…wed allow list, continuation + reasoned break-glass in the guard, no awk in the image

- hosting-kv-set --wait reads SecretProviderClassPodStatus versions (metadata); the writer's
  ClusterRole drops its Secret get; operator-rbac gains the read-only pod-status grant.
- hosting::fingerprint uses pure bash arithmetic (Azure Linux 3 carries no awk).
- Hand-command fallbacks in kv-ensure/kv-copy/signin-app/registry-register now point at the
  write-only GUI.
- check-manual-keyvault: joins continued lines; a marker exempts only with a >=20-char reason;
  allow list names each governed file, value readers marked TRANSITIONAL.
- Design doc: owed components marked owed, lane wiring stated as deferred, migration plan incl.
  the RBAC-mode fleet vault (step 4).
- hosting-operator.yml: stub list separator, bin floor 34.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… GUI

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    5 files  ±0      5 suites  ±0   14m 40s ⏱️ +3s
4 258 tests ±0  4 256 ✅ ±0  2 💤 ±0  0 ❌ ±0 
4 262 runs  ±0  4 260 ✅ ±0  2 💤 ±0  0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   17 files  ±0     17 suites  ±0   35m 31s ⏱️ -20s
9 689 tests ±0  9 496 ✅ ±0  193 💤 ±0  0 ❌ ±0 
9 698 runs  ±0  9 505 ✅ ±0  193 💤 ±0  0 ❌ ±0 

Results for commit 8d22ecc. ± Comparison against base commit e18018d.

@meshweaver-cloud
meshweaver-cloud Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 2bb14d8 Sep 27, 2026
43 checks passed
@rbuergi
rbuergi deleted the feat/secrets-write-only-split-identities branch October 10, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants