Activate NK-WP-0009/0011; add tutorials slice and proposed ADR-0009
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

- docs/tutorials: template, OpenBao and SSH tutorials (unexercised)
- tools/tutorial-verify + make tutorials-verify (NK-WP-0009-T06)
- ADR-0009 proposed: expanded-mode Keycloak trigger and topology

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
This commit is contained in:
tegwick 2026-09-28 23:31:48 +02:00
parent 5c4bc16706
commit 0d460e3c02
11 changed files with 469 additions and 8 deletions

View file

@ -0,0 +1,111 @@
---
id: NK-ADR-0009
type: architecture-decision-record
title: "Expanded-Mode Keycloak: Adoption Trigger and Federation Topology"
status: proposed
owner: net-kingdom
revision: "1"
proposed: "2026-09-28"
last_reviewed: "2026-09-28"
review_interval: 12m
---
# ADR-0009 - Expanded-Mode Keycloak: Adoption Trigger and Federation Topology
**Status:** Proposed (awaiting Bernd's decision; 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 (proposed)
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.** Realms are the tenant boundary. The `tenant:platform` realm
is separate and reserved. A tenant without the `IAM` role gets a realm in
the shared broker; a tenant with the `IAM` role gets a dedicated broker
instance (ADR-0014). A shared broker gives realm-level, not process-level,
isolation, so it is acceptable only for tenants whose federation trust and
data classification allow that. Realm admins may not alter IAM Profile
semantics, the platform realm, federation trust settings, or audit
retention. tenant-engine, not Keycloak, is the source of truth for tenant
existence and capability roles.
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. Kept for `IAM`-role tenants.
- **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.
- 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?

36
docs/tutorials/README.md Normal file
View file

@ -0,0 +1,36 @@
# NetKingdom Security Pattern Tutorials
Hands-on paths for operating the canonical NetKingdom security patterns
(NK-WP-0009). Each tutorial is a file in this directory, written from
[`TEMPLATE.md`](TEMPLATE.md) and checked by `make tutorials-verify`.
## Rules
1. **Exercise status is mandatory.** Per
[`docs/attended-procedure-standard.md`](../attended-procedure-standard.md), a
tutorial header says `exercised <date> by <operator>` or `unexercised`.
Nothing is labelled exercised until someone has run it.
2. **Every concrete step names its owning repo.** This repo owns canon and
reference tooling only (see `SCOPE.md`); deployment belongs to owners.
3. **Verification and rollback are required**, not optional happy-path extras.
4. **No secrets, ever.** Tutorials show paths and commands, never values.
5. **Consume, don't copy.** Link owner runbooks; do not paste runtime
manifests. Use the named `openbao-ui-railiance01` tunnel, never a public
Bao URL (`bao.coulomb.social` is retired).
## Index
| Tutorial | Workplan task | Owners | Status |
| --- | --- | --- | --- |
| [OpenBao: consume, attend, recover](openbao-operating-path.md) | T03 | railiance-platform, net-kingdom | unexercised |
| [Short-lived SSH credentials](ssh-certificates-and-tunnels.md) | T04 | ops-warden, ops-bridge | unexercised |
Deferred (see NK-WP-0009): T02 object-storage STS (needs an owner-backed
issuer and refusal/lease proof — ADR-0008 is architecture, not evidence) and
T05 flex-auth protected consumer.
## Pattern mapping
NK-WP-0008 (the pattern library) has no file in this repo, so tutorials map to
the canonical documents directly: `docs/platform-identity-security-architecture.md`,
`docs/responsibility-map.md`, `docs/platform-root-custody.md`.

View file

@ -0,0 +1,41 @@
# <Tutorial title>
Exercise status: unexercised
Workplan task: NK-WP-NNNN-TNN
Pattern(s): <canonical doc and section this teaches>
## Outcome
<One sentence: what is true when you are done.>
## Prerequisites
- <Access, tools, and state required. Name the owner of each.>
## Architecture context
<Who owns what. Which boundaries this crosses.>
## Steps
1. **[owner: <repo>]** <step and command>
## Verification
Done when:
- <Observable, repeatable check. Command plus expected result.>
## Rollback
- <How to undo each step, or "not reversible" and why.>
## Threat checks
- <What could go wrong; what must never be logged, committed, or pasted.>
## Ownership notes
| Concern | Owner |
| --- | --- |
| <concern> | <repo> |

View file

@ -0,0 +1,76 @@
# OpenBao: consume, attend, recover
Exercise status: unexercised
Workplan task: NK-WP-0009-T03
Pattern(s): platform-root custody (`docs/platform-root-custody.md`); credential routing
## Outcome
You can reach the already-deployed private OpenBao, read a secret you are
entitled to, know which unseal custody model governs it, and follow the
attended recovery path without exposing shares or tokens.
## Prerequisites
- **[owner: ops-bridge]** `bridge` CLI and the named `openbao-ui-railiance01`
tunnel; an SSH certificate (see the SSH tutorial).
- **[owner: ops-warden]** `warden` CLI for credential routing.
- **[owner: railiance-platform]** OpenBao is already deployed and private.
Greenfield deployment is a lab exercise only, never against the live estate.
## Architecture context
OpenBao is the runtime secret authority. railiance-platform deploys and
operates it; net-kingdom owns the custody canon and the guarded bootstrap
console, which refuses live `bao operator init`. Three unseal custody models
exist (`docs/openbao-unseal-custody-models.md`); production blocks the
`sops-held-automation` lab model.
## Steps
1. **[owner: ops-warden]** Find the owner of your need:
`warden route find "read a database password" --json`.
2. **[owner: ops-bridge]** Check and, if needed, restore the tunnel:
`bridge status`, then `bridge up openbao-ui-railiance01`.
3. **[owner: railiance-platform]** Authenticate with your own identity and read
only the path the routing result names. Use the owner's
`railiance-platform/docs/openbao.md` for exact commands.
4. **[owner: net-kingdom]** Know your custody model:
`python3 tools/security-bootstrap-console/security_bootstrap_console.py openbao-unseal-custody-models`.
5. **[owner: net-kingdom + railiance-platform]** Recovery after a seal event
follows `docs/openbao-attended-ceremony-runbook.md`: operator and witness
present, shares escrowed out of band, root token revoked after handoff.
Record the ceremony in a non-secret record and run
`make security-bootstrap-validate-openbao-ceremony-record`.
## Verification
Done when:
- `bridge status` shows `openbao-ui-railiance01` up.
- `make security-bootstrap-console` reports no unmet custody gate for the
selected model.
- `make security-bootstrap-validate-openbao-ceremony-record` passes on a
ceremony record, and fails on one containing a token-shaped marker.
## Rollback
- Close the tunnel: `bridge down openbao-ui-railiance01`.
- Read-only steps need no rollback. A ceremony cannot be undone; a mistaken
share transcription is corrected by re-escrow per the custody roster.
## Threat checks
- Init output, shares and tokens go to the operator's screen only: never to
chat, State Hub, logs, or a Git checkout.
- Never use a public Bao URL; `bao.coulomb.social` is retired.
- Never place root token and unseal shares in one artifact outside lab.
## Ownership notes
| Concern | Owner |
| --- | --- |
| OpenBao deployment, config, unseal execution | railiance-platform |
| Custody canon, ceremony record validator | net-kingdom |
| Tunnel | ops-bridge |
| Credential routing | ops-warden |

View file

@ -0,0 +1,65 @@
# Short-lived SSH credentials for admins, agents and automations
Exercise status: unexercised
Workplan task: NK-WP-0009-T04
Pattern(s): credential routing; ops-warden `AccessManagementDirective`
## Outcome
An actor obtains a short-lived CA-signed SSH certificate and uses it through
an ops-bridge tunnel, with no static key doing the work.
## Prerequisites
- **[owner: ops-warden]** `warden` CLI installed; actor registered in the
principals inventory.
- **[owner: ops-bridge]** `bridge` CLI and a tunnel definition.
- **[owner: railiance-infra]** Target hosts trust the SSH CA and carry the
actor's principal.
## Architecture context
ops-warden issues SSH certificates only (`warden sign`). ops-bridge runs the
tunnel and calls the `cert_command` before each connect. Max TTLs: `adm` 48 h,
`agt` 24 h, `atm` 8 h; the caller refreshes about 5 minutes before expiry.
See `ops-warden/wiki/CertCommandInterface.md`.
## Steps
1. **[owner: ops-warden]** Sign a public key for the actor:
`warden sign <actor> --pubkey ~/.ssh/<actor>_ed25519.pub`.
2. **[owner: ops-bridge]** Set `cert_command` to that command in the tunnel
definition. Leave static-key mode unused.
3. **[owner: ops-bridge]** `bridge up <tunnel>` then `bridge status`.
4. **[owner: ops-warden]** Inspect the cert: `warden status`; audit history via
`warden log`.
## Verification
Done when:
- `ssh-keygen -L -f ~/.local/state/warden/<actor>-cert.pub` shows the expected
principal and a `Valid before` within the actor-type TTL.
- `warden status` exits 0 (it exits 1 if any cert is expired).
- After expiry, the tunnel reconnects only after `cert_command` succeeds.
## Rollback
- `bridge down <tunnel>`; `warden cleanup` removes stale certificates.
- Certificates expire on their own; there is no long-lived credential to
revoke. Remove the actor from the inventory to stop future signing.
## Threat checks
- Cert files must be mode 600; never reuse a cert across reconnects.
- A non-zero `cert_command` exit is a failure and must trigger backoff.
- ops-warden never vends API keys or passwords; route them with
`warden route find`.
## Ownership notes
| Concern | Owner |
| --- | --- |
| Certificate issuance and TTL policy | ops-warden |
| Tunnel lifecycle and refresh | ops-bridge |
| Host CA trust and principals | railiance-infra |