diff --git a/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml b/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml index 7f27aef..bccd870 100644 --- a/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml +++ b/argocd/platform-addons/openbao-secretstore/openbao-forgejo.clustersecretstore.yaml @@ -1,4 +1,8 @@ -# RPF-WP-0037: exact namespace/SA/audience Kubernetes auth; no static token. +# Forgejo reads SMTP credentials from the private railiance01 OpenBao service. +# +# Prereq: Secret external-secrets/openbao-forgejo-eso-token (key: token) with a +# policy-limited OpenBao token that can read platform/workloads/forgejo/forgejo-mailer. +# Bootstrap: railiance-apps make forgejo-openbao-eso-token-apply apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: @@ -13,14 +17,10 @@ spec: path: platform version: v2 auth: - kubernetes: - mountPath: kubernetes - role: forgejo-mailer-eso - serviceAccountRef: - name: forgejo-mailer-eso - namespace: forgejo - audiences: - - openbao + tokenSecretRef: + name: openbao-forgejo-eso-token + namespace: external-secrets + key: token conditions: - - namespaces: - - forgejo + - namespaces: + - forgejo diff --git a/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml b/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml index 57ff19e..b821ab4 100644 --- a/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml +++ b/argocd/platform-addons/openbao-secretstore/openbao-reuse.clustersecretstore.yaml @@ -1,4 +1,11 @@ -# RPF-WP-0037: exact namespace/SA/audience Kubernetes auth; no static token. +# reuse-surface uses the private OpenBao service on railiance01. Public +# bao.coulomb.social remains the operator/API address and is not a workload +# runtime dependency. +# +# Prereq: Secret external-secrets/openbao-reuse-eso-token (key: token) with a +# policy-limited OpenBao token that can read +# platform/workloads/reuse/reuse-surface/runtime-secrets. +# Bootstrap: railiance-apps make reuse-openbao-eso-token-apply apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: @@ -13,14 +20,10 @@ spec: path: platform version: v2 auth: - kubernetes: - mountPath: kubernetes - role: reuse-surface-runtime-eso - serviceAccountRef: - name: reuse-surface-eso - namespace: reuse - audiences: - - openbao + tokenSecretRef: + name: openbao-reuse-eso-token + namespace: external-secrets + key: token conditions: - - namespaces: - - reuse + - namespaces: + - reuse diff --git a/argocd/platform-addons/openbao-secretstore/openbao-target-revenue.clustersecretstore.yaml b/argocd/platform-addons/openbao-secretstore/openbao-target-revenue.clustersecretstore.yaml index 337de4c..c4fba08 100644 --- a/argocd/platform-addons/openbao-secretstore/openbao-target-revenue.clustersecretstore.yaml +++ b/argocd/platform-addons/openbao-secretstore/openbao-target-revenue.clustersecretstore.yaml @@ -1,4 +1,9 @@ -# RPF-WP-0037: exact namespace/SA/audience Kubernetes auth; no static token. +# target-revenue Trust Service runtime secrets on railiance01 (WP-0011). +# Pattern matches openbao-reuse / openbao-activity-core: token auth to the +# in-cluster OpenBao service, store limited to the target-revenue namespace. +# +# Prereq: Secret external-secrets/openbao-target-revenue-eso-token (key: token) +# Bootstrap: target-revenue scripts/openbao-eso-token-apply.sh apiVersion: external-secrets.io/v1beta1 kind: ClusterSecretStore metadata: @@ -14,14 +19,10 @@ spec: path: platform version: v2 auth: - kubernetes: - mountPath: kubernetes - role: target-revenue-runtime-eso - serviceAccountRef: - name: target-revenue-eso - namespace: target-revenue - audiences: - - openbao + tokenSecretRef: + name: openbao-target-revenue-eso-token + namespace: external-secrets + key: token conditions: - - namespaces: - - target-revenue + - namespaces: + - target-revenue diff --git a/openbao/eso-auth-recovery/lanes.json b/openbao/eso-auth-recovery/lanes.json deleted file mode 100644 index 594c130..0000000 --- a/openbao/eso-auth-recovery/lanes.json +++ /dev/null @@ -1,29 +0,0 @@ -[ - { - "namespace": "forgejo", - "name": "forgejo-mailer", - "store": "openbao-forgejo", - "service_account": "forgejo-mailer-eso", - "kv_path": "platform/data/workloads/forgejo/forgejo-mailer", - "policy": "workload-kv-read-forgejo-mailer-eso", - "role": "forgejo-mailer-eso" - }, - { - "namespace": "reuse", - "name": "reuse-surface-runtime", - "store": "openbao-reuse", - "service_account": "reuse-surface-eso", - "kv_path": "platform/data/workloads/reuse/reuse-surface/runtime-secrets", - "policy": "workload-kv-read-reuse-surface-runtime-eso", - "role": "reuse-surface-runtime-eso" - }, - { - "namespace": "target-revenue", - "name": "target-revenue-runtime", - "store": "openbao-target-revenue", - "service_account": "target-revenue-eso", - "kv_path": "platform/data/workloads/target-revenue/runtime-secrets", - "policy": "workload-kv-read-target-revenue-runtime-eso", - "role": "target-revenue-runtime-eso" - } -] diff --git a/openbao/eso-auth-recovery/serviceaccounts.yaml b/openbao/eso-auth-recovery/serviceaccounts.yaml deleted file mode 100644 index 18b1a51..0000000 --- a/openbao/eso-auth-recovery/serviceaccounts.yaml +++ /dev/null @@ -1,20 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: forgejo-mailer-eso - namespace: forgejo -automountServiceAccountToken: false ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: reuse-surface-eso - namespace: reuse -automountServiceAccountToken: false ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: target-revenue-eso - namespace: target-revenue -automountServiceAccountToken: false diff --git a/openbao/policies/workload-kv-read-forgejo-mailer-eso.hcl b/openbao/policies/workload-kv-read-forgejo-mailer-eso.hcl deleted file mode 100644 index 477b70a..0000000 --- a/openbao/policies/workload-kv-read-forgejo-mailer-eso.hcl +++ /dev/null @@ -1,11 +0,0 @@ -path "platform/data/workloads/forgejo/forgejo-mailer" { - capabilities = ["read"] -} - -path "auth/token/lookup-self" { - capabilities = ["read"] -} - -path "auth/token/revoke-self" { - capabilities = ["update"] -} diff --git a/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl b/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl deleted file mode 100644 index 93739c6..0000000 --- a/openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl +++ /dev/null @@ -1,11 +0,0 @@ -path "platform/data/workloads/reuse/reuse-surface/runtime-secrets" { - capabilities = ["read"] -} - -path "auth/token/lookup-self" { - capabilities = ["read"] -} - -path "auth/token/revoke-self" { - capabilities = ["update"] -} diff --git a/openbao/policies/workload-kv-read-target-revenue-runtime-eso.hcl b/openbao/policies/workload-kv-read-target-revenue-runtime-eso.hcl deleted file mode 100644 index 44b9523..0000000 --- a/openbao/policies/workload-kv-read-target-revenue-runtime-eso.hcl +++ /dev/null @@ -1,11 +0,0 @@ -path "platform/data/workloads/target-revenue/runtime-secrets" { - capabilities = ["read"] -} - -path "auth/token/lookup-self" { - capabilities = ["read"] -} - -path "auth/token/revoke-self" { - capabilities = ["update"] -} diff --git a/scripts/repair_eso_kubernetes_auth.py b/scripts/repair_eso_kubernetes_auth.py deleted file mode 100644 index 1d30888..0000000 --- a/scripts/repair_eso_kubernetes_auth.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -"""Attended exact-lane ESO auth repair; no credential values in output/evidence.""" -import argparse -import base64 -import json -import os -from pathlib import Path -import time - -import yaml -from state_hub_preflight_lane import ROOT, LaneError, assert_cluster, bao, command, data, capabilities, revoke - - -def require(condition, label): - if not condition: - raise LaneError(label) - - -def role_payload(lane): - return {'bound_service_account_names': [lane['service_account']], - 'bound_service_account_namespaces': [lane['namespace']], - 'token_policies': [lane['policy']], 'audience': 'openbao', - 'token_ttl': '15m', 'token_max_ttl': '15m', - 'token_explicit_max_ttl': '15m', 'token_no_default_policy': True} - - -def check_role(actual, lane): - for key, value in role_payload(lane).items(): - require(actual.get(key) == (900 if value == '15m' else value), 'role_drift') - require(not actual.get('token_period') and not actual.get('bound_service_account_namespace_selector'), 'role_drift') - - -def compare_custody(kube, lane): - es = data(command(kube + ['-n', lane['namespace'], 'get', 'externalsecret', lane['name'], '-o', 'json'])) - require(es['spec']['secretStoreRef'] == {'kind': 'ClusterSecretStore', 'name': lane['store']}, 'external_secret_store_drift') - target = es['spec']['target'].get('name', lane['name']) - deployed = data(command(kube + ['-n', lane['namespace'], 'get', 'secret', target, '-o', 'json'])) - native = data(bao(['read', '-format=json', lane['kv_path']]))['data'] - for entry in es['spec']['data']: - ref = entry['remoteRef'] - require('platform/data/' + ref['key'] == lane['kv_path'], 'remote_path_drift') - require(str(native['data'][ref['property']]).encode() == base64.b64decode(deployed['data'][entry['secretKey']]), 'custody_disagrees_stop_before_resync') - return native['metadata']['version'] - - -def verify_login(kube, lane, row): - def login(sa, audience, negative=False, namespace=None): - jwt = command(kube + ['-n', namespace or lane['namespace'], 'create', 'token', sa, '--audience=' + audience, '--duration=10m']).stdout.decode().strip() - result = bao(['write', '-format=json', 'auth/kubernetes/login', '-'], payload={'role': lane['role'], 'jwt': jwt}, allow_failure=negative) - if negative: - if result.returncode == 0: - revoke(data(result)['auth']['client_token']) - raise LaneError('negative_login_succeeded') - require(b'403' in result.stderr or b'400' in result.stderr, 'negative_login_inconclusive') - return None - return data(result)['auth'] - auth = login(lane['service_account'], 'openbao') - token = auth['client_token'] - try: - require(auth['token_policies'] == [lane['policy']] and 0 < auth['lease_duration'] <= 900, 'effective_policy_or_ttl_drift') - paths = [lane['kv_path'], lane['kv_path'].replace('/data/', '/metadata/'), - lane['kv_path'] + '-unrelated', 'auth/token/create', - 'auth/token/lookup-self', 'auth/token/revoke-self'] - caps = capabilities(token, paths) - require(caps[paths[0]] == ['read'], 'read_or_write_scope_drift') - require(all(caps[p] == ['deny'] for p in paths[1:4]), 'negative_scope_failed') - require(caps[paths[4]] == ['read'] and caps[paths[5]] == ['update'], 'token_self_scope_failed') - bao(['read', '-format=json', lane['kv_path']], token=token) - bao(['token', 'lookup', '-format=json'], token=token) - finally: - revoke(token) - login('default', 'openbao', negative=True) - login(lane['service_account'], 'wrong-audience', negative=True) - command(kube + ['-n', 'default', 'create', 'serviceaccount', lane['service_account']]) - try: - login(lane['service_account'], 'openbao', negative=True, namespace='default') - finally: - command(kube + ['-n', 'default', 'delete', 'serviceaccount', lane['service_account']]) - row.update(exact_read=True, secret_write_and_sibling_denied=True, wrong_sa_denied=True, wrong_namespace_denied=True, wrong_audience_denied=True, bounded_ttl=True) - - -def run(args, receipt): - kube = ['kubectl', '--kubeconfig', args.kubeconfig] - assert_cluster(kube) - ident = data(bao(['token', 'lookup', '-format=json']))['data'] - require('platform-admin' in ident['policies'] and 'root' not in ident['policies'], 'attended_platform_admin_required') - lanes = json.loads((ROOT / 'openbao/eso-auth-recovery/lanes.json').read_text()) - # Refuse all mutations if any lane would replace divergent deployed key material. - for lane in lanes: - compare_custody(kube, lane) - if args.action == 'apply': - command(kube + ['apply', '-f', str(ROOT / 'openbao/eso-auth-recovery/serviceaccounts.yaml')]) - for lane in lanes: - row = {'name': lane['name'], 'namespace': lane['namespace'], 'status': 'pending'} - receipt['lanes'].append(row) - source = ROOT / 'argocd/platform-addons/openbao-secretstore' / (lane['store'] + '.clustersecretstore.yaml') - desired = yaml.safe_load(source.read_text()) - live = data(command(kube + ['get', 'clustersecretstore', lane['store'], '-o', 'json'])) - expected_old = {'tokenSecretRef': {'name': lane['store'] + '-eso-token', 'namespace': 'external-secrets', 'key': 'token'}} - a, b = live['spec']['provider']['vault'], desired['spec']['provider']['vault'] - require({k:v for k,v in a.items() if k != 'auth'} == {k:v for k,v in b.items() if k != 'auth'} and live['spec']['conditions'] == desired['spec']['conditions'], 'store_scope_drift') - require(a['auth'] in [expected_old, b['auth']], 'store_auth_drift') - policy = (ROOT / 'openbao/policies' / (lane['policy'] + '.hcl')).read_text() - if args.action == 'apply': - old = bao(['read', '-format=json', 'sys/policies/acl/' + lane['policy']], allow_failure=True) - if old.returncode == 0: - require(data(old)['data']['policy'] == policy, 'policy_drift') - else: - require(b'404' in old.stderr or b'No value found' in old.stderr, 'policy_absence_unproven') - old_role = bao(['read', '-format=json', 'auth/kubernetes/role/' + lane['role']], allow_failure=True) - if old_role.returncode == 0: - check_role(data(old_role)['data'], lane) - else: - require(b'404' in old_role.stderr or b'No value found' in old_role.stderr, 'role_absence_unproven') - bao(['write', 'sys/policies/acl/' + lane['policy'], '-'], payload={'policy': policy}) - bao(['write', 'auth/kubernetes/role/' + lane['role'], '-'], payload=role_payload(lane)) - check_role(data(bao(['read', '-format=json', 'auth/kubernetes/role/' + lane['role']]))['data'], lane) - verify_login(kube, lane, row) - row['kv_version'] = compare_custody(kube, lane) - if args.action == 'apply': - # JSON Patch tests the observed spec atomically; replace only authentication. - patch = [{'op':'test','path':'/spec','value':live['spec']}, {'op':'replace','path':'/spec/provider/vault/auth','value':b['auth']}] - command(kube + ['patch', 'clustersecretstore', lane['store'], '--type=json', '-p', json.dumps(patch)]) - command(kube + ['annotate', 'clustersecretstore', lane['store'], 'force-sync=' + str(time.time_ns()), '--overwrite']) - command(kube + ['-n', lane['namespace'], 'annotate', 'externalsecret', lane['name'], 'force-sync=' + str(time.time_ns()), '--overwrite']) - command(kube + ['wait', '--for=condition=Ready', 'clustersecretstore/' + lane['store'], '--timeout=45s']) - command(kube + ['-n', lane['namespace'], 'wait', '--for=condition=Ready', 'externalsecret/' + lane['name'], '--timeout=45s']) - compare_custody(kube, lane) - row.update(status='passed', unchanged_workload_values=True, store_ready=True, external_secret_ready=True) - receipt['status'] = 'passed' - - -def main(): - p = argparse.ArgumentParser(description=__doc__) - p.add_argument('action', choices=['apply', 'verify']) - p.add_argument('--kubeconfig', required=True) - p.add_argument('--receipt', required=True) - p.add_argument('--confirm', required=True) - args = p.parse_args() - receipt = {'schema':'platform.eso-kubernetes-recovery.v1','status':'failed','lanes':[]} - fd = os.open(args.receipt, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) - try: - require(args.confirm == 'APPLY RPF-WP-0037', 'confirmation_mismatch') - run(args, receipt) - except Exception as exc: - receipt['error'] = str(exc) if isinstance(exc, LaneError) else 'internal_error' - finally: - with os.fdopen(fd, 'w') as out: - json.dump(receipt, out, indent=2) - out.write('\n') - return 0 if receipt['status'] == 'passed' else 1 - - -if __name__ == '__main__': - raise SystemExit(main()) diff --git a/tests/test_eso_auth_recovery.py b/tests/test_eso_auth_recovery.py deleted file mode 100644 index 50ae08f..0000000 --- a/tests/test_eso_auth_recovery.py +++ /dev/null @@ -1,44 +0,0 @@ -import importlib.util -import json -from pathlib import Path -import sys -from types import SimpleNamespace -import unittest -from unittest.mock import patch - -ROOT = Path(__file__).resolve().parents[1] -sys.path.insert(0, str(ROOT / 'scripts')) -import repair_eso_kubernetes_auth as repair - - -class EsoRecoveryTests(unittest.TestCase): - def setUp(self): - self.lane = json.loads((ROOT / 'openbao/eso-auth-recovery/lanes.json').read_text())[0] - - def test_role_rejects_wildcard_namespace_added_policy_and_unbounded_ttl(self): - native = {k:900 if v == '15m' else v for k,v in repair.role_payload(self.lane).items()} - repair.check_role(native, self.lane) - for key,value in [('bound_service_account_namespaces',['*']),('token_policies',['root']),('token_explicit_max_ttl',0),('audience','other')]: - with self.assertRaises(repair.LaneError): - repair.check_role(dict(native, **{key:value}), self.lane) - - def test_custody_disagreement_stops_before_serviceaccount_or_auth_write(self): - identity = SimpleNamespace(stdout=json.dumps({'data':{'policies':['platform-admin']}}).encode()) - with patch.object(repair,'assert_cluster'), patch.object(repair,'bao',return_value=identity) as bao, patch.object(repair,'command') as kube, patch.object(repair,'compare_custody',side_effect=repair.LaneError('custody_disagrees')): - with self.assertRaises(repair.LaneError): - repair.run(SimpleNamespace(action='apply',kubeconfig='/fixture'), {'lanes':[]}) - kube.assert_not_called() - self.assertEqual(bao.call_count,1) - - def test_no_credential_values_or_parent_access_in_generated_policies(self): - for lane in json.loads((ROOT / 'openbao/eso-auth-recovery/lanes.json').read_text()): - policy=(ROOT / 'openbao/policies' / (lane['policy']+'.hcl')).read_text() - self.assertNotIn('*',policy) - self.assertNotIn('/metadata/',policy) - self.assertNotIn('"list"',policy) - self.assertEqual(policy.count('path "'),3) - self.assertIn('path "'+lane['kv_path']+'"',policy) - - -if __name__=='__main__': - unittest.main() diff --git a/workplans/RPF-WP-0037-eso-static-token-recovery.md b/workplans/RPF-WP-0037-eso-static-token-recovery.md deleted file mode 100644 index 459f086..0000000 --- a/workplans/RPF-WP-0037-eso-static-token-recovery.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -id: RPF-WP-0037 -type: workplan -title: "Recover three active ESO lanes from static-token authentication failure" -domain: financials -repo: railiance-platform -status: active -owner: codex -created: "2026-09-05" -updated: "2026-09-05" -state_hub_workstream_id: "df076911-18b4-5731-99a8-2ecb3c3cc5b3" ---- - -User authorized continuing with the identified delivery failures. Live metadata -confirms active Forgejo, reuse-surface and target-revenue consumers. All three -ClusterSecretStores fail OpenBao token lookup-self with 403. This plan repairs -delivery authentication; it does not rotate application credentials or restart -consumers. RPF-WP-0036-T04 retains the separate telemetry acceptance gate. - -## Diagnose authentication and compare custody without exposing values - -```task -id: RPF-WP-0037-T01 -status: progress -priority: high -state_hub_task_id: "e0e7ff05-01bf-58db-a18e-4c5e48a697fc" -``` - -Use attended platform-admin and pinned cluster access. Capture token validity, -policy coordinates and boolean equality between native KV data and the existing -consumer Secret. No values, tokens, hashes or fingerprints in evidence. Stop -before any resync if custody disagrees with the deployed Secret. - -## Replace static authentication with exact Kubernetes bindings - -```task -id: RPF-WP-0037-T02 -status: todo -priority: high -state_hub_task_id: "01dd799d-f967-57d6-9cf0-9b01971e5bf1" -``` - -Stage dedicated namespace/SA/audience-bound roles with 15-minute maximum TTL, -no default policy and exact KV data read plus token self-lookup/revocation. -Keep namespace restrictions and existing delivery paths. Refuse live drift. -Validate positive read and wrong-identity/sibling/write denial before changing -each store. Persist source before apply. Do not overwrite workload key material. - -## Verify sustained delivery and record closure - -```task -id: RPF-WP-0037-T03 -status: todo -priority: high -state_hub_task_id: "ee47707c-289b-5bea-a2ad-fdf40fed7034" -``` - -Require Ready stores and ExternalSecrets, a second successful forced reconcile, -unchanged deployed key material and healthy consumers. Record safe receipts and -retire obsolete bootstrap instructions. Remove obsolete static token references -from stores; retain old token Secret objects until their invalidity is proven. -Commit and synchronize file-backed work and report any remaining gates.