Connect P04 audited recovery to fresh-MFA platform browser flow
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
This commit is contained in:
parent
54a47dd810
commit
7df21d188b
1 changed files with 95 additions and 0 deletions
95
sso-mfa/k8s/privacyidea/deploy-factor-recovery.py
Normal file
95
sso-mfa/k8s/privacyidea/deploy-factor-recovery.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue