Set listing_visibility=unauth on netkingdom and keycape during OIDC configure so the browser login mask can select KeyCape instead of falling back to token.
T05c — KeyCape (OIDC Orchestration Layer)
KeyCape is the stateless OIDC server that ties the stack together. It orchestrates the full authentication flow:
- User visits a registered application
- Application redirects to KeyCape (
kc.coulomb.social) for login - KeyCape redirects the browser to Authelia (
auth.coulomb.social) for password auth - Authelia validates the password against LLDAP and returns an authorization code
- KeyCape exchanges the code for user identity, then calls privacyIDEA for MFA
- 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.
The Authelia baseURL in create-secrets.sh must be the browser-facing
https://auth.coulomb.social URL. KeyCape uses it to build the redirect sent
to the user's browser during /authorize; a cluster-internal service URL or
relative Authelia path will make the public OIDC login flow land on a 404 even
when discovery and health checks are working.
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.shrunkubectlconfigured 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
bash ./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
bash ./create-secrets.sh
# 3. Restart KeyCape to pick up the new Secret
kubectl rollout restart deployment/keycape -n sso
If the browser flow reaches the KeyCape OTP screen and then reports
mfa check error, refresh the live privacyIDEA token without printing it:
cd sso-mfa/k8s/keycape
KEYCAPE_PI_REALM=coulomb KUBECTL="${KUBECTL:-kubectl}" \
bash ./refresh-pi-token-live.sh platform-root
The helper prompts for the pi-admin password, writes the token only into
Kubernetes Secrets, and restarts KeyCape. The current live privacyIDEA realm is
coulomb; use KEYCAPE_PI_REALM=netkingdom only for an explicit future realm
migration. The helper also restores privacyidea.requireForAll: true, which
keeps KeyCape from using the admin token-list API as the MFA-required check.
OIDC client registration
Downstream applications are registered in the clients: block in
keycape/create-secrets.sh. The NetKingdom bootstrap console and Railiance
OpenBao admin clients are code-defined there; operators should not create
those clients manually in a separate UI. After changing the block:
bash ./create-secrets.sh # regenerates keycape-config Secret
kubectl rollout restart deployment/keycape -n sso
The openbao-admin client is intentionally a public PKCE client for the
current operator flow. It registers both the OpenBao CLI callback URIs and the
browser UI callbacks for bao.coulomb.social:
http://localhost:8250/oidc/callback
http://127.0.0.1:8250/oidc/callback
https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
https://bao.coulomb.social/ui/vault/auth/keycape/oidc/callback
The browser UI callback is paired with the Railiance Platform OpenBao ingress
at https://bao.coulomb.social. The preferred browser auth mount is
netkingdom; keycape remains a compatibility alias. Keep the localhost
callbacks unless there is a separate decision to retire CLI login.
To add or refresh only the OpenBao client in a live cluster, do not decrypt the
bootstrap secret bundle and do not re-run the full secret generator. Patch the
existing live keycape-config Secret in place:
cd sso-mfa/k8s/keycape
bash ./patch-openbao-client.sh
kubectl rollout restart deployment/keycape -n sso
kubectl rollout status deployment/keycape -n sso --timeout=60s
bash ./verify-openbao-client.sh
The patch script preserves existing secret values and does not print the
decoded config.yaml or signing key. The verifier checks the live Secret and
then opens a short local kubectl port-forward to KeyCape; it does not require
curl or wget inside the KeyCape container image.
After the live KeyCape client is present, configure Railiance OpenBao to trust KeyCape:
bash ./configure-openbao-oidc.sh
That script registers the browser UI callbacks on the OpenBao
auth/netkingdom/role/platform-admin role and the compatibility
auth/keycape/role/platform-admin role. Browser operators should use the
OpenBao UI at https://bao.coulomb.social, leave namespace blank, choose
OIDC, set mount path netkingdom, and use role platform-admin; root-token
browser use is outside the approved operator path.
The script prompts for a root/sudo-capable OpenBao token inside the pod TTY.
OpenBao currently requires oidc_client_secret for OIDC auth config, while
KeyCape's openbao-admin client is public PKCE and does not validate a
downstream client secret. The script therefore writes the explicit
non-secret compatibility value keycape-public-pkce-compatibility-value.
Replace that with a real managed client secret when KeyCape supports
confidential downstream clients.
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"
For the local NetKingdom bootstrap console login check, keep the dedicated bootstrap client registered with exact local callback URIs:
clients:
- clientId: "netkingdom-bootstrap-console"
displayName: "NetKingdom Bootstrap Console"
redirectUris:
- "http://127.0.0.1:8876/oidc/callback"
- "http://localhost:8876/oidc/callback"
allowedScopes: ["openid", "profile", "email", "groups"]
grantTypes: ["authorization_code"]
clientType: "public"
The local callback page exchanges the authorization code and displays only non-secret claims. KeyCape presents a browser OTP challenge between Authelia password login and the final OIDC redirect whenever privacyIDEA requires MFA.
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
# Browser login redirect should start at KeyCape and then leave the kc host for
# Authelia. If it redirects to /api/oidc/authorization on kc.coulomb.social,
# regenerate keycape-config and restart KeyCape after confirming the Authelia
# browserBaseURL above.