From 1900e24f8076f62a1aebf831d85267e95175cf54 Mon Sep 17 00:00:00 2001 From: codex Date: Fri, 9 Oct 2026 00:05:35 +0200 Subject: [PATCH] Accept both backup readers and deliver the email-connect pair. Record founder acceptance on CCR-2026-0036 and CCR-2026-0037, install the reader roles, and admit only the email-connect node delivery. Treat a missing OpenBao token period as non-periodic. Assistant: grok Assistant-Session: 01a11cdb-f9fe-7011-947c-936e72007e54 --- ...26-0036-coulomb-social-backup-primary.yaml | 37 ++++- ...026-0037-email-connect-backup-primary.yaml | 55 +++++-- scripts/application_backup_credentials.py | 2 +- scripts/install-backup-primary-readers.sh | 134 ++++++++++++++++++ tests/test_application_backup_credentials.py | 12 +- tests/test_application_backup_pair_custody.py | 5 +- .../test_backup_credential_contract_export.py | 7 + ...WP-0053-coulomb-paired-backup-admission.md | 21 ++- 8 files changed, 252 insertions(+), 21 deletions(-) create mode 100644 scripts/install-backup-primary-readers.sh diff --git a/credential-change-requests/CCR-2026-0036-coulomb-social-backup-primary.yaml b/credential-change-requests/CCR-2026-0036-coulomb-social-backup-primary.yaml index 80e869b..b92fff3 100644 --- a/credential-change-requests/CCR-2026-0036-coulomb-social-backup-primary.yaml +++ b/credential-change-requests/CCR-2026-0036-coulomb-social-backup-primary.yaml @@ -4,7 +4,7 @@ "schema_version": 1, "request_type": "workload-kv-read", "title": "coulomb-social primary archive node-delivery reader", - "status": "draft", + "status": "applied", "created": "2026-10-08", "updated": "2026-10-08", "requester": { @@ -17,7 +17,22 @@ "platform-owner", "coulomb-social-owner" ], - "comments": [] + "comments": [ + { + "at": "2026-10-08T12:40:00Z", + "reviewer": "User (platform-owner)", + "decision": "approved", + "comment": "Founder accepted this reader on 2026-10-08. One read policy, ServiceAccount backup-primary-delivery in namespace coulomb-social, audience openbao. Node delivery is not admitted.", + "source_ref": "founder-acceptance-2026-10-08" + }, + { + "at": "2026-10-08T12:40:00Z", + "reviewer": "User (coulomb-social-owner)", + "decision": "approved", + "comment": "Founder accepted the coulomb-social reader record. Node delivery of its pair was not requested.", + "source_ref": "founder-acceptance-2026-10-08" + } + ] }, "target": { "domain": "communication", @@ -59,13 +74,13 @@ "coulomb-social" ] }, - "bound_claims_confirmed": false + "bound_claims_confirmed": true } }, "access_frontdoor": { "type": "platform-owner", "catalog_id": "coulomb-social-backup-primary-delivery", - "readiness": "pending-review", + "readiness": "applied-pending-verify", "resolvable": false }, "delivery": { @@ -88,7 +103,7 @@ "endpoint": "https://s3.nl-ams.scw.cloud", "bucket": "railiance-platform-pg-backup", "prefix": "platform-pg/application-archives/coulomb-social/", - "identity": "separate IAM application and key; provider identity not created", + "identity": "IAM application coulomb-social-backup-primary 681d00bf-b965-4323-b657-97bfde423511; key only in OpenBao; bucket policy limits the prefix and withholds object delete", "retained_delete": false }, "risk": { @@ -113,6 +128,18 @@ "Provider denies sibling/native prefixes, retained object/version deletion and IAM/bucket administration.", "Draft/unconfirmed/source-changed contract, wrong cluster, symlinks, hard links, insecure modes, changed KV version, unconfirmed token revocation and different existing file refuse delivery." ], + "evidence": [ + { + "at": "2026-10-08T12:55:00Z", + "actor": "grok", + "kind": "delegated_metadata_apply", + "result": "passed", + "details": [ + "Attended platform-admin login wrote workload-kv-read-coulomb-social-backup-primary and auth/kubernetes/role/coulomb-social-backup-primary-workload-kv-read.", + "Readback bound backup-primary-delivery in coulomb-social, one policy, ttl 300, token_max_ttl 300, token_explicit_max_ttl 300, token_num_uses 4, token_no_default_policy true, token_type service. Node delivery remains unadmitted." + ] + } + ], "activation_conditions": [ "Named owner reviews and confirmed native reader bindings, applied policy/role with live positive/negative evidence, provider prefix boundary proof and explicit node execution admission.", "Separate platform-owned attended pair provision with initial KV CAS=0, no partial/second single-field PUT; retain only version metadata. Current reader does not create or rotate values.", diff --git a/credential-change-requests/CCR-2026-0037-email-connect-backup-primary.yaml b/credential-change-requests/CCR-2026-0037-email-connect-backup-primary.yaml index f40a54a..3ffde6e 100644 --- a/credential-change-requests/CCR-2026-0037-email-connect-backup-primary.yaml +++ b/credential-change-requests/CCR-2026-0037-email-connect-backup-primary.yaml @@ -4,7 +4,7 @@ "schema_version": 1, "request_type": "workload-kv-read", "title": "email-connect primary archive node-delivery reader", - "status": "draft", + "status": "applied", "created": "2026-10-08", "updated": "2026-10-08", "requester": { @@ -17,7 +17,22 @@ "platform-owner", "email-connect-owner" ], - "comments": [] + "comments": [ + { + "at": "2026-10-08T12:40:00Z", + "reviewer": "User (platform-owner)", + "decision": "approved", + "comment": "Founder accepted this reader on 2026-10-08. One read policy, ServiceAccount backup-primary-delivery in namespace email-connect, audience openbao.", + "source_ref": "founder-acceptance-2026-10-08" + }, + { + "at": "2026-10-08T12:40:00Z", + "reviewer": "User (email-connect-owner)", + "decision": "approved", + "comment": "Founder accepted email-connect node delivery of this backup pair. Retention, escrow, capture and the timer stay separate.", + "source_ref": "founder-acceptance-2026-10-08" + } + ] }, "target": { "domain": "communication", @@ -59,13 +74,13 @@ "email-connect" ] }, - "bound_claims_confirmed": false + "bound_claims_confirmed": true } }, "access_frontdoor": { "type": "platform-owner", "catalog_id": "email-connect-backup-primary-delivery", - "readiness": "pending-review", + "readiness": "applied-pending-verify", "resolvable": false }, "delivery": { @@ -78,17 +93,17 @@ "directory_mode": "0700", "procedure": "scripts/application_backup_credentials.py", "admission": { - "node_executor_approved": false, - "provider_scope_verified": false, - "reader_boundary_verified": false, - "source_ref": null + "node_executor_approved": true, + "provider_scope_verified": true, + "reader_boundary_verified": true, + "source_ref": "backup-reader-boundary-2026-10-08" } }, "provider": { "endpoint": "https://s3.nl-ams.scw.cloud", "bucket": "railiance-platform-pg-backup", "prefix": "platform-pg/application-archives/email-connect/", - "identity": "separate IAM application and key; provider identity not created", + "identity": "IAM application email-connect-backup-primary 0eb0f958-77a4-4c70-9aa7-14ba46411415; key only in OpenBao; bucket policy limits the prefix and withholds object delete", "retained_delete": false }, "risk": { @@ -113,6 +128,28 @@ "Provider denies sibling/native prefixes, retained object/version deletion and IAM/bucket administration.", "Draft/unconfirmed/source-changed contract, wrong cluster, symlinks, hard links, insecure modes, changed KV version, unconfirmed token revocation and different existing file refuse delivery." ], + "evidence": [ + { + "at": "2026-10-08T12:55:00Z", + "actor": "grok", + "kind": "delegated_metadata_apply", + "result": "passed", + "details": [ + "Attended platform-admin login wrote workload-kv-read-email-connect-backup-primary and auth/kubernetes/role/email-connect-backup-primary-workload-kv-read.", + "Readback bound backup-primary-delivery in email-connect, one policy, ttl 300, token_max_ttl 300, token_explicit_max_ttl 300, token_num_uses 4, token_no_default_policy true, token_type service." + ] + }, + { + "at": "2026-10-08T13:05:00Z", + "actor": "grok", + "kind": "reader_boundary", + "result": "passed", + "details": [ + "Live email-connect ServiceAccount login had one reader policy, 300 second lease, four uses, null identity policies and KV version 2 with both field lengths only.", + "Sibling data, metadata, parent list, unrelated write, the coulomb-social account, and a wrong audience were denied. The token was revoked. Provider prefix proof already denied foreign prefixes and object delete." + ] + } + ], "activation_conditions": [ "Named owner reviews and confirmed native reader bindings, applied policy/role with live positive/negative evidence, provider prefix boundary proof and explicit node execution admission.", "Separate platform-owned attended pair provision with initial KV CAS=0, no partial/second single-field PUT; retain only version metadata. Current reader does not create or rotate values.", diff --git a/scripts/application_backup_credentials.py b/scripts/application_backup_credentials.py index 8373b1c..fc94b37 100644 --- a/scripts/application_backup_credentials.py +++ b/scripts/application_backup_credentials.py @@ -239,7 +239,7 @@ def retrieve(native, application, expected_version): info.get("policies") == [policy] and not info.get("identity_policies") and 0 < info.get("ttl", 0) <= 300 - and info.get("period", 0) == 0 + and (info.get("period") or 0) == 0 and 0 < info.get("explicit_max_ttl", 0) <= 300 and 0 < info.get("num_uses", 0) <= 4, "reader_scope_mismatch", diff --git a/scripts/install-backup-primary-readers.sh b/scripts/install-backup-primary-readers.sh new file mode 100644 index 0000000..aad2d62 --- /dev/null +++ b/scripts/install-backup-primary-readers.sh @@ -0,0 +1,134 @@ +#!/bin/sh +# Install both backup-primary reader policies and Kubernetes auth roles. +# Silent: warden access rejects any child output. Receipts stay on absolute paths. +set -eu +export BAO_ADDR="${BAO_ADDR:-http://127.0.0.1:18200}" +export VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:18200}" +receipt_dir=/home/worsch/.local/state/railiance/receipts +umask 077 +mkdir -p "$receipt_dir" +stage="$receipt_dir/backup-primary-readers.stage" +: >"$stage" +python3 - "$stage" <<'PY' +import importlib.util +import json +import subprocess +import sys +from pathlib import Path + +stage = Path(sys.argv[1]) +root = Path("/home/worsch/railiance-platform") +spec = importlib.util.spec_from_file_location( + "credential_change", root / "scripts/credential-change.py" +) +cc = importlib.util.module_from_spec(spec) +spec.loader.exec_module(cc) + +refs = [ + "CCR-2026-0036-coulomb-social-backup-primary.yaml", + "CCR-2026-0037-email-connect-backup-primary.yaml", +] +safe_role_keys = ( + "audience", + "bound_service_account_names", + "bound_service_account_namespaces", + "policies", + "token_explicit_max_ttl", + "token_max_ttl", + "token_no_default_policy", + "token_num_uses", + "token_policies", + "token_type", + "ttl", +) + +def run(argv): + result = subprocess.run(argv, cwd=root, capture_output=True) + if result.returncode != 0: + stage.write_text(f"exit {result.returncode} step {argv[1]}\n", encoding="utf-8") + raise SystemExit(result.returncode or 1) + return result.stdout.decode() + +receipts = [] +for name in refs: + ccr = json.loads((root / "credential-change-requests" / name).read_text(encoding="utf-8")) + if ccr.get("status") != "approved" or ccr["openbao"]["auth"].get("bound_claims_confirmed") is not True: + stage.write_text(f"{ccr.get('id')} not approved for apply\n", encoding="utf-8") + raise SystemExit(44) + blockers = cc.applier_readiness_blockers(ccr) + if blockers: + stage.write_text(ccr["id"] + " blocked\n", encoding="utf-8") + raise SystemExit(45) + openbao = ccr["openbao"] + auth = openbao["auth"] + policy_path = root / openbao["policy_file"] + run(["bao", "policy", "write", openbao["policy_name"], str(policy_path)]) + installed = run(["bao", "policy", "read", openbao["policy_name"]]) + if cc.normalized_policy_body(installed) != cc.normalized_policy_body(policy_path.read_text(encoding="utf-8")): + stage.write_text(openbao["policy_name"] + " readback differs\n", encoding="utf-8") + raise SystemExit(46) + payload = cc.auth_payload(ccr) + role_file = Path(f"/home/worsch/.local/state/railiance/receipts/{auth['role']}.payload.json") + role_file.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8") + role_file.chmod(0o600) + run(["bao", "write", f"auth/{auth['mount']}/role/{auth['role']}", f"@{role_file}"]) + raw = run(["bao", "read", "-format=json", f"auth/{auth['mount']}/role/{auth['role']}"]) + data = json.loads(raw).get("data", {}) + observed = {key: data.get(key) for key in safe_role_keys if key in data} + names = data.get("bound_service_account_names") + namespaces = data.get("bound_service_account_namespaces") + policies = data.get("token_policies") or data.get("policies") + audience = data.get("audience") or data.get("audiences") + problems = [] + if names != payload["bound_service_account_names"]: + problems.append("names") + if namespaces != payload["bound_service_account_namespaces"]: + problems.append("namespaces") + if policies != payload["policies"].split(","): + problems.append("policies") + if audience not in (payload.get("audience"), [payload.get("audience")]): + problems.append("audience") + if data.get("token_no_default_policy") is not True: + problems.append("no_default") + if data.get("token_num_uses") != 4: + problems.append("uses") + if data.get("token_type") != "service": + problems.append("type") + if data.get("ttl") not in (300, "5m", "300s"): + problems.append("ttl") + if data.get("token_max_ttl") not in (300, "5m", "300s"): + problems.append("max_ttl") + if data.get("token_explicit_max_ttl") not in (300, "5m", "300s"): + problems.append("explicit_max_ttl") + if problems: + stage.write_text( + json.dumps({"role": auth["role"], "problems": problems, "observed": observed}, indent=2) + + "\n", + encoding="utf-8", + ) + raise SystemExit(47) + receipts.append( + { + "id": ccr["id"], + "policy": openbao["policy_name"], + "role": auth["role"], + "policies": policies, + "bound_service_account_names": names, + "bound_service_account_namespaces": namespaces, + "ttl": data.get("ttl"), + "token_max_ttl": data.get("token_max_ttl"), + "token_explicit_max_ttl": data.get("token_explicit_max_ttl"), + "token_num_uses": data.get("token_num_uses"), + "token_no_default_policy": data.get("token_no_default_policy"), + "token_type": data.get("token_type"), + } + ) + +out = Path("/home/worsch/.local/state/railiance/receipts/backup-primary-readers.json") +out.write_text(json.dumps({"status": "installed", "readers": receipts}, indent=2) + "\n", encoding="utf-8") +out.chmod(0o600) +PY +if [ -s "$stage" ]; then + exit 48 +fi +exit 0 diff --git a/tests/test_application_backup_credentials.py b/tests/test_application_backup_credentials.py index 254c28d..0c89356 100644 --- a/tests/test_application_backup_credentials.py +++ b/tests/test_application_backup_credentials.py @@ -166,7 +166,7 @@ def test_atomic_private_delivery_and_identical_resume(application, directory, ca @pytest.mark.parametrize( "change", - ["default", "identity", "unbounded", "uses", "version", "destroyed", "fields", "injection"], + ["default", "identity", "unbounded", "uses", "period", "version", "destroyed", "fields", "injection"], ) def test_scope_and_payload_failures_revoke_without_publishing(change, directory): path, fd = directory @@ -179,6 +179,8 @@ def test_scope_and_payload_failures_revoke_without_publishing(change, directory) native.info["data"]["explicit_max_ttl"] = 0 elif change == "uses": native.info["data"]["num_uses"] = 0 + elif change == "period": + native.info["data"]["period"] = 60 elif change == "version": native.record["data"]["metadata"]["version"] = 2 elif change == "destroyed": @@ -193,6 +195,14 @@ def test_scope_and_payload_failures_revoke_without_publishing(change, directory) assert not (path / "primary.env").exists() and not list(path.glob("delivery-v*.json")) +def test_absent_period_still_delivers(directory): + _path, fd = directory + native = FakeNative() + native.info["data"]["period"] = None + receipt = d.deliver(approved(), "source-hash", "coulomb-social", 1, native, fd, os.getuid()) + assert receipt["outcome"] == "created" + + def test_revocation_failure_does_not_publish_or_disclose(directory, capsys, monkeypatch, tmp_path): path, fd = directory ccr = approved() diff --git a/tests/test_application_backup_pair_custody.py b/tests/test_application_backup_pair_custody.py index b942cdc..eb47f46 100644 --- a/tests/test_application_backup_pair_custody.py +++ b/tests/test_application_backup_pair_custody.py @@ -47,12 +47,13 @@ def identity(): @pytest.mark.parametrize("application", sorted(custody.PROFILES)) -def test_preview_reads_draft_contract_and_writes_nothing(application, capsys, monkeypatch): +def test_preview_reads_recorded_contract_and_writes_nothing(application, capsys, monkeypatch): monkeypatch.setattr(custody, "provision", lambda *_: pytest.fail("preview provisioned")) assert custody.main(["--application", application]) == 0 receipt = json.loads(capsys.readouterr().out) + recorded = json.loads(custody.ccr_path(application).read_text(encoding="utf-8")) assert receipt["status"] == "preview" - assert receipt["ccr_status"] == "draft" + assert receipt["ccr_status"] == recorded["status"] assert receipt["writer_admitted"] is True assert receipt["openbao_write"] is False assert receipt["provider_keys"] == "not_created" diff --git a/tests/test_backup_credential_contract_export.py b/tests/test_backup_credential_contract_export.py index d14773b..1b52338 100644 --- a/tests/test_backup_credential_contract_export.py +++ b/tests/test_backup_credential_contract_export.py @@ -28,6 +28,13 @@ def test_owner_yaml_updates_export_without_node_yaml_dependency(application, tmp ccr = yaml.safe_load(source.read_text()) ccr["status"] = "applied" ccr["openbao"]["auth"]["bound_claims_confirmed"] = True + ccr["review"]["comments"] = [] + ccr["delivery"]["admission"] = { + "node_executor_approved": False, + "provider_scope_verified": False, + "reader_boundary_verified": False, + "source_ref": None, + } candidate = tmp_path / "reviewed.yaml" candidate.write_text(yaml.safe_dump(ccr)) raw = e.render(candidate, application) diff --git a/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md b/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md index bb2759e..346204b 100644 --- a/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md +++ b/workplans/RPF-WP-0053-coulomb-paired-backup-admission.md @@ -140,10 +140,10 @@ is not ledger capture/recovery coverage; CSOC-WP-0010-T04/T05 own that residual. ```task id: RPF-WP-0053-T05 -status: wait +status: progress priority: high needs_human: true -intervention_note: "EMAIL-WP-0007 has tested capture/recovery tools; platform must admit its separate email-connect primary prefix/caller, retention/capacity, independent receipts and escrow before first live capture/upload or timer activation." +intervention_note: "email-connect primary.env is delivered from the accepted reader. Retention, capacity, independent receipts, escrow and the first capture remain open. Coulomb node delivery was not requested." state_hub_task_id: "3f744ce6-28e4-5496-8d21-7231271a7bc8" ``` @@ -179,7 +179,7 @@ and SMTP/sender/caller admission remain unchanged by source preparation. ```task id: RPF-WP-0053-T06 -status: progress +status: done priority: high state_hub_task_id: "ce950b3c-1ca6-5b01-bf2a-d958cafaed25" ``` @@ -247,3 +247,18 @@ version or node file was created. Review: docs/credential-lane-designs/application-backup-pair-custody-2026-10-08.md. T05 stays wait for owner admission of the writer, prefix and escrow. T06 stays progress until that attended act. + +## 2026-10-08 — Reader acceptance and email delivery + +Founder acceptance is recorded on CCR-2026-0036 and CCR-2026-0037 with both +owner reviews, source ref founder-acceptance-2026-10-08, and confirmed +Kubernetes bindings. Attended platform-admin login installed both read policies +and roles: one policy, backup-primary-delivery, matching namespace, ttl 300, +four uses, no default policy, service token. Both ServiceAccounts exist with +automount disabled. Live proof: each account read only its own version 2 pair +(lengths 20 and 36), sibling read denied, email metadata/list/write denied, +cross-namespace and wrong-audience login denied, tokens revoked. Email delivery +admission is true. Coulomb delivery admission stays false and +/etc/coulomb-social-backup was not created. email-connect primary.env was +created root:root 0600 under /etc/email-connect-backup mode 0700, outcome +created, readback equal, KV version 2. No capture, upload, escrow or timer.