net-kingdom/docs/adr/ADR-0009-expanded-mode-keycloak-federation-topology.md
tegwick 5497e2c100
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
Accept ADR-0009 with tenant-selectable isolation upgrade path
Add shared-realm and dedicated-instance tiers selected by the IAM capability
role, binding reference points (issuer invariance, subject preservation,
tier-neutral declaration, drill), and NK-WP-0011-T09.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: sonnet
Assistant-Process: 295952@bnt-lap001
Assistant-Session: e93f64ad-516c-46eb-9666-aad8d300c477
2026-09-28 23:41:12 +02:00

149 lines
8.1 KiB
Markdown

---
id: NK-ADR-0009
type: architecture-decision-record
title: "Expanded-Mode Keycloak: Adoption Trigger and Federation Topology"
status: accepted
owner: net-kingdom
revision: "1"
decided: "2026-09-28"
last_reviewed: "2026-09-28"
review_interval: 12m
---
# ADR-0009 - Expanded-Mode Keycloak: Adoption Trigger and Federation Topology
**Status:** Accepted 2026-09-28, with the isolation-upgrade amendment in decision 3 (NK-WP-0011-T01)
**Date:** 2026-09-28
**Deciders:** Bernd Worsch, Claude
## Context
The live IAM Profile issuer is KeyCape (Authelia + LLDAP + privacyIDEA) at
`https://kc.coulomb.social`. No Keycloak Deployment exists. The architecture
doc says expanded mode is capability-driven, chiefly inbound enterprise
federation (Entra ID, AD, SAML), and leaves the per-tenant trigger and
dual-issuer rule open. ADR-0014 defines the `IAM` capability role as a tenant
operating its own dedicated IAM instance. ADR-0006 keeps flex-auth as the PDP.
ADR-0016 makes MFA a user preference with workload opt-in step-up. The user-engine
extension-point doc requires immutable provider subjects, tenant-scoped group
mappings, and no privilege from unmapped upstream claims. The estate is one
node with single-instance databases, so no option here buys HA.
NK-WP-0001 decision D2 (Keycloak as primary internal user store) predates all
of this.
## Decision
1. **Trigger.** A tenant moves to expanded mode only when all three hold: a
named tenant, a named owner of the upstream IdP, and a concrete federation
requirement the lightweight stack cannot meet. User count, or the wish to
have Keycloak, is not a trigger. Until then NK-WP-0011 implementation tasks
stay unstarted.
2. **Role: OIDC identity broker only.** Keycloak brokers upstream Entra ID
(OIDC), AD (LDAP federation) and generic SAML 2.0 IdPs (SAML as an
upstream, via Keycloak's identity-provider brokering). Downstream it issues
only IAM Profile OIDC/PKCE tokens. Emitting SAML assertions to downstream
applications is out of scope. Keycloak is not the primary user store
(**supersedes D2**), not the PDP, and not a secret store.
3. **Isolation tiers, with a defined upgrade path.** The realm is the tenant
boundary and `tenant:platform` is a separate, reserved realm. Isolation is
a tenant-selectable tier, expected to be a paid upgrade:
| Tier | Name | Boundary | Selected by |
| --- | --- | --- | --- |
| `shared-realm` | Default | Own realm in the shared broker: realm-level isolation only (shared process, database and admin plane) | Default for any federated tenant |
| `dedicated-instance` | Upgrade | Own broker instance and database: process-level isolation, own scale and performance | Tenant acquires the `IAM` capability role (ADR-0014) |
Realm admins in either tier may not alter IAM Profile semantics, the
platform realm, federation trust settings, or audit retention. Shared-realm
tenants must not be described as strongly isolated. tenant-engine owns
tenant existence, capability roles and plan; the tier is *derived* from
the `IAM` role, never stored as a second independent fact.
**Reference points that make the upgrade non-breaking.** These are
binding on implementation and documentation:
1. **Issuer invariance.** The tenant's issuer URL (host plus
`/realms/<tenant>`) is the same in both tiers. An upgrade moves the
routing behind the hostname (Traefik), not the issuer, so applications,
audiences and account bindings do not change. The realm name is
therefore chosen once, is immutable, and is derived from the tenant id.
2. **Subject preservation.** Keycloak-local user ids are the `sub`. Upgrade
migrates the realm with users and their original ids preserved, and
federated-identity links intact. A migration that changes any `sub`
fails the drill and is not permitted.
3. **Tier-neutral declaration.** Each tenant realm is declared in git as
sanitized realm configuration plus a small tenant manifest naming the
tenant id, realm name and issuer. The manifest carries no tier field; the
tier is derived from tenant-engine, and the same realm declaration
deploys to either tier.
4. **Owner-executed runbook and drill.** A documented
shared-to-dedicated procedure exists with a rehearsed drill before
any paid tier is offered: export, import into an isolated instance,
verify `sub` preservation and login, cut routing, keep the source
realm read-only for a retention window, then remove it. Rollback is
re-routing to the still-intact source realm until that window ends.
5. **Per-tier evidence.** Backup ownership, off-host custody and an
isolated restore proof are recorded per tier; the dedicated tier
has its own database consumer and OpenBao role.
6. **Billing is out of scope here.** Plan and pricing are tenant-engine
facts; this ADR only fixes the technical contract that the `IAM` role
selects. Downgrade is not defined and needs its own decision.
4. **Coexistence.** Each tenant has exactly one active issuer at a time,
recorded by tenant-engine and published as issuer configuration. KeyCape
stays unchanged and keeps serving lightweight tenants. Applications target
the IAM Profile and discover the issuer; they do not embed provider
choice. Account bindings remain keyed on issuer plus immutable subject, so
moving a tenant between issuers is a planned migration with an explicit
subject-mapping step, never a silent dual login.
5. **Assurance.** Federated login is AAL1 by default. Upstream MFA (for example
Entra Conditional Access) counts as higher assurance only through a
reviewed, per-IdP trust mapping written down before use. Privileged step-up
continues to use privacyIDEA under ADR-0016 and the proposed IAM v0.4
step-up. The privacyIDEA provider JAR is added only if a tenant needs
Keycloak-side MFA.
6. **Provisioning and mapping.** Follow the user-engine extension-point rules:
JIT creates a pending projection; group mappings are tenant-scoped,
versioned and deny ambiguous envelopes; no platform role from raw upstream
group names.
7. **Hostname and issuer.** Not chosen here. It is assigned by the package
owner under ADR-0015 with the first tenant, as a distinct host from
`kc.coulomb.social`; the issuer URL is then treated as immutable.
8. **Ownership.** net-kingdom owns this contract and conformance checks.
Packaging and runtime belong to the ADR-0015 owner; the database consumer to
railiance-platform; OpenBao roles to railiance-platform.
## Alternatives considered
- **Keycloak replaces KeyCape.** Rejected: unnecessary migration risk for
working tenants; contradicts capability-driven adoption.
- **Dedicated instance for every federated tenant.** Strongest isolation, but on
one node the cost is multiplied database, memory and patching for tenants
that do not need it. Offered as the paid `dedicated-instance` upgrade.
- **Realm-per-tenant only, including `IAM` tenants.** Contradicts ADR-0014's
meaning of `IAM`.
- **Keycloak as SAML IdP downstream too.** No consumer requires it; widens the
contract surface. Revisit on a named need.
- **Trust upstream MFA claims by default.** Rejected: assurance would depend on
each customer's tenant configuration.
- **Build ahead of demand.** Rejected: adds an issuer, a database and a
break-glass path with no owner or user.
## Consequences
- NK-WP-0011 T02 onward stay gated on the trigger in decision 1.
- T06 conformance uses `tools/iam-profile-conformance/` against IAM v0.3.
- tenant-engine needs an issuer-per-tenant field; this is a dependency to
raise with its owner, not something built here.
- Shared-broker realms cannot be described as strongly isolated tenants.
- The upgrade constrains the design now: immutable realm names, issuer
hostnames that can be re-routed, and `sub`-preserving realm migration.
- No HA is implied; T02 and T08 must name backup ownership, off-host custody
and an isolated restore proof.
## Open questions
- Which named tenant and IdP owner will trigger the first deployment?
- Does key-cape require changes for issuer-per-tenant discovery (EP-NK-001)?
- Which claim carries the federated-assurance mapping so flex-auth can consume it?