net-kingdom/sso-mfa/k8s/keycape/README.md
tegwick 710dc99585
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
docs: index privacyIDEA and KeyCape helper entry points from the NK49 caller ledger
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 696317@bnt-lap001
Assistant-Session: bab9e511-0915-45b3-926a-2fab552edff6
2026-10-23 00:19:09 +02:00

279 lines
13 KiB
Markdown

# 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](../../../docs/attended-procedure-inventory.md).
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)
```bash
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)
```bash
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:
```yaml
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](../../../../railiance-platform/docs/net-kingdom-credential-custody-contract.md).
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:
```text
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` enforces
the retired-callback restriction. `verify-t07.sh` now checks LDAP structure only
and cannot prove client registration. 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:
```bash
cd sso-mfa/k8s/keycape
# Legacy patch-openbao-client.sh is retired; use the owner-reviewed
# KeyCape registration/rollout adapter with exact revision and object guards.
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
# Legacy configure-openbao-oidc.sh is retired.
# Prepare the exact railiance-platform-owned change and run its governed admin operation
# through warden access openbao-platform-admin-login; do not supply a root token here.
```
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):
```yaml
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:
```yaml
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.
### Other registry helpers
- `authentication-policy-rollout.py` is for initial activation only. It migrates
the two reviewed baseline registrations and is kept for a greenfield or
recovery rebuild of the issuer. Later policy changes use the authenticated
policy journey.
- `create-secrets.py` creates the first-bootstrap config from private inputs.
It never replaces an existing Secret, mints a provider token or rotates a
signing key. Greenfield only.
- `openbao-signin-diagnosis-rollout.py` adds `signInDiagnosis: true` to the
existing `openbao-admin` registration (NK-WP-0051-T02, KEY-WP-0041-T02).
- `restart-readiness.py` is a source candidate for a bounded post-registration
restart and readiness check (NK-WP-0048-T12). It is not wired in yet.
## 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
```bash
# 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.
```