generated from coulomb/repo-seed
344 lines
14 KiB
Markdown
344 lines
14 KiB
Markdown
# 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/`:
|
|
|
|
```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 <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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:**
|
|
|
|
```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=<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):
|
|
|
|
```bash
|
|
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:
|
|
|
|
```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 <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):
|
|
```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 <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](../workplans/NK-WP-0002-local-identity.md).
|