net-kingdom/sso-mfa/k8s/keycape
tegwick 276b95f6ba
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
security: reject Playwright audience and delivery alias collisions
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a1346b-72a4-7d40-b8bd-8b8ca346cfa2
2026-10-13 23:44:41 +02:00
..
approval-clients-rollout.py feat: accept KeyCape approval clients with tested recovery 2026-09-09 02:18:12 +02:00
authentication-policy-rollout.py Implement scoped P06 authentication policy and guarded optional onboarding 2026-09-14 00:00:09 +02:00
bootstrap-config.json fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
configure-openbao-oidc.sh Stop configure-openbao-oidc.sh from writing the platform-admin role 2026-09-23 19:39:53 +02:00
create-pi-token.sh fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
create-secrets.py fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
create-secrets.sh fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
deployment.yaml Register secrets-engine-openbao live and fold its env binding in (NK-WP-0046-T03) 2026-10-07 02:20:08 +02:00
exercise-approval-clients.py feat: accept KeyCape approval clients with tested recovery 2026-09-09 02:18:12 +02:00
ingress.yaml feat(sso-mfa): T05 SSO stack pivot — Keycloak → Authelia + LLDAP + KeyCape (NK-WP-0001-T05) 2026-03-19 08:31:51 +00:00
keycape-client.py NK-WP-0048: run keycape check-config before any registry patch 2026-10-10 15:28:05 +02:00
middleware.yaml feat(sso-mfa): T05 SSO stack pivot — Keycloak → Authelia + LLDAP + KeyCape (NK-WP-0001-T05) 2026-03-19 08:31:51 +00:00
openbao-client-config.py Retire bao.coulomb.social callbacks from the KeyCape openbao-admin client 2026-09-23 19:41:35 +02:00
openbao-negative-node-route.md Publish reviewed bounded JWT ready handoff and retained owner history 2026-10-11 18:31:04 +02:00
openbao-negative-node-wrapper.py Publish reviewed bounded JWT ready handoff and retained owner history 2026-10-11 18:31:04 +02:00
openbao-negative-session.py Publish reviewed bounded JWT ready handoff and retained owner history 2026-10-11 18:31:04 +02:00
patch-openbao-client.sh feat(keycape): add netkingdom OIDC mount and bao.coulomb.social callbacks 2026-06-18 01:23:02 +02:00
playwright-activation.py security: reject Playwright audience and delivery alias collisions 2026-10-13 23:44:41 +02:00
playwright-registration.py security: reject Playwright audience and delivery alias collisions 2026-10-13 23:44:41 +02:00
policy-pvc.yaml Implement scoped P06 authentication policy and guarded optional onboarding 2026-09-14 00:00:09 +02:00
portal-client-rollout.py Record canonical portal hostname and callback cutover 2026-09-14 00:16:33 +02:00
README.md fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
refresh-pi-token-live.sh fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
register-coulomb-social.py Register app.coulomb.social OIDC redirect for coulomb-social 2026-08-09 23:20:30 +02:00
register-coulomb-social.sh Add KeyCape client registration for coulomb.social 2026-08-09 01:50:52 +02:00
register-user-engine-portal.py Record canonical portal hostname and callback cutover 2026-09-14 00:16:33 +02:00
secrets-engine-openbao-client-rollout.py Prepare the secrets-engine-openbao KeyCape client registration as source only 2026-10-06 13:35:12 +02:00
secrets-engine-openbao-client.proposed.patch.yaml Prepare the secrets-engine-openbao KeyCape client registration as source only 2026-10-06 13:35:12 +02:00
test_approval_clients_rollout.py Pin approval-clients rollout tests to their pre-rollout fixture 2026-09-23 19:56:55 +02:00
test_authentication_policy_rollout.py Implement scoped P06 authentication policy and guarded optional onboarding 2026-09-14 00:00:09 +02:00
test_issuer_pin.py Add contained issuer-only configuration check and revision-guarded pin 2026-09-09 00:23:21 +02:00
test_keycape_client.py NK-WP-0048: run keycape check-config before any registry patch 2026-10-10 15:28:05 +02:00
test_openbao_client_config.py Retire bao.coulomb.social callbacks from the KeyCape openbao-admin client 2026-09-23 19:41:35 +02:00
test_openbao_negative_session.py Bound JWT private input and gate native effects on public health 2026-10-11 17:30:10 +02:00
test_openbao_node_route.py Publish reviewed bounded JWT ready handoff and retained owner history 2026-10-11 18:31:04 +02:00
test_openbao_private_input.py Publish reviewed bounded JWT ready handoff and retained owner history 2026-10-11 18:31:04 +02:00
test_openbao_private_ready.py Publish reviewed bounded JWT ready handoff and retained owner history 2026-10-11 18:31:04 +02:00
test_playwright_activation.py security: reject Playwright audience and delivery alias collisions 2026-10-13 23:44:41 +02:00
test_playwright_registration.py security: reject Playwright audience and delivery alias collisions 2026-10-13 23:44:41 +02:00
test_portal_client_rollout.py fix(security): reconcile KeyCape bootstrap with scoped factor custody 2026-10-13 19:46:53 +02:00
test_secrets_engine_openbao_client_rollout.py Register secrets-engine-openbao live and fold its env binding in (NK-WP-0046-T03) 2026-10-07 02:20:08 +02:00
test_vergabe_client_rollout.py Bind password setup grants to approved company welcome pages 2026-09-12 02:43:31 +02:00
vergabe-client-rollout.py Bind password setup grants to approved company welcome pages 2026-09-12 02:43:31 +02:00
vergabe-fresh-login.proposed.patch.yaml Prepare pinned company-welcome and shared-issuer rollout patches 2026-09-12 02:53:55 +02:00
verify-openbao-client.sh Retire bao.coulomb.social callbacks from the KeyCape openbao-admin client 2026-09-23 19:41:35 +02:00
verify-user-engine-portal.py fix: restore native user portal login and track tenant integration 2026-09-11 20:55:30 +02:00

