diff --git a/docs/evidence/2026-09-14-ccr0019-delivery-check.json b/docs/evidence/2026-09-14-ccr0019-delivery-check.json new file mode 100644 index 0000000..db44dde --- /dev/null +++ b/docs/evidence/2026-09-14-ccr0019-delivery-check.json @@ -0,0 +1,26 @@ +{ + "observed_at": "2026-09-13T23:44:04.757286+00:00", + "ccr_id": "CCR-2026-0019", + "status": "passed", + "approval_mutations": false, + "checks": { + "private_delivery_existing_version_1": true, + "approval:read": { + "native_claim_http_status": 404, + "profile_checked": true + }, + "approval:consume": { + "native_claim_http_status": 403, + "profile_checked": true + } + }, + "cleanup": true, + "warden_session": "exited_0_after_self_revocation_and_helper_cleanup", + "approval_engine_pod_uid": "7b8c05d9-f1d5-403d-abb3-2250bff9db5e", + "approval_engine_image": "sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49", + "limits": [ + "Read request used a fresh nonexistent approval id; 404 proves authenticated routing, not a usable claim.", + "Consume-only token was denied the read route; no real approval was consumed.", + "Wrong-group human login remains untested." + ] +} diff --git a/docs/evidence/2026-09-14-ccr0019-reader-preflight.json b/docs/evidence/2026-09-14-ccr0019-reader-preflight.json new file mode 100644 index 0000000..96e256b --- /dev/null +++ b/docs/evidence/2026-09-14-ccr0019-reader-preflight.json @@ -0,0 +1,34 @@ +{ + "observed_at": "2026-09-13T23:44:04.757314+00:00", + "ccr_id": "CCR-2026-0019", + "secret_values_read": false, + "status": "passed", + "checks": { + "platform/data/workloads/secrets-engine/approval-client": [ + "read" + ], + "platform/metadata/workloads/secrets-engine/approval-client": [ + "read" + ], + "platform/data/workloads/approval-engine/operator-client": [ + "deny" + ], + "platform/metadata/workloads/secrets-engine": [ + "deny" + ], + "platform/metadata/workloads": [ + "deny" + ], + "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets": [ + "deny" + ], + "sys/policies/acl/workload-kv-read-secrets-engine-approval-client": [ + "deny" + ], + "auth/netkingdom/role/secrets-engine-approval-client-workload-kv-read": [ + "deny" + ] + }, + "identity_policy_scope_verified": true, + "warden_session": "exited_0_after_self_revocation_and_helper_cleanup" +} diff --git a/scripts/approval-client-delivery-check.py b/scripts/approval-client-delivery-check.py new file mode 100644 index 0000000..fe53d3a --- /dev/null +++ b/scripts/approval-client-delivery-check.py @@ -0,0 +1,106 @@ +"""Silent bounded CCR-2026-0019 acceptance; never creates or consumes approvals.""" +import json +import os +from pathlib import Path +import signal +import stat +import tempfile +from datetime import datetime, timezone +from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler +from urllib.error import HTTPError +from uuid import uuid4 + +import importlib.util +_spec = importlib.util.spec_from_file_location('reader_preflight', Path(__file__).with_name('approval-client-reader-preflight.py')) +preflight = importlib.util.module_from_spec(_spec) +_spec.loader.exec_module(preflight) + +RECEIPT = Path('/home/worsch/railiance-platform/docs/evidence/2026-09-14-ccr0019-delivery-check.json') +class NoRedirect(HTTPRedirectHandler): + def redirect_request(self, *args, **kwargs): + return None + +def transport(request, *, timeout): + return build_opener(ProxyHandler({}), NoRedirect()).open(request, timeout=timeout) + +def private(path, mode, directory=False): + info = path.lstat() + if stat.S_IMODE(info.st_mode) != mode or info.st_uid != os.getuid(): + raise ValueError('private_path_required') + if not (stat.S_ISDIR(info.st_mode) if directory else stat.S_ISREG(info.st_mode)): + raise ValueError('private_path_required') + +def main(): + receipt = {'observed_at':datetime.now(timezone.utc).isoformat(), 'ccr_id':'CCR-2026-0019', + 'status':'refused', 'approval_mutations':False, 'checks':{}, 'cleanup':False} + directory = None + secret_file = None + try: + preflight.main() + from secrets_engine.approval_auth import KeyCapeApprovalAuthConfig + from secrets_engine.service_auth import KeyCapeServiceAuthProvider + runtime = Path('/run/user') / str(os.getuid()) + private(runtime, 0o700, True) + if runtime.resolve() != runtime: + raise ValueError('private_path_required') + result = preflight.subprocess.run(['findmnt','-n','-o','FSTYPE','-T',str(runtime)],capture_output=True,text=True,timeout=10) + if result.returncode or result.stdout.strip() != 'tmpfs': + raise ValueError('runtime_tmpfs_required') + directory = Path(tempfile.mkdtemp(prefix='secrets-approval-',dir=runtime)) + private(directory, 0o700, True) + secret_file = directory/'client-secret' + helper = Path.home()/'.vault-token' + private(helper, 0o600) + token = helper.read_text().strip() + req = Request('https://bao.coulomb.social/v1/platform/data/workloads/secrets-engine/approval-client?version=1',headers={'X-Vault-Token':token}) + with transport(req,timeout=20) as response: + data = response.read(65537) + if len(data)>65536: + raise ValueError('custody_response_invalid') + data=json.loads(data) + if data['data']['metadata']['version'] != 1: + raise ValueError('custody_response_invalid') + value=data['data']['data']['CLIENT_SECRET'] + if not isinstance(value,str) or not value or len(value)>16384: + raise ValueError('custody_response_invalid') + fd=os.open(secret_file,os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o600) + with os.fdopen(fd,'w') as stream: + stream.write(value) + del value, data, token, req + private(secret_file,0o600) + receipt['checks']['private_delivery_existing_version_1']=True + approval_id=str(uuid4()) + for scope in ('approval:read','approval:consume'): + config=KeyCapeApprovalAuthConfig(token_url='https://kc.coulomb.social/token',issuer='https://kc.coulomb.social',client_secret_file=secret_file,scope=scope) + jwt=KeyCapeServiceAuthProvider(config,transport=transport).exchange() + request=Request('http://127.0.0.1:18281/v1/approvals/'+approval_id+'/claim',headers={'Authorization':'Bearer '+jwt.token}) + try: + with transport(request,timeout=20) as response: + status=response.status + except HTTPError as error: + status=error.code + error.close() + expected=404 if scope=='approval:read' else 403 + if status != expected: + raise ValueError('native_scope_check_failed') + receipt['checks'][scope]={'native_claim_http_status':status,'profile_checked':True} + del jwt, request + receipt['status']='passed' + except Exception: + receipt['failure']='delivery_check_failed' + raise + finally: + if secret_file is not None and secret_file.exists(): + secret_file.unlink() + if directory is not None: + directory.rmdir() + receipt['cleanup']=directory is None or not directory.exists() + RECEIPT.write_text(json.dumps(receipt,indent=2)+'\n') + +if __name__=='__main__': + for sig in (signal.SIGINT,signal.SIGTERM): + signal.signal(sig,lambda *_: (_ for _ in ()).throw(SystemExit(130))) + try: + main() + except Exception: + raise SystemExit(1) from None diff --git a/scripts/approval-client-reader-preflight.py b/scripts/approval-client-reader-preflight.py new file mode 100644 index 0000000..7821841 --- /dev/null +++ b/scripts/approval-client-reader-preflight.py @@ -0,0 +1,62 @@ +"""Silent exact-reader metadata preflight under the Warden login envelope.""" +import json +import os +from pathlib import Path +import subprocess +from datetime import datetime, timezone + +POLICY = 'workload-kv-read-secrets-engine-approval-client' +DATA = 'platform/data/workloads/secrets-engine/approval-client' +META = 'platform/metadata/workloads/secrets-engine/approval-client' +EXPECTED = { + DATA: ['read'], META: ['read'], + 'platform/data/workloads/approval-engine/operator-client': ['deny'], + 'platform/metadata/workloads/secrets-engine': ['deny'], + 'platform/metadata/workloads': ['deny'], + 'platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets': ['deny'], + 'sys/policies/acl/' + POLICY: ['deny'], + 'auth/netkingdom/role/secrets-engine-approval-client-workload-kv-read': ['deny'], +} +RECEIPT = Path('/home/worsch/railiance-platform/docs/evidence/2026-09-14-ccr0019-reader-preflight.json') + +def validate_identity(identity): + policies = set(identity.get('policies', [])) | set(identity.get('identity_policies', [])) + if POLICY not in policies or policies - {POLICY, 'default'}: + raise ValueError('unexpected_effective_policies') + if not identity.get('entity_id') or not 0 < identity.get('ttl', 0) <= 900: + raise ValueError('unbounded_or_unidentified_reader') + +def bao(*args): + result = subprocess.run(['bao', *args], capture_output=True, text=True, timeout=30) + if result.returncode: + raise ValueError('metadata_request_failed') + return json.loads(result.stdout) + +def main(): + receipt = {'observed_at': datetime.now(timezone.utc).isoformat(), + 'ccr_id': 'CCR-2026-0019', 'secret_values_read': False, + 'status': 'refused', 'checks': {}} + try: + if Path.home().parent.name != '.warden-attended-login' or os.getenv('BAO_TOKEN') or os.getenv('VAULT_TOKEN'): + raise ValueError('attended_envelope_required') + identity = bao('token', 'lookup', '-format=json')['data'] + validate_identity(identity) + receipt['identity_policy_scope_verified'] = True + for path, expected in EXPECTED.items(): + values = bao('token', 'capabilities', '-format=json', path) + if sorted(values) != expected: + raise ValueError('capabilities_mismatch') + receipt['checks'][path] = expected + receipt['status'] = 'passed' + except Exception as exc: + code = str(exc) + receipt['failure'] = code if code in {'attended_envelope_required', 'unexpected_effective_policies', 'unbounded_or_unidentified_reader', 'metadata_request_failed', 'capabilities_mismatch'} else 'preflight_failed' + raise + finally: + RECEIPT.write_text(json.dumps(receipt, indent=2) + '\n') + +if __name__ == '__main__': + try: + main() + except Exception: + raise SystemExit(1) from None diff --git a/tests/test_approval_client_reader.py b/tests/test_approval_client_reader.py new file mode 100644 index 0000000..932a990 --- /dev/null +++ b/tests/test_approval_client_reader.py @@ -0,0 +1,47 @@ +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT=Path(__file__).resolve().parents[1] +def load(name, filename): + spec=importlib.util.spec_from_file_location(name, ROOT/'scripts'/filename) + module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module) + return module +reader=load('reader','approval-client-reader-preflight.py') +delivery=load('delivery','approval-client-delivery-check.py') + +class ReaderBoundary(unittest.TestCase): + def test_additive_identity_admin_policy_is_refused(self): + base={'policies':[reader.POLICY,'default'],'entity_id':'fixture','ttl':899} + reader.validate_identity(base) + for change in ({'identity_policies':['platform-admin']},{'policies':['default']},{'ttl':901},{'entity_id':''}): + with self.subTest(change=change), self.assertRaises(ValueError): + reader.validate_identity(base|change) + + def test_refused_preflight_never_fetches_credentials(self): + with tempfile.TemporaryDirectory() as directory: + receipt=Path(directory)/'receipt.json' + with patch.object(delivery,'RECEIPT',receipt), patch.object(delivery.preflight,'main',side_effect=ValueError('fixture refusal')), patch.object(delivery,'transport') as transport: + with self.assertRaises(ValueError): delivery.main() + transport.assert_not_called() + recorded=json.loads(receipt.read_text()) + self.assertEqual(recorded['status'],'refused') + self.assertTrue(recorded['cleanup']) + self.assertNotIn('fixture refusal',receipt.read_text()) + + def test_private_file_refuses_symlink_and_group_read(self): + with tempfile.TemporaryDirectory() as directory: + path=Path(directory)/'secret'; path.write_text('synthetic'); path.chmod(0o600) + delivery.private(path,0o600) + link=Path(directory)/'link'; link.symlink_to(path) + with self.assertRaises(ValueError): delivery.private(link,0o600) + path.chmod(0o640) + with self.assertRaises(ValueError): delivery.private(path,0o600) + + def test_credential_redirect_is_refused(self): + self.assertIsNone(delivery.NoRedirect().redirect_request(None,None,302,'redirect',{},'https://unrelated.invalid')) + +if __name__=='__main__': unittest.main() diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md index c16d325..745e305 100644 --- a/workplans/RPF-WP-0035-credential-lane-implementation.md +++ b/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -607,3 +607,29 @@ Service/domain-transaction/human and factory runtime/spend admission remain separate; factory attempts and paid model calls remain zero. Receipt: [native readback](../docs/evidence/2026-09-11-factory-native-readback.json). + + +### T06 native reader acceptance — 2026-09-14 + +The scoped Warden login lane `secrets-engine-approval-client-login` now routes +to the applied OIDC reader and the platform's silent preflight. The installed +Warden package still carries an older catalog; use the explicit source catalog +`WARDEN_ROUTING_CATALOG=/home/worsch/ops-warden/registry/routing/catalog.yaml` +until its next normal installation refresh. This lane authenticates the reader; +it is not a raw secret fetch or retained-file delivery interface. + +Live effective-policy and capability checks passed: only exact data/metadata +read, no sibling secret, parent listing, write or control-plane authority. +A second contained session read existing version 1 into an operator-owned 0600 +file in a private 0700 runtime tmpfs directory. The native consumer exchanged +separate read and consume scopes; Approval Engine verified the read token before +returning 404 for a fresh nonexistent approval, and refused the consume-only +token's read request with 403. No approval was created, bound or consumed. +Both Warden sessions exited 0 after self-revocation/helper cleanup; temporary +credential file and directory were removed. Four refusal/path/redirect tests +pass. Receipts are in platform `docs/evidence/2026-09-14-ccr0019-{reader-preflight,delivery-check}.json`. + +Remaining: an actual nonmember login refusal, real approval claim/consume, +separate narrow requester admission and deployed Informed Decision review with +an explicitly admitted human mandate. The reader group alone grants no review +mandate. T03 and CCR delivery activation remain open; no OpenRouter key was read.