# M3.5.10 — Auth integration with Authentik/Vault | Field | Value | |---|---| | Phase | M3.5 — Distributed API Layer | | Size | M — 1–3 days | | Status | ⬜ Not started | | Flags | — | | Spec | inlined below | | Blocks | — | | Depends | M3.5.1 | ## Goal Replace the placeholder `apikey` header check with proper authentication via the cluster's IAM stack: **Authentik** (OIDC provider) → **HashiCorp Vault** (token issuer) → **memory service** (token validator). ## Facts (inlined — no spec read needed) **Current (wrong):** ```rust fn check_auth(req: &HttpRequest, state: &AppState) -> Result<(), HttpResponse> { let api_key = req.headers().get("apikey").and_then(|h| h.to_str().ok()); if api_key != Some(&state.api_key) { return Err(HttpResponse::Unauthorized().json(...)); } Ok(()) } ``` This is a raw string match against `MEM_API_KEY` env var. No JWT, no Vault, no user identity. It does not integrate with the cluster's IAM stack. **Cluster IAM stack:** - **Authentik** (`iam` namespace) — OIDC provider at `https://authentik.riotpiao.com/application/o/vault/` - **HashiCorp Vault** (`iam` namespace) — OIDC auth method enabled, validates Authentik JWTs, issues Vault tokens based on role/policy. - **Vault OIDC role:** `auth/oidc/role/homelab-admin` - `bound_claims: { "permissions": "*" }` - Policy: `homelab-admin` (path `"*"` full access) - **Vault unseal:** Shamir 3/3, keys in `vault-unseal-keys` secret, S3 backend via MinIO. **Auth flow (production):** ``` User/Agent authenticates with Authentik (OIDC) → Receives JWT with claims { sub, permissions, groups, ... } → Presents JWT to Vault OIDC auth method → Vault validates JWT against Authentik JWKS → Vault issues Vault token with matched policy → Client sends Vault token to memory service → Memory service validates token via Vault API ``` **Three integration options (pick one):** ### Option A: Vault token validation (recommended) Memory service receives `X-Vault-Token` header, calls Vault's `POST /v1/auth/token/lookup-self` to validate. Extracts policy and metadata. - Pro: Vault is the single source of truth for authorization. - Pro: Token revocation is immediate (Vault controls lifecycle). - Con: Extra network call per request (cache with TTL to mitigate). ### Option B: Direct JWKS validation Memory service fetches Authentik's JWKS endpoint, validates JWT `Authorization: Bearer ` directly. No Vault in the request path. - Pro: No Vault dependency at request time. - Pro: Standard OAuth2/OIDC pattern. - Con: Token revocation is delayed (until JWT expires). - Con: Memory service must know about Authentik's OIDC config. ### Option C: Trust gateway Memory service trusts homelab-frontend gateway (cluster-internal traffic). Gateway validates auth, forwards `X-User-Id` and `X-Capabilities` headers. Memory service checks capabilities against ServiceAdapter CRD requirements. - Pro: Auth logic centralized in gateway. - Pro: Memory service stays simple. - Con: Gateway auth is currently a stub (`hasCapability()` returns true for any `Authorization` header). - Con: Requires gateway auth to be completed first (homelab-frontend task 8.3). **ServiceAdapter CRD for memory (`memory-adapter`):** ```yaml auth: capability: memory:read # default required: true resources: - name: ingest methods: - verb: POST auth: { capability: memory:write, required: true } - name: query methods: - verb: POST - name: skills methods: - verb: GET ``` **Capabilities needed:** - `memory:read` — query, skills, vault browse, projects, sources - `memory:write` — ingest, source sync, skill draft ## Steps ### Option A (Vault token — recommended) 1. Add `vault_addr` to `AppState` (default: `http://vault.iam.svc.cluster.local:8200`). 2. Replace `check_auth()` with `validate_vault_token()`: ```rust async fn validate_vault_token(req: &HttpRequest, state: &AppState) -> Result { let token = req.headers().get("X-Vault-Token") .or_else(|| req.headers().get("Authorization")) // Bearer .and_then(|h| h.to_str().ok()); // POST vault_addr/v1/auth/token/lookup-self with X-Vault-Token header // Parse response: policies, metadata, ttl // Cache token -> identity for TTL duration } ``` 3. Add token cache (HashMap) with configurable TTL. 4. Extract `VaultIdentity` (policies, metadata) from lookup response. 5. Map policies to capabilities: `homelab-admin` → `memory:read` + `memory:write`. 6. Update each handler to check required capability. 7. Keep `apikey` as fallback for dev/test (controlled by env var `MEM_AUTH_MODE=vault|apikey`). ### For all options 8. Add env vars: `VAULT_ADDR`, `MEM_AUTH_MODE` (vault/jwks/gateway/apikey). 9. Update K8s deployment to inject `VAULT_ADDR`. 10. Update ServiceAdapter CRD if needed. 11. Document auth flow in README. ## Acceptance - Requests with valid Vault token are accepted. - Requests with expired/revoked Vault token are rejected (401). - Requests without any auth are rejected (401). - `memory:write` capability required for ingest/sync endpoints. - `memory:read` capability sufficient for query/skills/vault endpoints. - Token cache reduces Vault API calls on repeated requests. - Fallback to `apikey` mode for dev/test environments. ## Verify **Integration test** — `tests/it_auth_integration.rs`: 1. `a1_vault_token_accepted` — mock Vault lookup-self returning valid response; assert request proceeds. 2. `a2_expired_token_rejected` — mock Vault returning 403; assert 401 response. 3. `a3_no_auth_rejected` — request with no auth headers; assert 401. 4. `a4_write_requires_capability` — token with `memory:read` only; POST /ingest; assert 403. 5. `a5_read_with_read_capability` — token with `memory:read`; GET /query; assert proceeds. 6. `a6_token_cache_hit` — same token twice; assert Vault called once. 7. `a7_apikey_fallback` — `MEM_AUTH_MODE=apikey`; assert old behavior works. 8. `a8_auth_mode_configurable` — assert `MEM_AUTH_MODE` switches validation logic. **Command:** `cargo test --test it_auth_integration` **False pass:** - Testing only with apikey fallback. The Vault integration is the whole point. - Mocking Vault without testing cache expiry. A cache that never expires accepts revoked tokens forever. ## Traps - Calling Vault on every request without caching. Vault API calls add 5-10ms per request. Cache with TTL matching token TTL (or shorter). - Not handling Vault being temporarily unreachable. Return 503 (not 401) if Vault is down — "cannot verify" is not "unauthorized". - Hardcoding Vault addr. Use env var + service discovery. - Not supporting `Authorization: Bearer ` format alongside `X-Vault-Token`. Different clients use different conventions. --- Background: [DESIGN.md](../DESIGN.md) — auth section, Authentik/Vault IAM stack