feat(local-identity): Stage 4 — security hardening (NK-WP-0002-T04)

Permission enforcement on startup: enforce_permissions() checks store dir
(700), user files (600), signing key, TLS key, audit.log, revoked.json.
CLI and run_server() call it before any sensitive operation.

New modules:
  security.py  check_store(), enforce_permissions(), print_security_check()
  audit.py     log_event() — append-only TSV audit log (mode 600)
  revoke.py    revoke(jti), is_revoked(jti) — revocation list (mode 600)

New CLI commands:
  security-check          Print per-check pass/warn/fail report; exit 1 on failure
  revoke-token <jti|jwt>  Add JTI to revocation list; accepts raw JTI or full JWT

Serve integration:
  Audit log written for auth request, token issuance, and userinfo calls
  Revocation checked at /userinfo; revoked tokens return 401

Docs: security model section in LocalIdentity.md — threat model,
assumptions, non-guarantees, SELinux/AppArmor guidance, revocation usage.

138 tests passing (34 new for Stage 4).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-02 08:06:56 +01:00
parent ae348d0e54
commit e7bafd69fc
9 changed files with 795 additions and 16 deletions

View File

@@ -107,11 +107,13 @@ 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
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 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)
@@ -228,6 +230,81 @@ production_identity:
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