From 15184aaa016ab7a6f10ccdf72953fe64f180752b Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 23 Sep 2026 20:00:40 +0200 Subject: [PATCH] CCR-2026-0029/0030: activity-core ops_run worker token paths (proposed) ACTIVITY-WP-0039-T02. Add read on the two exact worker-token paths to workload-kv-read-activity-core-eso, one CCR per worker identity. Adds scripts/openbao-policy-sync.sh, a silent attended policy writer that refuses undeclared live drift and verifies on readback. Not applied; CCRs await platform-operator and activity-core-owner approval. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 150322@bnt-lap001 Assistant-Session: 16a7b788-374e-4915-a1df-fc87ffd9a5e4 --- WORK-RECORDS.md | 7 ++ ...-run-worker-rein-aharness-railiance01.yaml | 101 +++++++++++++++ ...ker-rein-aharness-metered-railiance01.yaml | 101 +++++++++++++++ .../workload-kv-read-activity-core-eso.hcl | 14 ++- scripts/openbao-policy-sync.sh | 4 + scripts/openbao_policy_sync.py | 116 ++++++++++++++++++ tests/test_openbao_policy_sync.py | 65 ++++++++++ .../RPF-WP-0046-eso-database-token-renewal.md | 7 ++ 8 files changed, 413 insertions(+), 2 deletions(-) create mode 100644 credential-change-requests/CCR-2026-0029-activity-core-ops-run-worker-rein-aharness-railiance01.yaml create mode 100644 credential-change-requests/CCR-2026-0030-activity-core-ops-run-worker-rein-aharness-metered-railiance01.yaml create mode 100755 scripts/openbao-policy-sync.sh create mode 100755 scripts/openbao_policy_sync.py create mode 100644 tests/test_openbao_policy_sync.py diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 1753e8a..d2aadcd 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -22,6 +22,7 @@ | workplan | RPF-WP-0043 | active | — | workplans/RPF-WP-0043-policy-nexus-argocd-onboarding.md | | workplan | RPF-WP-0044 | active | — | workplans/RPF-WP-0044-argocd-phase-b-adopt-existing-applications.md | | workplan | RPF-WP-0045 | active | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | +| workplan | RPF-WP-0046 | active | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | | task | RPF-WP-0015-T01 | done | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md | | task | RPF-WP-0015-T02 | wait | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md | | task | RPF-WP-0015-T03 | wait | — | workplans/RPF-WP-0015-audit-core-custody-and-recovery-coordination.md | @@ -85,3 +86,9 @@ | task | RPF-WP-0045-T04 | done | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | | task | RPF-WP-0045-T05 | cancel | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | | task | RPF-WP-0045-T06 | wait | — | workplans/RPF-WP-0045-eso-kubernetes-auth-activity-core-email-connect.md | +| task | RPF-WP-0046-T01 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | +| task | RPF-WP-0046-T02 | done | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | +| task | RPF-WP-0046-T03 | wait | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | +| task | RPF-WP-0046-T04 | wait | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | +| task | RPF-WP-0046-T05 | wait | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | +| task | RPF-WP-0046-T06 | todo | — | workplans/RPF-WP-0046-eso-database-token-renewal.md | diff --git a/credential-change-requests/CCR-2026-0029-activity-core-ops-run-worker-rein-aharness-railiance01.yaml b/credential-change-requests/CCR-2026-0029-activity-core-ops-run-worker-rein-aharness-railiance01.yaml new file mode 100644 index 0000000..70e8943 --- /dev/null +++ b/credential-change-requests/CCR-2026-0029-activity-core-ops-run-worker-rein-aharness-railiance01.yaml @@ -0,0 +1,101 @@ +id: CCR-2026-0029 +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-read +title: activity-core ops_run worker token for rein-aharness@railiance01 via the activity-core-eso store +status: proposed +created: '2026-09-23' +updated: '2026-09-23' +requester: + agent: activity-core + message_id: e87bdefc + reason: ACTIVITY-WP-0039-T02. secrets-engine (SECRETS-WP-0009-T03, + SECRETS-WP-0011-T04) asked that the ops_run worker tokens move from a + hand-generated value into OpenBao custody and be delivered by the + existing openbao-activity-core ClusterSecretStore. +review: + required: true + required_approvers: + - platform-operator + - activity-core-owner + comments: + - at: '2026-09-23' + reviewer: claude-code + decision: metadata_review_pending_owner_approval + comment: Both paths already hold values (founder-attended mint 2026-09-23, + ACTIVITY-WP-0039-T03). The consuming ExternalSecret + activity-core/actcore-ops-run-worker-tokens (activity-core + k8s/railiance/15-externalsecret-worker-tokens.yaml, not yet applied) + reads field token at exactly these two paths through store + openbao-activity-core. That store is on Kubernetes auth role + activity-core-eso (RPF-WP-0045). The change adds two exact data-read + paths to that role's policy. There is no wildcard, no metadata or list, + and no new role. +target: + domain: financials + tenant: activity-core + workload: activity-core-ops-run-worker-rein-aharness-railiance01 + environment: production + purpose: Deliver the bearer token of ops_run queue worker rein-aharness@railiance01 to the + activity-core runtime Secret through External Secrets. The companion lane is + CCR-2026-0030 (the other worker identity, same policy change). +openbao: + mount: platform + kv_path: platform/workloads/activity-core/ops-run-workers/rein-aharness-railiance01 + fields: + - token + policy_name: workload-kv-read-activity-core-eso + policy_file: openbao/policies/workload-kv-read-activity-core-eso.hcl + auth: + method: kubernetes + mount: kubernetes + role: activity-core-eso + bound_claims: + service_account_names: + - activity-core-eso + service_account_namespaces: + - activity-core + bound_claims_confirmed: true + policies: + - workload-kv-read-activity-core-eso +access_frontdoor: + type: external-secrets + catalog_id: activity-core-ops-run-worker-rein-aharness-railiance01 + readiness: pending-review + resolvable: false +delivery: + surface: external-secrets + target: ExternalSecret actcore-ops-run-worker-tokens merges ACTIVITY_CORE_WORKER_TOKEN + into Secret actcore-runtime-secret in namespace activity-core. +risk: + classification: high + notes: + - Each token authenticates one queue worker identity to activity-core. A + leaked token lets its holder claim ops_run work as that worker. + - Read is limited to the two exact paths. Other ops-run-workers paths stay + unreadable. + - A read-only lane for the metered worker's path is being catalogued + separately by secrets-engine. It is not granted by this CCR. +verification: + positive: + - ExternalSecret actcore-ops-run-worker-tokens reaches SecretSynced without + printing values. + negative: + - The activity-core-eso role cannot read a sibling path under + platform/data/workloads/activity-core/ops-run-workers/. + - Metadata and list on ops-run-workers are denied. + activation_conditions: + - Policy applied with platform-admin authority through + scripts/openbao-policy-sync.sh, guarded by the prior declared digest + 41f4278c3f62ff879575e62ef52071feaeb794fabd05868cb3ee40608cfd4785. + - Values provisioned directly in OpenBao (done, ACTIVITY-WP-0039-T03). + evidence: [] +lifecycle: + deactivate: Remove the two path blocks from workload-kv-read-activity-core-eso + and re-apply it. + rotate: activity-core re-mints the worker token at its path. ESO syncs it + within 1h, and the queue accepts the new token per ACTIVITY_CORE_WORKERS. + compromised: Re-mint the affected worker token, force-sync the + ExternalSecret, and record blast-radius notes. +state_hub: + workplan_id: ACTIVITY-WP-0039 diff --git a/credential-change-requests/CCR-2026-0030-activity-core-ops-run-worker-rein-aharness-metered-railiance01.yaml b/credential-change-requests/CCR-2026-0030-activity-core-ops-run-worker-rein-aharness-metered-railiance01.yaml new file mode 100644 index 0000000..9af37d7 --- /dev/null +++ b/credential-change-requests/CCR-2026-0030-activity-core-ops-run-worker-rein-aharness-metered-railiance01.yaml @@ -0,0 +1,101 @@ +id: CCR-2026-0030 +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-read +title: activity-core ops_run worker token for rein-aharness-metered@railiance01 via the activity-core-eso store +status: proposed +created: '2026-09-23' +updated: '2026-09-23' +requester: + agent: activity-core + message_id: e87bdefc + reason: ACTIVITY-WP-0039-T02. secrets-engine (SECRETS-WP-0009-T03, + SECRETS-WP-0011-T04) asked that the ops_run worker tokens move from a + hand-generated value into OpenBao custody and be delivered by the + existing openbao-activity-core ClusterSecretStore. +review: + required: true + required_approvers: + - platform-operator + - activity-core-owner + comments: + - at: '2026-09-23' + reviewer: claude-code + decision: metadata_review_pending_owner_approval + comment: Both paths already hold values (founder-attended mint 2026-09-23, + ACTIVITY-WP-0039-T03). The consuming ExternalSecret + activity-core/actcore-ops-run-worker-tokens (activity-core + k8s/railiance/15-externalsecret-worker-tokens.yaml, not yet applied) + reads field token at exactly these two paths through store + openbao-activity-core. That store is on Kubernetes auth role + activity-core-eso (RPF-WP-0045). The change adds two exact data-read + paths to that role's policy. There is no wildcard, no metadata or list, + and no new role. +target: + domain: financials + tenant: activity-core + workload: activity-core-ops-run-worker-rein-aharness-metered-railiance01 + environment: production + purpose: Deliver the bearer token of ops_run queue worker rein-aharness-metered@railiance01 to the + activity-core runtime Secret through External Secrets. The companion lane is + CCR-2026-0029 (the other worker identity, same policy change). +openbao: + mount: platform + kv_path: platform/workloads/activity-core/ops-run-workers/rein-aharness-metered-railiance01 + fields: + - token + policy_name: workload-kv-read-activity-core-eso + policy_file: openbao/policies/workload-kv-read-activity-core-eso.hcl + auth: + method: kubernetes + mount: kubernetes + role: activity-core-eso + bound_claims: + service_account_names: + - activity-core-eso + service_account_namespaces: + - activity-core + bound_claims_confirmed: true + policies: + - workload-kv-read-activity-core-eso +access_frontdoor: + type: external-secrets + catalog_id: activity-core-ops-run-worker-rein-aharness-metered-railiance01 + readiness: pending-review + resolvable: false +delivery: + surface: external-secrets + target: ExternalSecret actcore-ops-run-worker-tokens merges ACTIVITY_CORE_WORKER_TOKEN_METERED + into Secret actcore-runtime-secret in namespace activity-core. +risk: + classification: high + notes: + - Each token authenticates one queue worker identity to activity-core. A + leaked token lets its holder claim ops_run work as that worker. + - Read is limited to the two exact paths. Other ops-run-workers paths stay + unreadable. + - A read-only lane for the metered worker's path is being catalogued + separately by secrets-engine. It is not granted by this CCR. +verification: + positive: + - ExternalSecret actcore-ops-run-worker-tokens reaches SecretSynced without + printing values. + negative: + - The activity-core-eso role cannot read a sibling path under + platform/data/workloads/activity-core/ops-run-workers/. + - Metadata and list on ops-run-workers are denied. + activation_conditions: + - Policy applied with platform-admin authority through + scripts/openbao-policy-sync.sh, guarded by the prior declared digest + 41f4278c3f62ff879575e62ef52071feaeb794fabd05868cb3ee40608cfd4785. + - Values provisioned directly in OpenBao (done, ACTIVITY-WP-0039-T03). + evidence: [] +lifecycle: + deactivate: Remove the two path blocks from workload-kv-read-activity-core-eso + and re-apply it. + rotate: activity-core re-mints the worker token at its path. ESO syncs it + within 1h, and the queue accepts the new token per ACTIVITY_CORE_WORKERS. + compromised: Re-mint the affected worker token, force-sync the + ExternalSecret, and record blast-radius notes. +state_hub: + workplan_id: ACTIVITY-WP-0039 diff --git a/openbao/policies/workload-kv-read-activity-core-eso.hcl b/openbao/policies/workload-kv-read-activity-core-eso.hcl index e470532..76b147c 100644 --- a/openbao/policies/workload-kv-read-activity-core-eso.hcl +++ b/openbao/policies/workload-kv-read-activity-core-eso.hcl @@ -1,7 +1,7 @@ # RPF-WP-0045: ESO delivery for ClusterSecretStore openbao-activity-core via # Kubernetes auth role activity-core-eso (SA activity-core/activity-core-eso). -# Exactly the four KV paths the four activity-core ExternalSecrets read; data -# read only, no metadata/list/write. ESO also needs lookup-self and revoke-self. +# Exactly the KV paths the activity-core ExternalSecrets read; data read only, +# no metadata/list/write. ESO also needs lookup-self and revoke-self. path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets" { capabilities = ["read"] @@ -19,6 +19,16 @@ path "platform/data/workloads/railiance/backup/offsite-lane" { capabilities = ["read"] } +# CCR-2026-0029 and CCR-2026-0030 / ACTIVITY-WP-0039-T02: ops_run queue worker tokens, one path +# per worker identity, read by ExternalSecret actcore-ops-run-worker-tokens. +path "platform/data/workloads/activity-core/ops-run-workers/rein-aharness-railiance01" { + capabilities = ["read"] +} + +path "platform/data/workloads/activity-core/ops-run-workers/rein-aharness-metered-railiance01" { + capabilities = ["read"] +} + path "auth/token/lookup-self" { capabilities = ["read"] } diff --git a/scripts/openbao-policy-sync.sh b/scripts/openbao-policy-sync.sh new file mode 100755 index 0000000..91bee11 --- /dev/null +++ b/scripts/openbao-policy-sync.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +# Silent child for the governed attended login; results go to the receipt only. +set -euo pipefail +exec python3 "$(dirname "$0")/openbao_policy_sync.py" "$@" >/dev/null 2>&1 diff --git a/scripts/openbao_policy_sync.py b/scripts/openbao_policy_sync.py new file mode 100755 index 0000000..c4665a5 --- /dev/null +++ b/scripts/openbao_policy_sync.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Silent contained write of one declared ACL policy, guarded against drift. + +Writes openbao/policies/.hcl to sys/policy/ only when the live +rules still equal the previously declared version (--expect-live-sha256). If +the live rules already equal the file, nothing is written. Any other live +state is refused, so an undeclared live edit is never overwritten. Readback +must match. One non-secret receipt; no output. + + python3 scripts/openbao-attended-exec.py -- scripts/openbao-policy-sync.sh \\ + --policy --expect-live-sha256 --receipt +""" +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import re +import subprocess +import sys + +POLICY_DIR = Path(__file__).resolve().parents[1] / 'openbao/policies' +NAME = re.compile(r'^[a-z0-9][a-z0-9-]{1,80}$') + + +class Refused(Exception): + pass + + +def digest(text): + return hashlib.sha256(text.strip().encode('utf-8')).hexdigest() + + +def require_attended(): + if Path.home().parent.name != '.warden-attended-login' or os.getenv('BAO_TOKEN') or os.getenv('VAULT_TOKEN'): + raise Refused('attended_envelope_required') + + +def read_live(name): + result = subprocess.run(['bao', 'read', '-format=json', 'sys/policy/' + name], + capture_output=True, check=True, timeout=30) + return json.loads(result.stdout)['data']['rules'] + + +def write_live(name, path): + subprocess.run(['bao', 'policy', 'write', name, str(path)], + capture_output=True, check=True, timeout=30) + + +def sync(name, expect, read=read_live, write=write_live): + path = POLICY_DIR / (name + '.hcl') + declared = digest(path.read_text(encoding='utf-8')) + live = digest(read(name)) + if live == declared: + return {'status': 'already_current', 'changed': False, 'live_sha256': live} + if live != expect: + raise Refused('live_policy_drifted') + write(name, path) + after = digest(read(name)) + if after != declared: + raise Refused('readback_mismatch') + return {'status': 'applied', 'changed': True, 'previous_sha256': live, 'live_sha256': after} + + +def write_receipt(path, name, **body): + fd = os.open(Path(path), os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + body = { + 'schema': 'railiance-platform.openbao-policy-sync.v1', + 'observed_at': datetime.now(timezone.utc).isoformat(), + 'policy_name': name, 'credential_values_emitted': False, **body, + } + with os.fdopen(fd, 'w', encoding='utf-8') as handle: + json.dump(body, handle, indent=2, sort_keys=True) + handle.write('\n') + + +def classify(error): + if isinstance(error, Refused): + return str(error) + if isinstance(error, subprocess.CalledProcessError): + return 'bao_write_failed' if 'policy' in error.cmd else 'bao_read_failed' + return 'contained_operation_failed' + + +def parse(argv): + opts, args = {}, list(argv) + while args: + if args[0] in ('--policy', '--expect-live-sha256', '--receipt') and len(args) > 1: + opts[args[0]] = args[1] + args = args[2:] + else: + raise SystemExit(2) + if set(opts) != {'--policy', '--expect-live-sha256', '--receipt'} or not NAME.match(opts['--policy']): + raise SystemExit(2) + if not (POLICY_DIR / (opts['--policy'] + '.hcl')).is_file(): + raise SystemExit(2) + return opts['--policy'], opts['--expect-live-sha256'], opts['--receipt'] + + +def main(argv): + name, expect, receipt = parse(argv) + try: + require_attended() + result = sync(name, expect) + write_receipt(receipt, name, declared_sha256=digest((POLICY_DIR / (name + '.hcl')).read_text()), **result) + return 0 + except Exception as error: + try: + write_receipt(receipt, name, status=classify(error), changed=False) + except Exception: + pass + return 1 + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/test_openbao_policy_sync.py b/tests/test_openbao_policy_sync.py new file mode 100644 index 0000000..204066b --- /dev/null +++ b/tests/test_openbao_policy_sync.py @@ -0,0 +1,65 @@ +import importlib.util +import json +from pathlib import Path + +import pytest + +spec = importlib.util.spec_from_file_location( + 'sync', Path(__file__).resolve().parents[1] / 'scripts/openbao_policy_sync.py') +m = importlib.util.module_from_spec(spec) +spec.loader.exec_module(m) + +NAME = 'workload-kv-read-activity-core-eso' +DECLARED = (m.POLICY_DIR / (NAME + '.hcl')).read_text() + + +def store(initial): + live = {'rules': initial} + writes = [] + + def write(name, path): + writes.append(name) + live['rules'] = Path(path).read_text() + return live, writes, (lambda name: live['rules']), write + + +def test_applies_when_live_is_the_expected_prior_version(): + live, writes, read, write = store('path "old" {}') + result = m.sync(NAME, m.digest('path "old" {}'), read, write) + assert result['status'] == 'applied' and writes == [NAME] + assert m.digest(live['rules']) == m.digest(DECLARED) + + +def test_already_current_writes_nothing(): + _, writes, read, write = store(DECLARED) + assert m.sync(NAME, 'unused', read, write)['status'] == 'already_current' + assert writes == [] + + +def test_refuses_undeclared_drift(): + _, writes, read, write = store('path "someone-else" {}') + with pytest.raises(m.Refused, match='live_policy_drifted'): + m.sync(NAME, m.digest('path "old" {}'), read, write) + assert writes == [] + + +def test_readback_mismatch_is_refused(): + live = {'rules': 'path "old" {}'} + with pytest.raises(m.Refused, match='readback_mismatch'): + m.sync(NAME, m.digest('path "old" {}'), lambda n: live['rules'], lambda n, p: None) + + +def test_new_paths_are_exact_and_read_only(): + for worker in ('rein-aharness-railiance01', 'rein-aharness-metered-railiance01'): + block = 'path "platform/data/workloads/activity-core/ops-run-workers/%s" {\n capabilities = ["read"]\n}' % worker + assert block in DECLARED + assert 'ops-run-workers/*' not in DECLARED and '"list"' not in DECLARED + + +def test_refuses_outside_envelope_and_bad_args(tmp_path, monkeypatch): + monkeypatch.setenv('HOME', str(tmp_path)) + receipt = tmp_path / 'r.json' + assert m.main(['--policy', NAME, '--expect-live-sha256', 'x', '--receipt', str(receipt)]) == 1 + assert json.loads(receipt.read_text())['status'] == 'attended_envelope_required' + with pytest.raises(SystemExit): + m.parse(['--policy', '../etc', '--expect-live-sha256', 'x', '--receipt', 'r']) diff --git a/workplans/RPF-WP-0046-eso-database-token-renewal.md b/workplans/RPF-WP-0046-eso-database-token-renewal.md index 2c930d8..79e5402 100644 --- a/workplans/RPF-WP-0046-eso-database-token-renewal.md +++ b/workplans/RPF-WP-0046-eso-database-token-renewal.md @@ -10,6 +10,7 @@ topic_slug: railiance created: "2026-09-23" updated: "2026-09-23" related: [RPF-WP-0045, RPF-WP-0037, RPF-WP-0036] +state_hub_workstream_id: "cdb309e0-adb4-58aa-9c98-5ea270780e8d" --- ## Problem @@ -56,6 +57,7 @@ Design: id: RPF-WP-0046-T01 status: done priority: high +state_hub_task_id: "25592a83-d2e5-5aa2-b23e-5d563a621f19" ``` `argocd/platform-addons/eso-token-renewer/` holds `renew.py`, the @@ -73,6 +75,7 @@ AppProject is applied by hand, so the git change alone is not live. Tests: id: RPF-WP-0046-T02 status: done priority: high +state_hub_task_id: "a318792f-82ba-568b-8633-fba3513aa978" ``` The script is `scripts/openbao-eso-db-token-periodic-attended.sh`. It is @@ -87,6 +90,7 @@ parity with the CronJob mounts is tested. id: RPF-WP-0046-T03 status: wait priority: high +state_hub_task_id: "211bc548-34af-5f3a-ab2c-bf29d33f97ad" ``` This step is `ADMINISTER @ realm:kubernetes/railiance01`, `activation=APPROVED`, @@ -109,6 +113,7 @@ ExternalSecrets are `SecretSynced`. id: RPF-WP-0046-T04 status: wait priority: high +state_hub_task_id: "28726b1b-3777-5945-859b-9e563b12fee6" ``` 1. Apply `argocd/railiance01/bootstrap/02-railiance-platform-addons-project.yaml` @@ -125,6 +130,7 @@ priority: high id: RPF-WP-0046-T05 status: wait priority: high +state_hub_task_id: "acf49bee-e40a-533c-b1f0-4152c989211d" ``` The replaced 768h tokens still expire around 2026-10-25T17:00Z, and they @@ -144,6 +150,7 @@ ready. id: RPF-WP-0046-T06 status: todo priority: medium +state_hub_task_id: "2f528e56-ebf0-547e-8d4f-3cb34c348d46" ``` - rapp-postgres: the 768h mint scripts (`apply-*-eso-token*.sh`) would undo