Bernd Worsch 7822ba0703
Some checks failed
Build and Publish Container Image / build-and-push (push) Has been cancelled
feat(image): KEY-WP-0002 T01/T02/T06 — Makefile image targets, Gitea Actions workflow, README CI docs
- Makefile: add IMAGE_REGISTRY/IMAGE_REPO/IMAGE_TAG vars + image, push, image-tag targets
- .gitea/workflows/image.yaml: build+push on main push and v* tags via metadata-action
- README: Container Image section with pull/build/push/CI secret docs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 23:27:39 +00:00
2026-03-13 00:30:46 +01:00
2026-03-12 23:11:30 +00:00
2026-03-12 23:11:30 +00:00

KeyCape

Prepare for Keycloak without Keycloak

KeyCape is the lightweight IAM component of NetKingdom. It implements the NetKingdom IAM Profile — a versioned OIDC/PKCE contract — 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)
    ▼
 KeyCape  ←── profile enforcement, claim normalization, telemetry
  /  |  \
Auth  LLDAP  privacyIDEA
elia

Expanded mode: Replace KeyCape with Keycloak. Same profile, same tests pass.

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: "https://authelia.local"
  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"

Config is validated at startup — the server exits 1 with validation errors if config is invalid.

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

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/netkingdom/key-cape

Pull

docker pull 92.205.130.254:32166/netkingdom/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.

Description
Prepare for keycloak without keycloak
Readme MIT-0 312 KiB
Languages
Go 98.3%
Shell 1.2%
Makefile 0.4%