railiance-platform/manifests/keycape-factor-custody.yaml
codex 2e2c31d237
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
KeyCape factor custody acceptance / acceptance (push) Successful in 7s
Establish scoped KeyCape factor custody and verified automatic renewal
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
2026-09-13 16:25:33 +02:00

267 lines
9.5 KiB
YAML

apiVersion: v1
kind: ServiceAccount
metadata:
name: keycape-factor-eso
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: keycape-factor-renewer
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
automountServiceAccountToken: false
---
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: openbao-keycape-factor-read
spec:
conditions:
- namespaces:
- sso
provider:
vault:
server: http://openbao.openbao.svc.cluster.local:8200
path: platform
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: keycape-factor-workload-kv-read
serviceAccountRef:
name: keycape-factor-eso
namespace: sso
audiences:
- openbao
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: keycape-factor-read
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
refreshInterval: 1m
secretStoreRef:
kind: ClusterSecretStore
name: openbao-keycape-factor-read
target:
name: keycape-factor-read
creationPolicy: Owner
data:
- secretKey: admin-token
remoteRef:
key: workloads/net-kingdom/keycape-factor-read
property: TOKEN
- secretKey: expires-at
remoteRef:
key: workloads/net-kingdom/keycape-factor-read
property: EXPIRES_AT
---
apiVersion: v1
kind: ConfigMap
metadata:
name: keycape-factor-renewer-2cf770000e
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
immutable: true
data:
renew.py: "\"\"\"Dedicated Kubernetes renewal worker. Only sanitized outcome metadata\
\ is emitted.\"\"\"\nimport base64,json,sys,time,urllib.request,urllib.error,urllib.parse\n\
from pathlib import Path\nBAO=\"http://openbao.openbao.svc.cluster.local:8200/v1\"\
\nPI=\"http://privacyidea.mfa.svc.cluster.local:8080\"\nISSUER=\"platform/data/workloads/net-kingdom/keycape-factor-issuer\"\
\nTARGET=\"platform/data/workloads/net-kingdom/keycape-factor-read\"\nUSER=\"\
keycape-factor-reader\"\nPROVENANCE=\"RPF-WP-0040\"\nclass Failure(Exception):pass\n\
class NoRedirect(urllib.request.HTTPRedirectHandler):\n def redirect_request(self,*args,**kwargs):return\
\ None\n\ndef http(url,body=None,headers=None,method=None,form=False):\n data=None\n\
\ headers=dict(headers or {})\n if body is not None:\n data=(urllib.parse.urlencode(body)\
\ if form else json.dumps(body)).encode()\n headers[\"Content-Type\"]=\"application/x-www-form-urlencoded\"\
\ if form else \"application/json\"\n req=urllib.request.Request(url,data=data,headers=headers,method=method)\n\
\ try:\n with urllib.request.build_opener(NoRedirect()).open(req,timeout=15)\
\ as r:\n raw=r.read(1048577)\n if len(raw)>1048576:raise Failure()\n return\
\ r.status,json.loads(raw) if raw else {}\n except urllib.error.HTTPError as e:return\
\ e.code,{}\n\ndef success(code,data):\n if code!=200 or not data.get(\"result\"\
,{}).get(\"status\"):raise Failure()\n return data[\"result\"][\"value\"]\n\n\
def run():\n session=None;receipt={\"success\":False,\"phase\":\"workload_login\"\
}\n try:\n jwt=Path(\"/var/run/keycape-factor/token\").read_text().strip()\n\
\ code,body=http(BAO+\"/auth/kubernetes/login\",{\"role\":\"keycape-factor-renewer\"\
,\"jwt\":jwt})\n if code!=200:raise Failure()\n session=body[\"auth\"][\"client_token\"\
];headers={\"X-Vault-Token\":session}\n receipt[\"phase\"]=\"issuer_custody\"\
\n code,body=http(BAO+\"/\"+ISSUER,headers=headers)\n if code!=200:raise Failure()\n\
\ issuer=body[\"data\"][\"data\"]\n if issuer.get(\"REQUEST\")!=PROVENANCE or\
\ issuer.get(\"USERNAME\")!=USER:raise Failure()\n receipt[\"phase\"]=\"provider_authentication\"\
\n value=success(*http(PI+\"/auth\",{\"username\":USER,\"password\":issuer[\"\
PASSWORD\"]},form=True))\n if value.get(\"role\")!=\"admin\" or value.get(\"\
username\")!=USER:raise Failure()\n token=value[\"token\"];claims=json.loads(base64.urlsafe_b64decode(token.split('.')[1]+'==='));expiry=int(claims[\"\
exp\"])\n if not 300<expiry-time.time()<=7200:raise Failure()\n receipt[\"phase\"\
]=\"provider_scope\"\n provider_headers={\"Authorization\":token}\n listing=success(*http(PI+\"\
/token/?tokenrealm=coulomb&active=True&pagesize=1\",headers=provider_headers))\n\
\ if not isinstance(listing.get(\"tokens\"),list) or not isinstance(listing.get(\"\
count\"),int):raise Failure()\n code,_=http(PI+\"/policy/\",headers=provider_headers)\n\
\ if code not in (401,403):raise Failure()\n receipt[\"phase\"]=\"publish\"\n\
\ code,body=http(BAO+\"/\"+TARGET,headers=headers)\n if code!=200 or body[\"\
data\"][\"data\"].get(\"REQUEST\")!=PROVENANCE:raise Failure()\n version=body[\"\
data\"][\"metadata\"][\"version\"]\n code,_=http(BAO+\"/\"+TARGET,{\"options\"\
:{\"cas\":version},\"data\":{\"TOKEN\":token,\"EXPIRES_AT\":expiry,\"REQUEST\"\
:PROVENANCE}},headers=headers)\n if code!=200:raise Failure()\n code,body=http(BAO+\"\
/\"+TARGET,headers=headers)\n if code!=200 or body[\"data\"][\"data\"][\"TOKEN\"\
]!=token:raise Failure()\n receipt.update(success=True,phase=\"complete\",expires_at=expiry,kv_version=body[\"\
data\"][\"metadata\"][\"version\"],cross_user_factor_visible=listing[\"count\"\
]>0)\n except Exception:pass\n finally:\n if session:\n try:\n code,_=http(BAO+\"\
/auth/token/revoke-self\",headers={\"X-Vault-Token\":session},method=\"POST\"\
)\n receipt[\"session_revoked\"]=code in (200,204)\n except Exception:receipt[\"\
session_revoked\"]=False\n if not receipt[\"session_revoked\"]:receipt[\"success\"\
]=False;receipt[\"phase\"]=\"session_cleanup\"\n return receipt\nif __name__==\"\
__main__\":\n receipt=run();print(json.dumps(receipt));raise SystemExit(0 if receipt[\"\
success\"] else 1)\n"
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: keycape-factor-renewer
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
schedule: '*/10 * * * *'
suspend: false
concurrencyPolicy: Forbid
startingDeadlineSeconds: 120
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 3
jobTemplate:
spec:
activeDeadlineSeconds: 120
backoffLimit: 1
ttlSecondsAfterFinished: 3600
template:
metadata:
labels:
app.kubernetes.io/name: keycape-factor-renewer
spec:
serviceAccountName: keycape-factor-renewer
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: renew
image: python:3.12-slim@sha256:d764629ce0ddd8c71fd371e9901efb324a95789d2315a47db7e4d27e78f1b0e9
command:
- python3
- /worker/renew.py
env:
- name: PYTHONDONTWRITEBYTECODE
value: '1'
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
cpu: 10m
memory: 24Mi
limits:
cpu: 100m
memory: 64Mi
volumeMounts:
- name: worker
mountPath: /worker
readOnly: true
- name: identity
mountPath: /var/run/keycape-factor
readOnly: true
volumes:
- name: worker
configMap:
name: keycape-factor-renewer-2cf770000e
- name: identity
projected:
defaultMode: 288
sources:
- serviceAccountToken:
path: token
audience: openbao
expirationSeconds: 600
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: keycape-factor-renewer
namespace: sso
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: keycape-factor-renewer
policyTypes:
- Ingress
- Egress
ingress: []
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: openbao
ports:
- port: 8200
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: mfa
podSelector:
matchLabels:
app.kubernetes.io/name: privacyidea
ports:
- port: 8080
protocol: TCP
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-keycape-factor-renewal
namespace: mfa
labels:
app.kubernetes.io/part-of: net-kingdom-sso-mfa
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: privacyidea
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: sso
podSelector:
matchLabels:
app.kubernetes.io/name: keycape-factor-renewer
ports:
- port: 8080
protocol: TCP