T05c — KeyCape (OIDC Orchestration Layer)

Exercise status: unknown for the procedures in this runbook; no per-procedure successful-run receipt with operator attribution was established in the 2026-09-05 review. See procedure inventory.

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.

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.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

First-bootstrap creation and factor custody

KeyCape uses the scoped keycape-factor-reader provider identity. Railiance Platform RPF-WP-0040 owns its renewable credential, the keycape-factor-read ExternalSecret, and the mounted /etc/keycape-factor/admin-token file. See the owner custody contract. Neither a personal pi-admin JWT nor a local pi_admin_token file belongs in KeyCape configuration. create-pi-token.sh and refresh-pi-token-live.sh are retired refusal entry points; they perform no login or changes.

Under an approved first-bootstrap procedure, establish owner factor delivery before running bash sso-mfa/k8s/keycape/create-secrets.sh /absolute/private/secrets. The helper requires Python 3 with PyYAML, OpenSSL and kubectl; owned private directories/files; literal generator-format LLDAP and Authelia credentials; Ready factor delivery; and an absent keycape-config. It preserves an existing valid private RSA key or atomically creates a missing one. It sends Secret data only over kubectl stdin, suppresses child output, and uses API creation without a last-applied annotation. Existing configuration is refused; concurrent creation cannot overwrite it. A failed API call can have an unknown remote outcome: reconcile metadata before retrying. Retain any newly generated private key for recovery, including after a failure.

The helper does not restart workloads or establish native login acceptance. Deployment and fresh factor/login checks belong to the reviewed bootstrap or recovery ceremony. Do not use it to restore an existing installation: preserve its current clients, policy, signing key and independently held backup.

Factor credential renewal and recovery

For expired factor credentials or mfa check error, use the owner renewal and recovery procedure in the custody contract: inspect the renewer and ESO status, repair the underlying provider/custody failure, then verify mounted delivery and native factor acceptance. A fresh credential is not predecessor revocation. Never restore the retired personal-admin-token path, change realm/MFA policy as part of a token refresh, or replace the full KeyCape configuration.

OIDC client registration

bootstrap-config.json is the initial configuration only. The helper fills its empty LLDAP/Authelia credential fields in memory and serializes ordinary YAML for KeyCape and the scoped configuration update tools. Its registrations are not a complete inventory of current clients. Live client changes require a scoped owner procedure that preserves the rest of the configuration and verifies the affected consumer. The legacy client-secret and RSA replacement rotations refuse before mutation; coordinated rotation, rollback custody and native multi-consumer acceptance remain NK-WP-0049-T02 work.

The openbao-admin client is intentionally a public PKCE client for the current operator flow. It registers the OpenBao CLI callback URIs and the operator-tunneled browser UI callback:

http://localhost:8250/oidc/callback
http://127.0.0.1:8250/oidc/callback
http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback

The browser UI callback is paired with the named Railiance Platform operator tunnel at http://127.0.0.1:18200. The public bao.coulomb.social callbacks were retired with that name on 2026-09-15; openbao-client-config.py and verify-t07.sh fail if either is registered again. The preferred browser auth mount is netkingdom; keycape remains a compatibility alias. Keep the CLI 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 enables and configures the OpenBao netkingdom and compatibility keycape OIDC mounts. It never writes the platform-admin role: the role's policies, callbacks and bound claims are declared centrally by railiance-platform at openbao/auth/netkingdom-platform-admin-role.json, and the script fails if the declared role is missing. Any additional allowance belongs in that declaration, not here. Browser operators should use the OpenBao UI at http://127.0.0.1:18200 through the named openbao-ui-railiance01 tunnel, 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.

Prepared, not live: secrets-engine-openbao service client (NK-WP-0046)

Source only for KeyCape KEY-WP-0037-T03; nothing here is registered or deployed. The live run needs the founder's go (decision caafe4be-4f68-47b4-9567-316d47b94992) and the platform-owned Secret keycape-secrets-engine-openbao-client (key client-secret), which does not exist yet.

  • secrets-engine-openbao-client-rollout.py — exact-match merge into sso/keycape-config: absent adds only this client, identical is a noop, duplicates/drift/shared secretRef refuse. --operation remove is the rollback and removes only this exact registration. Default mode is inspect; apply refuses without --live-go-decision caafe4be-… and, for add, until the Deployment already carries the env binding.
  • secrets-engine-openbao-client.proposed.patch.yaml — the single env secretKeyRef for KEYCAPE_SECRETS_ENGINE_OPENBAO_CLIENT_SECRET.
  • test_secrets_engine_openbao_client_rollout.py — synthetic fixtures only.

Order under the go: Secret materialised → server dry-run of both → env patch (pod restarts with an unused env) → config apply → restart → native issuance and denial checks (KEY-WP-0037-T04). Rollback reverses it: --operation remove, restart, then drop the env binding. The image pin for that rollout is not yet chosen; see NK-WP-0046.

Secrets managed

Secret name Keys Purpose
keycape-config config.yaml KeyCape configuration (LLDAP credentials, Authelia client secret, privacyIDEA URL + scoped credential-file reference, OIDC clients)
key.pem RSA-2048 private key for signing OIDC tokens issued to downstream applications
keycape-factor-read admin-token Scoped provider JWT delivered by owner-managed ESO; mounted and reloaded by KeyCape

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.