Tier-2 Forgejo migration pilot — container image CI
Find a file
tegwick f45784f951 Make INTENT.md self-coherent
Remove external reference points so the intent stands on its own at the
abstract, stable level. The IAM profile this repo implements is described
as a versioned profile contract rather than attributed to an external
owner, and the heavier comparison mode is described generically instead of
by product name. All of KeyCape's own substance is preserved — purpose,
primary utility, intended users, strategic role and boundaries, design
principles, maturity target, and stability note.

Relationships to other systems belong in interface contracts and the
orchestration responsibility map, not in intent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 01:50:08 +02:00
.claude/rules Refresh agent instruction files 2026-05-18 16:55:43 +02:00
.gitea/workflows feat(image): KEY-WP-0002 T01/T02/T06 — Makefile image targets, Gitea Actions workflow, README CI docs 2026-03-21 23:27:39 +00:00
.github/workflows feat: implement T01-T04 — Go module, canonical model, LDAP validator, error taxonomy 2026-03-13 01:27:54 +01:00
config feat: implement T22, T18, T23 — dev stack, profile tests, server binary 2026-03-13 02:18:36 +01:00
docs/adr feat: implement T01-T04 — Go module, canonical model, LDAP validator, error taxonomy 2026-03-13 01:27:54 +01:00
scripts feat: implement T19, T20 — Scenario B/C replacement tests; complete workplan 2026-03-13 02:36:29 +01:00
spec feat: implement T01-T04 — Go module, canonical model, LDAP validator, error taxonomy 2026-03-13 01:27:54 +01:00
src fix(authelia): use adapter's own client_id/redirect_uri in AuthorizeURL 2026-03-25 03:15:36 +00:00
wiki chore: track specification documents 2026-03-13 00:30:46 +01:00
workplans feat(close): mark KEY-WP-0002 done — all 6 tasks complete 2026-03-21 23:33:28 +00:00
.custodian-brief.md chore(consistency): sync task status from DB [auto] 2026-03-26 17:47:47 +01:00
.gitignore Initial commit 2026-03-12 23:11:30 +00:00
AGENTS.md Refresh agent instruction files 2026-05-18 16:55:43 +02:00
CLAUDE.md Refresh agent instruction files 2026-05-18 16:55:43 +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
Dockerfile feat: implement T22, T18, T23 — dev stack, profile tests, server binary 2026-03-13 02:18:36 +01:00
INTENT.md Make INTENT.md self-coherent 2026-05-21 01:50:08 +02:00
LICENSE Initial commit 2026-03-12 23:11:30 +00:00
Makefile feat(image): KEY-WP-0002 T01/T02/T06 — Makefile image targets, Gitea Actions workflow, README CI docs 2026-03-21 23:27:39 +00:00
README.md feat(image): KEY-WP-0002 T01/T02/T06 — Makefile image targets, Gitea Actions workflow, README CI docs 2026-03-21 23:27:39 +00:00
SCOPE.md Scope update from repo-scoping refactor 2026-05-01 12:26:34 +02: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.