- 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
6.9 KiB
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):
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 (
iamnamespace) — OIDC provider athttps://authentik.riotpiao.com/application/o/vault/ - HashiCorp Vault (
iamnamespace) — OIDC auth method enabled, validates Authentik JWTs, issues Vault tokens based on role/policy. - Vault OIDC role:
auth/oidc/role/homelab-adminbound_claims: { "permissions": "*" }- Policy:
homelab-admin(path"*"full access)
- Vault unseal: Shamir 3/3, keys in
vault-unseal-keyssecret, 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 anyAuthorizationheader). - Con: Requires gateway auth to be completed first (homelab-frontend task 8.3).
ServiceAdapter CRD for memory (memory-adapter):
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, sourcesmemory:write— ingest, source sync, skill draft
Steps
Option A (Vault token — recommended)
- Add
vault_addrtoAppState(default:http://vault.iam.svc.cluster.local:8200). - Replace
check_auth()withvalidate_vault_token():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 } - Add token cache (HashMap<token_hash, (VaultIdentity, Instant)>) with configurable TTL.
- Extract
VaultIdentity(policies, metadata) from lookup response. - Map policies to capabilities:
homelab-admin→memory:read+memory:write. - Update each handler to check required capability.
- Keep
apikeyas fallback for dev/test (controlled by env varMEM_AUTH_MODE=vault|apikey).
For all options
- Add env vars:
VAULT_ADDR,MEM_AUTH_MODE(vault/jwks/gateway/apikey). - Update K8s deployment to inject
VAULT_ADDR. - Update ServiceAdapter CRD if needed.
- 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:writecapability required for ingest/sync endpoints.memory:readcapability sufficient for query/skills/vault endpoints.- Token cache reduces Vault API calls on repeated requests.
- Fallback to
apikeymode for dev/test environments.
Verify
Integration test — tests/it_auth_integration.rs:
a1_vault_token_accepted— mock Vault lookup-self returning valid response; assert request proceeds.a2_expired_token_rejected— mock Vault returning 403; assert 401 response.a3_no_auth_rejected— request with no auth headers; assert 401.a4_write_requires_capability— token withmemory:readonly; POST /ingest; assert 403.a5_read_with_read_capability— token withmemory:read; GET /query; assert proceeds.a6_token_cache_hit— same token twice; assert Vault called once.a7_apikey_fallback—MEM_AUTH_MODE=apikey; assert old behavior works.a8_auth_mode_configurable— assertMEM_AUTH_MODEswitches 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 alongsideX-Vault-Token. Different clients use different conventions.
Background: DESIGN.md — auth section, Authentik/Vault IAM stack