Connect P04 audited recovery to fresh-MFA platform browser flow
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
tegwick 2026-09-13 21:05:33 +02:00
parent 54a47dd810
commit 7df21d188b

View file

@ -0,0 +1,95 @@
"""Deploy the private P04 service using existing provider custody and CAS guards."""
import argparse
import hashlib
import json
import subprocess
from pathlib import Path
CLUSTER='a553c742-0115-43d4-99a4-a5ca56fe0786'
PROVIDER='58c7f96d-61cb-4dd4-bca2-54661c0ac375'
PORTAL='e1c4274a-4b6d-4ea2-b619-e09630b745b0'
IMAGE='ghcr.io/gpappsoft/privacyidea-docker@sha256:af7841adad262f129e0c1d4f553af13f21cb2f4dc713533f316cfe43ed0b4473'
def kube(*args, body=None):
r=subprocess.run(['kubectl',*args],input=json.dumps(body).encode() if body is not None else None,
capture_output=True,timeout=45)
if r.returncode:raise RuntimeError('Kubernetes operation failed; inspect resource state')
return json.loads(r.stdout) if r.stdout.strip().startswith(b'{') else None
def peer(namespace,name):
return {'namespaceSelector':{'matchLabels':{'kubernetes.io/metadata.name':namespace}},
'podSelector':{'matchLabels':{'app.kubernetes.io/name':name}}}
def resources(source):
data={name:(source/name).read_text() for name in ('factor_recovery.py','recovery_service.py')}
digest=hashlib.sha256(json.dumps(data,sort_keys=True).encode()).hexdigest()[:16]
name='factor-recovery-'+digest
docs=[{'apiVersion':'v1','kind':'ConfigMap','metadata':{'name':name,'namespace':'mfa'},'immutable':True,'data':data},
{'apiVersion':'v1','kind':'Service','metadata':{'name':'factor-recovery','namespace':'mfa'},
'spec':{'selector':{'app.kubernetes.io/name':'privacyidea'},'ports':[{'name':'recovery','port':8091,'targetPort':8091}]}}]
for ns,selector,direction,remote,port in [
('mfa','privacyidea','ingress',peer('user-engine','user-engine'),8091),
('user-engine','user-engine','egress',peer('mfa','privacyidea'),8091),
('mfa','privacyidea','egress',peer('sso','keycape'),8080),
('sso','keycape','ingress',peer('mfa','privacyidea'),8080)]:
docs.append({'apiVersion':'networking.k8s.io/v1','kind':'NetworkPolicy',
'metadata':{'name':'factor-recovery-'+direction+'-'+str(port),'namespace':ns},
'spec':{'podSelector':{'matchLabels':{'app.kubernetes.io/name':selector}},
'policyTypes':[direction.title()],direction:[{'from' if direction=='ingress' else 'to':[remote],
'ports':[{'protocol':'TCP','port':port}]}]}})
return name,docs
def main():
p=argparse.ArgumentParser()
p.add_argument('--source',type=Path,required=True)
p.add_argument('--apply',action='store_true')
p.add_argument('--portal-image')
a=p.parse_args()
if kube('get','ns','kube-system','-o','json')['metadata']['uid']!=CLUSTER:raise RuntimeError('wrong cluster')
provider=kube('-n','mfa','get','deploy','privacyidea','-o','json')
if provider['metadata']['uid']!=PROVIDER:raise RuntimeError('provider identity changed')
name,docs=resources(a.source)
spec=provider['spec']['template']['spec']
main=next(c for c in spec['containers'] if c['name']=='privacyidea')
sidecar={'name':'factor-recovery','image':IMAGE,'command':['python3','/recovery/recovery_service.py'],
'env':[{'name':'PRIVACYIDEA_CONFIGFILE','value':'/etc/privacyidea/pi.cfg'},
{'name':'PYTHONDONTWRITEBYTECODE','value':'1'},
{'name':'RECOVERY_ISSUER','value':'https://kc.coulomb.social'},
{'name':'RECOVERY_AUDIENCE','value':'user-engine-portal'},
{'name':'RECOVERY_JWKS_URL','value':'http://keycape.sso.svc.cluster.local:8080/jwks'}],
'envFrom':main['envFrom'],
'volumeMounts':[dict(m,readOnly=m['name']!='logs') for m in main['volumeMounts']]+[{'name':'factor-recovery-code','mountPath':'/recovery','readOnly':True}],
'securityContext':{'runAsUser':65534,'runAsGroup':999,'runAsNonRoot':True,'allowPrivilegeEscalation':False,'readOnlyRootFilesystem':True,'capabilities':{'drop':['ALL']}},
'ports':[{'name':'recovery','containerPort':8091}],
'resources':{'requests':{'cpu':'10m','memory':'128Mi'},'limits':{'cpu':'500m','memory':'384Mi'}},
'readinessProbe':{'httpGet':{'path':'/healthz','port':8091},'initialDelaySeconds':10,'periodSeconds':10},
'livenessProbe':{'httpGet':{'path':'/healthz','port':8091},'initialDelaySeconds':60,'periodSeconds':20}}
containers=[c for c in spec['containers'] if c['name']!='factor-recovery']+[sidecar]
volumes=[v for v in spec['volumes'] if v['name']!='factor-recovery-code']+[{'name':'factor-recovery-code','configMap':{'name':name}}]
patch=[{'op':'test','path':'/metadata/resourceVersion','value':provider['metadata']['resourceVersion']},
{'op':'replace','path':'/spec/template/spec/containers','value':containers},
{'op':'replace','path':'/spec/template/spec/volumes','value':volumes}]
print(json.dumps({'config':name,'resources':[(d['kind'],d['metadata']['name']) for d in docs],'provider_sidecar':True,'portal_update':bool(a.portal_image),'apply':a.apply}))
if not a.apply:return
for doc in docs:kube('apply','-f','-',body=doc)
kube('-n','mfa','patch','deploy','privacyidea','--type=json','-p',json.dumps(patch))
if a.portal_image:
if not a.portal_image.startswith('forgejo.coulomb.social/coulomb/user-engine@sha256:') or len(a.portal_image.rsplit(':',1)[1])!=64:raise ValueError('digest-pinned portal required')
portal=kube('-n','user-engine','get','deploy','user-engine','-o','json')
if portal['metadata']['uid']!=PORTAL:raise RuntimeError('portal identity changed')
items=portal['spec']['template']['spec']['containers']
c=next(c for c in items if c['name']=='portal');c['image']=a.portal_image
names={'USER_ENGINE_FACTOR_RECOVERY_URL','USER_ENGINE_MFA_MANAGEMENT_URL'}
c['env']=[e for e in c['env'] if e['name'] not in names]+[
{'name':'USER_ENGINE_FACTOR_RECOVERY_URL','value':'http://factor-recovery.mfa.svc.cluster.local:8091'},
{'name':'USER_ENGINE_MFA_MANAGEMENT_URL','value':'https://pink-account.coulomb.social'}]
kube('-n','user-engine','patch','deploy','user-engine','--type=json','-p',json.dumps([
{'op':'test','path':'/metadata/resourceVersion','value':portal['metadata']['resourceVersion']},
{'op':'replace','path':'/spec/template/spec/containers','value':items}]))
print('Recovery deployment applied; verify rollout and authenticated acceptance.')
if __name__=='__main__':main()