Files
net-kingdom/sso-mfa/k8s/keycape
Bernd Worsch 59ba9e6fe1 fix(creds-bootstrap): harden agent bootstrap for non-interactive execution
- creds-bootstrap-agent.sh: skip Phase 3 if all secrets already applied
  (avoids CNPG SSL connection drops from repeated reconciliation)
- creds-bootstrap-agent.sh: wait for rollout to complete after restart
  before running enckey/admin bootstrap (fixes race with old pod)
- creds-bootstrap-agent.sh: only restart privacyIDEA when Phase 3 ran
- create-pi-token.sh: use env-var + retry for token fetch (no heredoc
  stdin; handles transient 500 from idle connection pool)
- create-pi-token.sh: create keycape-pi-token K8s Secret after fetching
- creds-verify.sh: map keycape-pi-token to secrets_applied.keycape
  (not pi_admin_created, which caused spurious Phase 5 re-runs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 12:11:13 +00:00
..

T05c — KeyCape (OIDC Orchestration Layer)

KeyCape is the stateless OIDC server that ties the stack together. It orchestrates the full authentication flow:

  1. User visits a registered application
  2. Application redirects to KeyCape (kc.coulomb.social) for login
  3. KeyCape redirects the browser to Authelia (auth.coulomb.social) for password auth
  4. Authelia validates the password against LLDAP and returns an authorization code
  5. KeyCape exchanges the code for user identity, then calls privacyIDEA for MFA
  6. On success, KeyCape issues a signed OIDC token to the application

KeyCape is stateless — all state lives in Authelia (sessions), LLDAP (users), and privacyIDEA (MFA tokens). No PVC is required.

Prerequisites

  • T04 complete (privacyIDEA is Running and bootstrapped — admin account + enckey done)
  • T05a complete (LLDAP is Running)
  • T05b complete (Authelia is Running)
  • KeyCape container image built and available (see "Building the image" below)
  • bootstrap/gen-secrets.sh run
  • kubectl configured with cluster access

Building the image

KeyCape has no published image. Build it from the source repository and make it available to K3s before applying deployment.yaml.

Option A — Local import into K3s (dev/single-node)

cd ~/key-cape
docker build -t keycape:v0.1 .

# Import directly into the K3s containerd runtime (no registry needed)
docker save keycape:v0.1 | sudo k3s ctr images import -

# After import, set imagePullPolicy: Never in deployment.yaml
# (the image is now in the K3s local store, not a registry)

Option B — Private registry (production)

cd ~/key-cape
docker build -t <registry>/keycape:v0.1 .
docker push <registry>/keycape:v0.1

# Update the image field in deployment.yaml:
#   image: <registry>/keycape:v0.1
# imagePullPolicy: IfNotPresent (default) is correct for registry images.

After building, update deployment.yaml line:

image: keycape:v0.1   # replace with your actual tag

Apply order

# 1. Create Secrets (config.yaml + key.pem)
#    Run this AFTER T04 bootstrap if you want the privacyIDEA token included.
#    If T04 is not yet done, run it now and re-run after create-pi-token.sh.
cd sso-mfa/k8s/keycape
chmod +x create-secrets.sh create-pi-token.sh
./create-secrets.sh

# 2. Apply manifests
kubectl apply -f deployment.yaml
kubectl apply -f middleware.yaml
kubectl apply -f ingress.yaml

# 3. Wait for pod to be ready
kubectl rollout status deployment/keycape -n sso --timeout=60s

Post-deploy: inject privacyIDEA admin token

If T04 was not complete when you ran create-secrets.sh, the privacyIDEA admin token is a placeholder. After T04 bootstrap is done:

# 1. Fetch the token from privacyIDEA and store it
chmod +x create-pi-token.sh
./create-pi-token.sh

# 2. Re-run create-secrets.sh to update keycape-config with the real token
./create-secrets.sh

# 3. Restart KeyCape to pick up the new Secret
kubectl rollout restart deployment/keycape -n sso

OIDC client registration

Downstream applications are registered in the clients: block in keycape/create-secrets.sh. After editing:

./create-secrets.sh           # regenerates keycape-config Secret
kubectl rollout restart deployment/keycape -n sso

Example entry (public client, PKCE, for a SPA):

clients:
  - clientId: "my-app"
    displayName: "My Application"
    redirectUris:
      - "https://my-app.coulomb.social/callback"
    allowedScopes: ["openid", "profile", "email", "groups"]
    grantTypes: ["authorization_code"]
    clientType: "public"

Secrets managed

Secret name Keys Purpose
keycape-config config.yaml Full KeyCape configuration (LLDAP URL + creds, Authelia URL + client secret, privacyIDEA URL + admin token, OIDC clients)
key.pem RSA-2048 private key for signing OIDC tokens issued to downstream applications
keycape-pi-token pi_admin_token privacyIDEA admin JWT — created by create-pi-token.sh, referenced in config.yaml

Store key.pem in KeePassXC as a binary attachment. If it is lost, all active sessions become invalid (tokens cannot be verified) and all applications must re-authenticate.

Verify

# Pod status
kubectl get pod -n sso -l app.kubernetes.io/name=keycape

# Health check
kubectl run -n sso --rm -it kc-test --image=busybox --restart=Never \
  -- wget -qO- http://keycape.sso.svc.cluster.local:8080/healthz

# OIDC discovery (public endpoint)
curl -s https://kc.coulomb.social/.well-known/openid-configuration | jq .

# Check issuer matches CP-NK-004
curl -s https://kc.coulomb.social/.well-known/openid-configuration \
  | jq -r .issuer   # should be: https://kc.coulomb.social