Files
poimen-memory/tasks/M3.5.10-auth-integration.md
T
Story Crater Bot 71ecf482e7
Build and Push / Test (push) Successful in 3m36s
Build and Push / Build and push image (push) Successful in 20s
docs: Add M7 source connectors (10 tasks), M3.5.10 auth integration, remove Kong refs
- M7.1-M7.10: Extensible SourceConnector trait, Obsidian/paperless/git/S3
  connectors, sync framework, CLI, HTTP endpoints, health monitoring, gate
- M3.5.10: Auth integration with Authentik OIDC → Vault token validation
- DESIGN.md: Add source connectors architecture, update auth to
  Authentik/Vault (Kong removed from cluster)
- INDEX.md: 75 tasks, 11 gates
- Fix all Kong references in M3.5.1 task
2026-08-26 16:56:39 -07:00

179 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# M3.5.10 — Auth integration with Authentik/Vault
| Field | Value |
|---|---|
| Phase | M3.5 — Distributed API Layer |
| Size | M — 13 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 <token>` 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<VaultIdentity, HttpResponse> {
let token = req.headers().get("X-Vault-Token")
.or_else(|| req.headers().get("Authorization")) // Bearer <token>
.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<token_hash, (VaultIdentity, Instant)>) 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 <token>` format alongside `X-Vault-Token`.
Different clients use different conventions.
---
Background: [DESIGN.md](../DESIGN.md) — auth section, Authentik/Vault IAM stack