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
279 lines
13 KiB
Markdown
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.
|
|
```
|