--- id: NK-WP-0002 type: workplan title: "Local Identity — Bootstrap User Store & Minimal OIDC" domain: netkingdom status: completed owner: worsch topic_slug: netkingdom state_hub_workstream_id: 7c9021b1-319c-4b4a-a8be-0642239a1893 created: "2026-03-01" updated: "2026-03-05" --- # Local Identity — Bootstrap User Store & Minimal OIDC ## Summary Implement a zero-dependency, file-based user management capability for net-kingdom environments that do not yet have (or do not need) a running Keycloak instance. Local Identity derives the primary user from the Linux identity, auto-generates test users, provides a sandbox→production mapping mechanism, and (in Stage 3) a minimal native OIDC provider for dev/test use. See [docs/LocalIdentity.md](../docs/LocalIdentity.md) for the full capability description, design principles, user schema, and risk mitigations. ## Context Resolved from Decision D5 (2026-03-01, Tegwick). The decision chose to implement Local Identity in-repo (not as a separate repository) in staged workplan form, with a clear scope boundary and explicit out-of-scope limitations. The minimal OIDC provider is to be implemented natively to avoid heavy dependencies, keeping the bootstrap footprint minimal. ## Relationship to NK-WP-0001 Local Identity is complementary to the SSO & MFA Platform (NK-WP-0001). It is not a blocking dependency: the SSO platform core deployment (T01–T08) does not require Local Identity to be complete. However: - NK-WP-0001 T07 (user management) references Local Identity for the pre-Keycloak bootstrap use case. - Stage 2 of this workplan produces Keycloak-compatible user exports, which feed the NK-WP-0001 T06 realm configuration. - Once NK-WP-0001 is fully operational, Local Identity is no longer needed for new instances and should be explicitly migrated away from. ## Architecture ``` ~/.local-identity/ ├── config.yaml # operator email, optional overrides └── users/ ├── .yaml # primary user (derived from Linux identity) ├── 1.yaml # test user 1 (generated) └── 2.yaml # test user 2 (generated) local-identity CLI ├── init # derive + generate users ├── list / show # read operations ├── export # Keycloak-compatible JSON ├── security-check # permissions validation └── serve # Stage 3: minimal OIDC server (localhost only) ``` **Secret injection:** Local Identity does not use Vault or K8s Secrets — it operates entirely at the filesystem level, pre-cluster. This is by design. ## Tasks ### T01 — Stage 1: Core file store ```task id: NK-WP-0002-T01 state_hub_task_id: 656652dd-05af-4fa4-95b2-17ce029ac7bd status: done priority: high commit: 4491bea ``` Define YAML user schema (`schema_version`, `username`, `fullname`, `email`, `environment`, `generated`, `source_user`, `production_identity`). Implement: - `local-identity init` — read `$USER`, `/etc/passwd` GECOS, prompt for email if not in config; write primary user file; auto-generate two test users with `N` / `+testN` suffixes - `local-identity list` — tabular output of all users in the store - `local-identity show ` — pretty-print user YAML File store: - Create `~/.local-identity/` with mode `700` - Create user files with mode `600` - Refuse to overwrite existing store without `--force` Unit tests: - GECOS name parsing edge cases (missing fields, non-ASCII) - Test user derivation: username suffix, email `+testN` insertion - Idempotency: `init` twice with `--force` produces identical output **Done when:** init/list/show work; files created with correct permissions; unit tests passing. --- ### T02 — Stage 2: Bootstrap integration ```task id: NK-WP-0002-T02 state_hub_task_id: 5ea6e68d-7ebe-4ea7-b92e-61aac17ff04c status: done priority: high commit: dad8365 ``` Extend user schema with optional `production_identity` block (`username`, `realm`). Test users carry `environment: local` and `generated: true`. Implement: - `local-identity export ` — emit Keycloak-compatible user JSON (Keycloak Admin REST API representation); apply `production_identity` mapping if present - Schema validation: run against Keycloak user JSON schema on export; fail with a clear diff if schema has drifted Bootstrap tooling integration: - `local-identity export --all` produces a bulk import file compatible with Keycloak's partial import endpoint - Document the import procedure in `docs/LocalIdentity.md` Isolation guarantee: - Production connectors (Keycloak, future services) must reject users with `environment: local` — document the configuration required on the Keycloak side (e.g. custom attribute check in authentication flow) **Done when:** export produces valid Keycloak JSON; schema validation catches drift; bulk import procedure documented and tested against a local Keycloak dev instance. --- ### T03 — Stage 3: Minimal native OIDC provider ```task id: NK-WP-0002-T03 state_hub_task_id: eb09d287-8e08-4c88-8bd1-6f0501ef5fc8 status: done priority: medium commit: d35823d ``` Implement `local-identity serve` — a minimal OIDC Authorization Code flow server, implemented natively (no heavy OIDC library dependencies). Target: a single binary or script that can be invoked without installing an application framework. Endpoints required: - `GET /.well-known/openid-configuration` — OIDC discovery document - `GET /auth` — authorization endpoint (redirects with `code`) - `POST /token` — token endpoint (exchanges `code` for JWT) - `GET /userinfo` — userinfo endpoint Token requirements: - JWT signed with a local key (generated on first `serve` invocation; stored in `~/.local-identity/keys/`) - Claims: `sub`, `iss: local-identity`, `aud`, `exp`, `iat`, `email`, `name`, `preferred_username` - `iss: local-identity` is intentionally non-routable; configure production Keycloak to reject tokens with this issuer TLS: - Auto-generate a self-signed certificate on first run; store in `~/.local-identity/tls/` - Bind to `127.0.0.1` only; document that external binding is explicitly unsupported Scope: - Supports `openid`, `profile`, `email` scopes - No refresh tokens (stateless; re-auth required after expiry) - No client secret validation (dev-mode only; all registered clients are trusted) **Done when:** a standard OIDC client library can authenticate against `local-identity serve`; discovery, auth, token, and userinfo endpoints pass an OIDC conformance smoke test; server refuses to bind to 0.0.0.0. --- ### T04 — Stage 4: Security hardening ```task id: NK-WP-0002-T04 state_hub_task_id: 936de7fa-dfb4-48a2-804f-6b9bd7271a05 status: done priority: medium commit: e7bafd6 ``` Permission enforcement: - On every startup, validate `~/.local-identity/` mode `700` and all user files mode `600`; fail loudly (exit 1 + clear error) if violated - `local-identity security-check` command: explicit security audit with per-check output (pass / warn / fail) Audit log: - Append-only log at `~/.local-identity/audit.log`; mode `600` - Log entries: timestamp, command, username, outcome - For `serve`: log every authentication event (auth request, token issued, userinfo call) Token hardening (for Stage 3 OIDC server): - Configurable token TTL (default: 1 hour) - Token revocation list stored in `~/.local-identity/revoked.json` - `local-identity revoke-token ` command Documentation: - Optional SELinux/AppArmor label guidance added to `docs/LocalIdentity.md` - Security model section: threat model, assumptions, explicit non-guarantees **Done when:** security-check passes cleanly on a correct install; audit log records all auth events; startup fails on incorrect permissions; token expiry and revocation functional. --- ## Deliverables Checklist - [x] `~/.local-identity/` store initialised from Linux identity; test users generated - [x] `local-identity list / show / export` working; Keycloak export validated - [x] Minimal OIDC server passes conformance smoke test; binds localhost only - [x] Filesystem permissions enforced on startup; `security-check` passes - [x] Audit log recording all auth events - [x] `docs/LocalIdentity.md` complete with import procedure and security model - [x] NK-WP-0001 T07 migration procedure documented (Local Identity → Keycloak) ## Open Questions None at this stage. All decisions resolved. Stage 3 language selection (implementation language for the OIDC server) is a task-level detail to be determined in T03.