From 56fcc4c92119dde5ed1224b82d3c10b2799aaa4f Mon Sep 17 00:00:00 2001 From: codex Date: Mon, 28 Sep 2026 11:21:57 +0200 Subject: [PATCH] Add attended SMTP verification and exact reader admission Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e6f1-443f-7783-9920-a16b2ffc467f --- docs/telemetry-smtp-custody.md | 25 ++++++ scripts/telemetry_smtp_delivery.py | 118 ++++++++++++++++++++++++++ tests/test_telemetry_smtp_delivery.py | 40 +++++++++ 3 files changed, 183 insertions(+) create mode 100644 scripts/telemetry_smtp_delivery.py create mode 100644 tests/test_telemetry_smtp_delivery.py diff --git a/docs/telemetry-smtp-custody.md b/docs/telemetry-smtp-custody.md index 6c3b4ab..90a872e 100644 --- a/docs/telemetry-smtp-custody.md +++ b/docs/telemetry-smtp-custody.md @@ -28,3 +28,28 @@ completed-but-revocation-unconfirmed means creation ran but session revocation requires attention. Native creation is pending until that attended result. This helper does not grant ESO access, copy another mailbox's credential, or activate SMTP. Scoped workload delivery remains the existing telemetry task. + +## Password supplied; scoped delivery admission + +The founder reports SMTP_PASSWORD added. This is not yet native verification. +The dedicated telemetry-smtp-eso ServiceAccount and package-owned +acknowledgment/smtp-custody.yaml are installed after dry-run and diff. +They project only SMTP_PASSWORD to telemetry/telemetry-alert-smtp:password. + +The next attended command validates the exact public settings and password, +performs certificate-verified IONOS STARTTLS authentication without sending mail, +and admits the exact read-only policy and Kubernetes role (telemetry namespace, +telemetry-smtp-eso ServiceAccount, openbao audience, maximum 15 minutes). +Existing different policy/role values fail closed. Mailbox versions are untouched. +Coding-agent deny boundary is extended for data and metadata and tested. + +```sh +BAO_ADDR=http://127.0.0.1:18200 VAULT_ADDR=http://127.0.0.1:18200 warden access openbao-platform-admin-login --exec -- python3 /home/worsch/railiance-platform/scripts/telemetry_smtp_delivery.py --receipt /tmp/telemetry-smtp-delivery.json +``` + +Use a new receipt filename on retry; existing evidence is never overwritten. +The helper is silent and writes only fixed status fields/KV version in its +0600 receipt. It never reads Kubernetes Secret values. Ten custody tests pass. +After successful attended completion, check ClusterSecretStore/ExternalSecret +Ready and record the receipt. Actual scoped Kubernetes auth and ESO delivery +remain unproved until that reconciliation. The alert route is still disabled. diff --git a/scripts/telemetry_smtp_delivery.py b/scripts/telemetry_smtp_delivery.py new file mode 100644 index 0000000..01ccdc9 --- /dev/null +++ b/scripts/telemetry_smtp_delivery.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Silent attended SMTP custody verification and exact ESO reader admission. + +Never rewrites mailbox data. No Kubernetes Secret reads. Receipt contains only +fixed status fields and KV version. Does not send email or activate Alertmanager. +""" +import argparse +import json +import os +from pathlib import Path +import re +import smtplib +import ssl +import sys + +from state_hub_preflight_lane import bao, data, LaneError, capabilities, revoke +from repair_eso_kubernetes_auth import role_payload, check_role +from telemetry_smtp_entry import FIELDS + +LANE = dict(service_account='telemetry-smtp-eso', namespace='telemetry', + role='telemetry-smtp-eso', policy='telemetry-smtp-eso', + kv_path='platform/data/workloads/railiance-telemetry/smtp') +POLICY = ('path "'+LANE['kv_path']+'" { capabilities = ["read"] }\n' + 'path "auth/token/lookup-self" { capabilities = ["read"] }\n' + 'path "auth/token/revoke-self" { capabilities = ["update"] }\n') + + +def require(ok, reason): + if not ok: + raise LaneError(reason) + + +def validate_entry(native): + values = native['data'] + require(all(values.get(k) == v for k,v in FIELDS.items()), 'smtp_settings_differ') + password = values.get('SMTP_PASSWORD') + require(isinstance(password, str) and bool(password.strip()) and len(password) <= 4096 + and '\r' not in password and '\n' not in password, 'smtp_password_missing_or_invalid') + return password + + +def missing(result): + return result.returncode != 0 and b'No value found' in result.stdout + result.stderr + + +def ensure(path, wanted, check): + old = bao(['read','-format=json',path], allow_failure=True) + if old.returncode == 0: + check(data(old)['data']) + else: + require(missing(old), 'metadata_absence_unproven') + bao(['write',path,'-'], payload=wanted) + check(data(bao(['read','-format=json',path]))['data']) + + +def run(receipt): + identity = data(bao(['token','lookup','-format=json']))['data']['policies'] + require('platform-admin' in identity and 'root' not in identity, 'attended_platform_admin_required') + native = data(bao(['read','-format=json',LANE['kv_path']]))['data'] + password = validate_entry(native) + # Exact fixed endpoint; standard certificate validation and STARTTLS mandatory. + with smtplib.SMTP('smtp.ionos.de',587,timeout=15) as smtp: + smtp.ehlo() + smtp.starttls(context=ssl.create_default_context()) + smtp.ehlo() + smtp.login(FIELDS['SMTP_USERNAME'], password) + receipt.update(kv_version=native['metadata']['version'], smtp_authenticated=True) + boundary = 'sys/policies/acl/agent-high-risk-boundary' + current = data(bao(['read','-format=json',boundary]))['data']['policy'] + added = '' + for path in [LANE['kv_path'], LANE['kv_path'].replace('/data/','/metadata/',1)]: + stanza = 'path "'+path+'" { capabilities = ["deny"] }\n' + if '"'+path+'"' not in current: + added += stanza + else: + require(re.search(r'path\s+"'+re.escape(path)+r'"\s*\{\s*capabilities\s*=\s*\["deny"\]\s*\}',current), 'boundary_drift') + if added: + require(data(bao(['read','-format=json',boundary]))['data']['policy'] == current,'boundary_changed') + bao(['write',boundary,'-'],payload={'policy':current+'\n'+added}) + require(data(bao(['read','-format=json',boundary]))['data']['policy'] == current+'\n'+added,'boundary_readback_failed') + ensure('sys/policies/acl/'+LANE['policy'], {'policy':POLICY}, + lambda actual: require(actual['policy'] == POLICY,'reader_policy_drift')) + ensure('auth/kubernetes/role/'+LANE['role'],role_payload(LANE),lambda actual: check_role(actual,LANE)) + child = data(bao(['token','create','-format=json','-policy='+LANE['policy'], + '-policy=agent-high-risk-boundary','-no-default-policy','-ttl=60s']))['auth']['client_token'] + try: + paths = [LANE['kv_path'],LANE['kv_path'].replace('/data/','/metadata/',1)] + require(all(v == ['deny'] for v in capabilities(child,paths).values()),'agent_boundary_failed') + finally: + revoke(child) + receipt.update(status='verified', reader_admitted=True, coding_agent_denied=True, + password_modified=False, email_sent=False) + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--receipt',required=True,type=Path) + args=parser.parse_args() + fd=os.open(args.receipt,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600) + receipt={'schema':'telemetry.smtp-delivery.v1','status':'failed'} + try: + run(receipt) + except Exception as exc: + receipt['reason']=str(exc) if isinstance(exc,LaneError) else 'contained_operation_failed' + finally: + with os.fdopen(fd,'w') as target: + json.dump(receipt,target,indent=2);target.write('\n') + return 0 if receipt['status']=='verified' else 1 + + +if __name__=='__main__': + with open(os.devnull,'w') as sink: + os.dup2(sink.fileno(),1);os.dup2(sink.fileno(),2) + try: + code=main() + except Exception: + code=1 + sys.exit(code) diff --git a/tests/test_telemetry_smtp_delivery.py b/tests/test_telemetry_smtp_delivery.py new file mode 100644 index 0000000..9ccc71f --- /dev/null +++ b/tests/test_telemetry_smtp_delivery.py @@ -0,0 +1,40 @@ +import json +import sys +from pathlib import Path +import subprocess +import unittest +from unittest.mock import patch +sys.path.insert(0,str(Path(__file__).resolve().parents[1]/'scripts')) +import telemetry_smtp_delivery as delivery + +class DeliveryTests(unittest.TestCase): + def test_valid_entry(self): + value=dict(delivery.FIELDS,SMTP_PASSWORD='fixture-password') + self.assertEqual(delivery.validate_entry({'data':value}),'fixture-password') + def test_password_missing_or_invalid(self): + for password in (None,'',' ','bad\npassword'): + with self.subTest(password=password),self.assertRaises(delivery.LaneError): + delivery.validate_entry({'data':dict(delivery.FIELDS,SMTP_PASSWORD=password)}) + def test_wrong_identity_or_server_refused(self): + for field in ('SMTP_USERNAME','SMTP_HOST','SMTP_FROM','SMTP_STARTTLS'): + value=dict(delivery.FIELDS,SMTP_PASSWORD='fixture');value[field]='wrong' + with self.subTest(field=field),self.assertRaises(delivery.LaneError): + delivery.validate_entry({'data':value}) + def test_permission_denied_does_not_create_policy(self): + with patch.object(delivery,'bao',return_value=subprocess.CompletedProcess([],2,b'',b'permission denied')) as bao: + with self.assertRaises(delivery.LaneError):delivery.ensure('policy',{},lambda _:None) + self.assertEqual(bao.call_count,1) + def test_different_existing_policy_never_overwritten(self): + result=subprocess.CompletedProcess([],0,b'{"data":{"policy":"different"}}',b'') + with patch.object(delivery,'bao',return_value=result) as bao: + with self.assertRaises(delivery.LaneError): + delivery.ensure('policy',{'policy':'wanted'},lambda a:delivery.require(a['policy']=='wanted','drift')) + self.assertEqual(bao.call_count,1) + def test_exact_reader_role(self): + role=delivery.role_payload(delivery.LANE) + self.assertEqual(role['bound_service_account_names'],['telemetry-smtp-eso']) + self.assertEqual(role['bound_service_account_namespaces'],['telemetry']) + self.assertEqual(role['audience'],'openbao') + self.assertEqual(role['token_explicit_max_ttl'],'15m') + self.assertTrue(role['token_no_default_policy']) + self.assertNotIn('*',delivery.POLICY)