Files
net-kingdom/docs/LocalIdentity.md
2026-05-02 16:58:44 +02:00

14 KiB

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 <fullname>+test1 <fullname>+test2
    email configured <user>+test1@… <user>+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/:

# ~/.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:

# ~/.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 <user> 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 <username>           # display user file
local-identity export <username>         # emit Keycloak-compatible JSON
local-identity bootstrap-oidc            # print local OIDC client settings
local-identity serve [--port P] [--ttl T]  # start minimal OIDC server
local-identity security-check           # validate filesystem permissions
local-identity revoke-token <jti|jwt>   # 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.

To bootstrap a local app against the provider, initialise the store and emit client settings:

local-identity init --email bernd@example.com
local-identity bootstrap-oidc \
  --client-id local-dev \
  --redirect-uri http://127.0.0.1:3000/callback
local-identity serve

bootstrap-oidc persists the client settings under oidc_clients in ~/.local-identity/config.yaml and prints environment variables:

OIDC_ISSUER=https://127.0.0.1:8443
OIDC_DISCOVERY_URL=https://127.0.0.1:8443/.well-known/openid-configuration
OIDC_CLIENT_ID=local-dev
OIDC_REDIRECT_URI=http://127.0.0.1:3000/callback
OIDC_SCOPE='openid profile email'
OIDC_TOKEN_ENDPOINT_AUTH_METHOD=none

Redirect URIs must be loopback URLs (127.0.0.1, localhost, or ::1). The server intentionally trusts local clients and does not require a client secret.

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:

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:

# 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=<admin-pw>" \
  | 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):

curl -s -X PUT \
  https://keycloak.yourdomain.com/admin/realms/net-kingdom/users/<user-id>/reset-password \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"type":"password","value":"<new-password>","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:

"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:

# ~/.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 <jti>); 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):

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:

# By JTI (extract from JWT payload manually or from audit.log):
local-identity revoke-token <jti-uuid>

# By passing the full JWT — the JTI is extracted automatically:
local-identity revoke-token <jwt-string>

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.