"""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}} sidecar['env'] += [entry for entry in main.get('env',[]) if entry['name']=='PYTHONPATH'] 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()