# Local Identity Local Identity is a zero-dependency, file-based user management capability for net-kingdom bootstrap environments — systems that do not yet have (or do not need) a running Keycloak instance. ## Why it exists In net-kingdom, Keycloak is the production identity provider. But Keycloak requires a running Kubernetes cluster, a database, and a configured realm before it can authenticate anyone. This creates a bootstrapping paradox: > You need identity to set up infrastructure, but the infrastructure provides > identity. Local Identity breaks this cycle. An operator with only a Linux home directory can establish their identity, generate deterministic test users, and run dev/test applications with OIDC authentication — before any service is deployed. ## Design principles 1. **Zero dependencies** — only the Linux filesystem; no Docker, no K8s, no running services required. 2. **Derived identity** — the primary user is derived from `$USER`, `/etc/passwd` (GECOS), and a configured email address. No manual setup required for the basic case. 3. **Deterministic test users** — two test users are auto-generated from the primary user at `init` time using `N` and `+testN` suffixes: | Field | Primary | Test 1 | Test 2 | |----------|-------------|---------------------|---------------------| | username | `$USER` | `${USER}1` | `${USER}2` | | fullname | GECOS field | `+test1` | `+test2` | | email | configured | `+test1@…` | `+test2@…` | Email aliases follow the Gmail `+xxx` convention so test emails route to the operator's inbox without extra accounts. 4. **Hard isolation** — test users carry `environment: local`; production connectors reject this flag by default. Test users cannot authenticate in production without an explicit override. 5. **Minimal OIDC** — a lightweight native OIDC provider backed by the file store, for apps that require OIDC in dev/test without a running Keycloak. Tokens carry `iss: local-identity`; production systems are configured to reject this issuer. 6. **Secure by default** — `~/.local-identity/` is created with mode `700`; individual user files with mode `600`; the tool validates permissions on every startup and refuses to run if the store is world-readable. ## What it is not - **Not a production identity provider.** Local Identity is never exposed to the internet. It has no MFA. It is not hardened for public traffic. - **Not a replacement for Keycloak.** Once a cluster is operational, Keycloak is the IdP. Local Identity provides an on-ramp, not an alternative. - **Not multi-user.** Local Identity is single-operator: one primary user derived from the Linux session, plus generated test users. - **Not an LDAP/AD/Entra bridge.** Enterprise federation is handled by Keycloak. See EP-NK-001 in the State Hub. - **No MFA.** Second factors are out of scope; this is intentionally minimal. ## User schema Users are stored as YAML files under `~/.local-identity/users/`: ```yaml # ~/.local-identity/users/tegwick.yaml schema_version: "1" username: tegwick fullname: "Bernd Worsch" email: "bernd.worsch@gmail.com" environment: local # never "production" for local-identity users generated: false # true for auto-generated test users production_identity: # optional: maps this user to a production identity username: tegwick realm: net-kingdom ``` Test users are generated at `init` time and stored alongside: ```yaml # ~/.local-identity/users/tegwick1.yaml schema_version: "1" username: tegwick1 fullname: "Bernd Worsch+test1" email: "bernd.worsch+test1@gmail.com" environment: local generated: true source_user: tegwick production_identity: # optional: can map to a test/staging account username: tegwick-test1 realm: net-kingdom ``` ## Sandbox → production mapping Each user file can optionally carry a `production_identity` block. When an entity owned by a local-identity user needs to be transferred to a production environment (e.g. a resource created during local development), the mapping provides the correct production user ID. `local-identity export ` produces a Keycloak-compatible user JSON that respects this mapping. The schema is validated against the Keycloak user representation to prevent silent drift. ## CLI reference ``` local-identity init # derive primary user, generate test users local-identity list # list all users in the store local-identity show # display user file local-identity export # emit Keycloak-compatible JSON local-identity serve [--port P] [--ttl T] # start minimal OIDC server local-identity security-check # validate filesystem permissions local-identity revoke-token # add a token JTI to the revocation list ``` ## OIDC provider (Stage 3) When running `local-identity serve`, a minimal OIDC Authorization Code flow server starts on localhost. It supports: - `GET /.well-known/openid-configuration` — discovery document - Authorization endpoint, token endpoint, userinfo endpoint - JWT tokens with `iss: local-identity` (hard-coded; production systems reject this issuer by default) - Auto-generated self-signed TLS certificate This allows dev/test applications to use standard OIDC libraries against Local Identity without any Keycloak dependency. **Security note:** the OIDC server binds to `127.0.0.1` only. Never expose it on a public interface. ## Risks and mitigations | Risk | Mitigation | |------|------------| | World-readable credential files | `~/.local-identity/` mode `700`; startup check fails loudly | | Test users leaking into production | `environment: local` flag; production connectors reject by default | | Local Identity tokens accepted in production | `iss: local-identity`; configure production Keycloak to reject this issuer | | File schema drifting from Keycloak model | `export` command validates against Keycloak representation; schema is versioned | | Bootstrap store becoming a long-lived crutch | Explicit scope limit: once Keycloak is operational, migrate and stop using Local Identity | ## Keycloak import procedure Once the Keycloak realm is operational (NK-WP-0001 T06), migrate the primary user from Local Identity into Keycloak using the partial import endpoint. **1. Export the primary user:** ```bash local-identity export --all --realm net-kingdom > /tmp/li-import.json # By default, only the primary user is exported (test users are excluded). # Check: the Note line on stderr confirms how many test users were skipped. ``` **2. Import via the Keycloak Admin REST API:** ```bash # Requires a Keycloak admin token TOKEN=$(curl -s -X POST https://keycloak.yourdomain.com/realms/master/protocol/openid-connect/token \ -d "client_id=admin-cli&grant_type=password&username=admin&password=" \ | jq -r .access_token) curl -s -X POST \ https://keycloak.yourdomain.com/admin/realms/net-kingdom/partialImport \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d @/tmp/li-import.json ``` **3. Set a password in Keycloak** (Local Identity does not export credentials): ```bash curl -s -X PUT \ https://keycloak.yourdomain.com/admin/realms/net-kingdom/users//reset-password \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"type":"password","value":"","temporary":false}' ``` **4. Retire Local Identity for this instance:** Once the user is operational in Keycloak, stop using `local-identity serve` for this environment and remove the store: `rm -rf ~/.local-identity`. ## Isolation guarantee All users exported by Local Identity carry the attribute: ```json "attributes": { "local_identity_environment": ["local"] } ``` Generated test users additionally carry `local_identity_generated: ["true"]`. ### Configuring Keycloak to reject local-identity users Add a condition to the Keycloak browser authentication flow that denies login for any user with `local_identity_environment = local`. This is a defence-in-depth measure: even if test users are accidentally imported into a production realm, they cannot authenticate. In Keycloak 23+, use a **Conditional Authenticator** with a User Attribute Condition: 1. In the realm's **Authentication → Flows → browser** flow, add a sub-flow. 2. Add the **Condition - User Attribute** authenticator. 3. Configure: attribute = `local_identity_environment`, value = `local`, negation = **false** (matches when attribute equals the value). 4. Set the sub-flow to **DENY** when the condition is true. Alternatively, use a Keycloak script authenticator or a custom policy enforcer. ### Production identity mapping Before importing, you can assign a `production_identity` block to the user so the Keycloak username differs from the local username: ```yaml # ~/.local-identity/users/tegwick.yaml production_identity: username: bworsch # the username used in the production realm realm: net-kingdom ``` Re-run `local-identity export --all` — the exported JSON will use `bworsch` as the Keycloak username and a deterministic UUID derived from `net-kingdom/bworsch`. ## Security model ### Threat model Local Identity is designed for **single-operator, localhost-only** use. The threat model covers accidental exposure, not active adversarial attack. | Threat | Control | |--------|---------| | Other local users reading credential files | `~/.local-identity/` mode `700`; user files mode `600`; startup check exits on violation | | Attacker elevates a local OIDC token to production | `iss: local-identity` rejected by production Keycloak; `environment: local` attribute rejected by Keycloak attribute check | | Stolen token used after the fact | Token revocation list (`revoke-token `); configurable TTL (default 1h) | | Long-lived store left behind post-migration | Explicit retirement step: `rm -rf ~/.local-identity` after Keycloak migration | | OIDC server exposed on non-loopback interface | Server hard-codes `127.0.0.1`; `0.0.0.0` binding is not offered | ### Assumptions - The operator's Linux account is not compromised (Local Identity cannot protect against a root-level attacker). - The `LOCAL_IDENTITY_HOME` environment variable is not set to a world-readable path by accident. - The operator's umask does not silently widen permissions before the tool can apply `os.chmod`. (The tool sets permissions explicitly after every write, which limits this window.) ### Non-guarantees - **No MFA.** Token issuance requires only user selection in the browser form; there is no second factor. - **No audit-log integrity.** `audit.log` is append-only by convention, but the OS does not enforce append-only at the file level without `chattr +a` (which requires root). The log records events; it does not prove they were not tampered with. - **Self-signed TLS is not CA-trusted.** The TLS certificate generated for `local-identity serve` is not signed by a trusted CA. OIDC clients must either skip certificate verification or import the certificate manually. - **No privilege separation.** All operations run as the operator's user. ### Optional SELinux / AppArmor hardening If your system uses SELinux or AppArmor you can apply labels to further restrict access to `~/.local-identity/`: **SELinux** (example — adapt context type for your policy): ```bash chcon -R -t user_home_t ~/.local-identity ``` **AppArmor** — create a profile snippet that denies access to `~/.local-identity/` from any process other than `local-identity`: ``` deny /home/*/.local-identity/ r, deny /home/*/.local-identity/** r, ``` These are optional hardening layers. The tool's own permission controls (mode 700/600, startup enforcement) provide the baseline. ### Token revocation Tokens issued by `local-identity serve` can be revoked at any time: ```bash # By JTI (extract from JWT payload manually or from audit.log): local-identity revoke-token # By passing the full JWT — the JTI is extracted automatically: local-identity revoke-token ``` Revoked JTIs are stored in `~/.local-identity/revoked.json` (mode 600). The revocation list is checked on every `/userinfo` request. There is no endpoint to un-revoke a token; if you need to re-grant access, obtain a new token via the authorization code flow. ## Relationship to the SSO platform Local Identity is a complementary workstream to the SSO & MFA Platform (NK-WP-0001). The SSO platform provides production-grade identity; Local Identity provides the bootstrap path that allows the SSO platform itself to be set up and tested. Implementation: see [NK-WP-0002](../workplans/NK-WP-0002-local-identity.md).