export.py:
- split_fullname(): last-token strategy (Bernd Worsch → firstName/lastName)
- _deterministic_id(): uuid5(DNS, "local-identity.{realm}.{username}") for stable,
re-import-idempotent Keycloak IDs
- user_to_keycloak(): full Keycloak Admin REST API user representation;
production_identity mapping applied to username + realm; isolation attributes
(local_identity_environment, local_identity_generated) always present;
validate_keycloak_user() called on every conversion to catch schema drift
- bulk_export_body(): partial import body (ifResourceExists/realm/users)
cli.py: add `export` subcommand
- export <username> single user, prints Keycloak JSON
- export (no args) bulk; primary users only; stderr note on skipped test users
- export --include-test bulk; all users including generated
- --realm / --if-resource-exists flags
docs/LocalIdentity.md: add two new sections
- Keycloak import procedure: export → partialImport API → password reset → retire
- Isolation guarantee: attribute schema, Keycloak Condition authenticator config,
production_identity mapping walkthrough
tests/test_export.py: 34 new tests (88 total, all passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.3 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
-
Zero dependencies — only the Linux filesystem; no Docker, no K8s, no running services required.
-
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. -
Deterministic test users — two test users are auto-generated from the primary user at
inittime usingNand+testNsuffixes:Field Primary Test 1 Test 2 username $USER${USER}1${USER}2fullname GECOS field <fullname>+test1<fullname>+test2email configured <user>+test1@…<user>+test2@…Email aliases follow the Gmail
+xxxconvention so test emails route to the operator's inbox without extra accounts. -
Hard isolation — test users carry
environment: local; production connectors reject this flag by default. Test users cannot authenticate in production without an explicit override. -
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. -
Secure by default —
~/.local-identity/is created with mode700; individual user files with mode600; 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 security-check # validate filesystem permissions and config
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:
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:
- In the realm's Authentication → Flows → browser flow, add a sub-flow.
- Add the Condition - User Attribute authenticator.
- Configure: attribute =
local_identity_environment, value =local, negation = false (matches when attribute equals the value). - 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.
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.