# KeyCape *Prepare for Keycloak without Keycloak* KeyCape is the lightweight IAM component of [NetKingdom](../net-kingdom/). It implements lightweight mode for the **NetKingdom IAM Profile** — a versioned OIDC/PKCE contract whose canonical core is now `../net-kingdom/canon/standards/iam-profile_v0.2.md` — by orchestrating Authelia, LLDAP, and privacyIDEA. The same profile is implemented by Keycloak in expanded-mode deployments. Applications integrate against the profile, not against Keycape internals. This makes the lightweight → expanded migration a tested, automated operation rather than a rewrite. ## Status **Implementation complete (v0.1).** All 23 workplan tasks implemented and tested. 21 test packages, all green. See `workplans/KEY-WP-0001-keycape-implementation.md`. ## Architecture ``` Application │ (NetKingdom IAM Profile v0.2) ▼ KeyCape ←── profile enforcement, claim normalization, telemetry / | \ Auth LLDAP privacyIDEA elia ``` **Expanded mode:** Replace KeyCape with Keycloak. Same profile contract, same conformance suite in `../net-kingdom/tools/iam-profile-conformance/`. ## Quick Start ```bash # Start the dev stack (KeyCape + LLDAP + Authelia + privacyIDEA) make dev # Build the server binary make build # Run all tests make test ``` ## Configuration KeyCape uses a YAML config file. See `config/dev-config.yaml` for a full example. ```yaml issuer: "https://auth.netkingdom.local" port: 8080 tokenLifetime: "15m" privateKeyPem: "/etc/keycape/key.pem" environment: "production" lldap: url: "ldap://lldap:389" bindDN: "cn=admin,dc=netkingdom,dc=local" bindPW: "secret" baseDN: "dc=netkingdom,dc=local" authelia: baseURL: "http://authelia.sso.svc.cluster.local:9091" browserBaseURL: "https://authelia.local" tokenBaseURL: "http://authelia.sso.svc.cluster.local:9091" clientId: "keycape" clientSecret: "secret" redirectURI: "https://auth.netkingdom.local/authorize/callback" privacyidea: baseURL: "https://privacyidea.local" adminToken: "secret" realm: "netkingdom" clients: - clientId: "my-app" displayName: "My Application" redirectUris: ["https://myapp.local/callback"] allowedScopes: ["openid", "profile", "email", "groups"] grantTypes: ["authorization_code"] clientType: "public" - 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" ``` Config is validated at startup — the server exits 1 with validation errors if config is invalid. `browserBaseURL` is used only for the human browser redirect to Authelia. `tokenBaseURL` is used for server-side code exchange. If either is omitted, KeyCape falls back to `baseURL`. ## Endpoints | Endpoint | Description | |---|---| | `GET /.well-known/openid-configuration` | OIDC discovery document | | `GET /jwks` | RS256 public key in JWK Set format | | `GET /authorize` | Authorization endpoint (PKCE required) | | `GET /authorize/callback` | Authelia callback handler | | `POST /authorize/callback` | privacyIDEA MFA challenge submission | | `POST /token` | Token exchange (authorization_code only) | | `GET /userinfo` | Userinfo endpoint (Bearer token required) | | `GET /healthz` | Health check → `{"status":"ok","version":"0.1.0"}` | ## Profile Constraints KeyCape enforces the NetKingdom IAM Profile. Violations return structured errors: | Error type | Meaning | |---|---| | `feature_not_supported_by_profile` | Feature is outside the profile entirely | | `available_in_keycloak_mode_only` | Available in expanded mode, not lightweight | | `rejected_for_profile_safety` | Would weaken security guarantees | | `invalid_profile_usage` | Supported feature used incorrectly | Enforced boundaries: no implicit flow, no wildcard redirect URIs, no dynamic client registration, no identity brokering, PKCE S256 required. Profile v0.2 also requires normalized tenant, principal type, groups, roles, scopes, and assurance evidence in tokens consumed by applications and flex-auth. ## Migration Tools KeyCape ships migration tools for the two orthogonal migration dimensions: **IAM migration (KeyCape → Keycloak):** ```bash # Export canonical data from LLDAP ./lldap-export --url ldap://lldap:389 --bind-dn cn=admin,... --output canonical-export.yaml # Transform to Keycloak realm import ./keycape-to-keycloak --input canonical-export.yaml --realm netkingdom --output keycloak-realm-import.json ``` **Directory migration (LLDAP → OpenLDAP / 389DS / AD):** ```bash ./lldap-to-ldap --input canonical-export.yaml --target openldap --base-dn dc=netkingdom,dc=local --output migration.ldif ``` Both migrations are independent. Perform either or both without affecting privacyIDEA MFA enrollment. ## LDAP Schema Validator ```bash # Validate in CI mode (strict) ./validator --mode ci --input directory-snapshot.yaml # Validate before provisioning ./validator --mode provisioning --input users.yaml ``` Validates: DN structure, required attributes, no unknown attributes, user references, no cyclic groups, username uniqueness, email format. ## Repo Structure ``` src/ cmd/ # Binary entrypoints keycape/ # Main server validator/ # LDAP schema validator lldap-export/ # Migration: LLDAP → canonical keycape-to-keycloak/ # Migration: canonical → Keycloak lldap-to-ldap/ # Migration: canonical → LDIF internal/ config/ # Config loading and validation domain/ # Canonical identity model (Go types) errors/ # Profile error taxonomy adapters/ # Backend adapters (Authelia, LLDAP, privacyIDEA) server/ # OIDC handlers + telemetry + enforcement migration/ # Migration logic validator/ # LDAP schema validation tests/ profile/ # Scenario A: lightweight baseline negative/ # Scenario D: unsupported feature rejection migration/ # Scenarios B & C: replacement tests spec/ canonical-model.yaml # Source of truth for all identity data ldap-schema.yaml # Canonical LDAP schema rules docs/adr/ # Architecture Decision Records workplans/ # Implementation workplans wiki/ # Specifications ``` ## Key Documents - `wiki/KeyCapeSpecification_v0.1.md` — Architecture, design intent, objectives - `wiki/KeyCapeSpecificationPack_v0.1.md` — Normative implementation spec - `docs/adr/ADR-0001-choose-go-for-keycape.md` — Language decision (Go vs Rust) ## Container Image The KeyCape image is published to the Gitea OCI registry on CoulombCore. **Registry:** `92.205.130.254:32166` **Image:** `92.205.130.254:32166/coulomb/key-cape` ### Pull ```bash docker pull 92.205.130.254:32166/coulomb/key-cape:latest ``` The registry runs over plain HTTP. Configure Docker to allow it: ```json // /etc/docker/daemon.json { "insecure-registries": ["92.205.130.254:32166"] } ``` ### Build and push locally ```bash # Build with default tag (latest) make image # Build with a specific tag IMAGE_TAG=dev make image # Push to registry (requires prior docker login) docker login 92.205.130.254:32166 make push # Push with a specific tag IMAGE_TAG=v1.0.0 make push ``` ### Tags | Trigger | Tags | |---------|------| | Push to `main` | `latest`, `main-` | | Tag `v1.2.3` | `1.2.3`, `1.2`, `1`, `latest` | ### CI (Gitea Actions) The workflow at `.gitea/workflows/image.yaml` builds and publishes automatically on every push to `main` and on semver tags (`v*`). Required Gitea Actions secrets on the `key-cape` repo: | Secret | Value | |--------|-------| | `REGISTRY_USER` | Gitea username or machine account (e.g. `ci-netkingdom`) | | `REGISTRY_TOKEN` | Gitea personal access token with `write:packages` scope | ## Domain Part of the **NetKingdom** domain. Tracked in the Custodian State Hub under domain `netkingdom`, repo slug `key-cape`. See `CLAUDE.md` for agent session protocol and workplan conventions.