Add attended SMTP verification and exact reader admission
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0e6f1-443f-7783-9920-a16b2ffc467f
This commit is contained in:
parent
3d43a9b1a3
commit
56fcc4c921
3 changed files with 183 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
118
scripts/telemetry_smtp_delivery.py
Normal file
118
scripts/telemetry_smtp_delivery.py
Normal file
|
|
@ -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)
|
||||
40
tests/test_telemetry_smtp_delivery.py
Normal file
40
tests/test_telemetry_smtp_delivery.py
Normal file
|
|
@ -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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue