Prepare for keycloak without keycloak
Find a file
tegwick d568b79223
All checks were successful
Build and Publish Container Image / build-and-push (push) Successful in 41s
Give the runtime real readiness, graceful shutdown and stated limits
Closes gap G08. /healthz returned a constant without probing anything, the server
called ListenAndServe with no signal handling, and the operational limits of
in-memory state, startup-loaded keys and local-only logout lived in code comments
rather than anywhere an operator would look.

/readyz probes LLDAP, Authelia and privacyIDEA; /healthz stays liveness and
probes nothing. Keeping them distinct matters: wiring liveness to dependency
health means an orchestrator restarts KeyCape when a dependency blinks, and a
restart also discards every in-flight login, so the reaction is worse than the
condition it reacts to.

LLDAP is probed with a bind rather than a dial, since a rotated or revoked
service password leaves the port open and every lookup failing -- exactly what
readiness should catch and exactly what a dial would miss. The response names the
failing check but never the reason: the endpoint is unauthenticated and upstream
error text carries hostnames and sometimes credentials-in-URLs. Results are
cached for 2s so an unauthenticated endpoint cannot be used to drive unbounded
upstream traffic, and probes run concurrently under a 3s bound so a hung
dependency makes the endpoint answer rather than hang with it.

SIGTERM and SIGINT now drain in-flight requests for 15s, under the 30s read/write
timeouts so a stuck request cannot outlive the window before SIGKILL.

docs/operations.md states the single-replica topology and why, and three limits
easy to get wrong: the constant key-1 key ID makes same-kid rotation a trap for
consumers caching JWKS, removing a client does not revoke its issued tokens, and
/logout is local only. No throughput figures are given, since nothing here
benchmarks KeyCape. Shared storage and refresh tokens stay excluded, as G08
allows.

Verified in the running executable: 503 naming all three checks failed while
/healthz returned 200, the LLDAP check flipping to ok once started, and 40/40
requests succeeding across a SIGTERM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NV9oijZukGyGbRQGGKnK4P

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713576@bnt-lap001
Assistant-Session: 384c511d-9bce-4cb8-a676-2aef6c0c8df6
2026-09-08 09:43:49 +02:00
.claude/rules docs: workplan-first agent guidance prose (CUST-WP-0055 T04 batch 5) 2026-07-08 19:51:38 +02:00
.forgejo/workflows Tier-2 Forgejo pilot: image build workflow on railiance01 runner 2026-07-04 10:23:38 +02:00
.gitea/workflows Align KeyCape image namespace with deployment 2026-05-24 17:17:37 +02:00
.github/workflows feat: implement T01-T04 — Go module, canonical model, LDAP validator, error taxonomy 2026-03-13 01:27:54 +01:00
bin Make the Keycloak transform preserve or name every policy field 2026-09-07 13:48:48 +02:00
capabilities/playbooks Implement KeyCape provider and service identity contracts 2026-08-23 13:10:13 +02:00
config Align approval registrations to the tenant:platform decision 2026-09-06 22:30:32 +02:00
docs Give the runtime real readiness, graceful shutdown and stated limits 2026-09-08 09:43:49 +02:00
history Give the runtime real readiness, graceful shutdown and stated limits 2026-09-08 09:43:49 +02:00
intakes Add bounded resource audiences and enforce browser scope grants 2026-09-05 00:41:17 +02:00
registry Draft capability entry (reuse-surface REUSE-WP-0017-T04, cohort 2) 2026-07-06 19:41:37 +02:00
scripts Prove the migration against live directories and fix what that surfaced 2026-09-08 00:29:34 +02:00
spec Make snapshot attribute validation enforce a real rule 2026-09-07 23:22:55 +02:00
src Give the runtime real readiness, graceful shutdown and stated limits 2026-09-08 09:43:49 +02:00
tools ops: restart identity provisioner during keycape rotation 2026-08-23 14:42:44 +02:00
wiki Reference IAM Profile v0.2 2026-05-22 14:35:29 +02:00
workplans Give the runtime real readiness, graceful shutdown and stated limits 2026-09-08 09:43:49 +02:00
.custodian-brief.md chore(consistency): sync task status from DB [auto] 2026-09-07 09:01:53 +02:00
.gitignore Add bounded resource audiences and enforce browser scope grants 2026-09-05 00:41:17 +02:00
.repo-classification.yaml Human-review .repo-classification.yaml (CUST-WP-0050 follow-up) 2026-06-22 17:56:17 +02:00
AGENTS.md docs(agents): repoint remote State Hub URL to the in-cluster address 2026-08-25 00:21:16 +02:00
CLAUDE.md Add credential routing instructions for all agent runtimes 2026-06-18 22:48:38 +02:00
docker-compose.dev.yml feat: implement T22, T18, T23 — dev stack, profile tests, server binary 2026-03-13 02:18:36 +01:00
docker-compose.scenario-b.yml Make the replacement harness runnable and target a live issuer 2026-09-07 23:32:11 +02:00
docker-compose.scenario-c.yml Prove the migration against live directories and fix what that surfaced 2026-09-08 00:29:34 +02:00
Dockerfile feat: implement T22, T18, T23 — dev stack, profile tests, server binary 2026-03-13 02:18:36 +01:00
INTENT.md Add bounded resource audiences and enforce browser scope grants 2026-09-05 00:41:17 +02:00
LICENSE Adopt Target Revenue Source License V1C1 (org-wide preliminary rollout) 2026-07-29 23:48:58 +02:00
Makefile Implement KeyCape provider and service identity contracts 2026-08-23 13:10:13 +02:00
README.md Add native verified login and service-token commands 2026-09-05 01:08:58 +02:00
SCOPE.md Give the runtime real readiness, graceful shutdown and stated limits 2026-09-08 09:43:49 +02:00
WORK-RECORDS.md chore(consistency): register KEY-WP-0024 [auto] 2026-09-08 09:00:39 +02:00

KeyCape

Prepare for Keycloak without Keycloak

KeyCape is the lightweight IAM component of NetKingdom. 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

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

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.

Confidential client_credentials registrations may set a per-client tokenLifetime from 1 minute through 1 hour. This overrides the server default for that client and lets machine-login consumers carry an explicit maximum TTL. See config/service-clients.example.yaml and docs/openbao-service-auth-contract.md for the non-secret OpenBao service identity contracts. Client secret values remain outside Git and are resolved only from the registered env: reference at startup.

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):

# 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):

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

# 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

docker pull 92.205.130.254:32166/coulomb/key-cape:latest

The registry runs over plain HTTP. Configure Docker to allow it:

// /etc/docker/daemon.json
{ "insecure-registries": ["92.205.130.254:32166"] }

Build and push locally

# 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-<short-sha>
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.

Native authentication commands

keycape service-token exchanges a custody-injected client secret for a verified JWT; keycape login performs public-client browser PKCE login. Both save tokens to a new private file outside Git and never print token values. See Native caller authentication for usage, supported bindings, renewal, and the distinction from OpenBao login.