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.
|
requires attention. Native creation is pending until that attended result.
|
||||||
This helper does not grant ESO access, copy another mailbox's credential, or
|
This helper does not grant ESO access, copy another mailbox's credential, or
|
||||||
activate SMTP. Scoped workload delivery remains the existing telemetry task.
|
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