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

6.9 KiB
Raw Blame History

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):

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):

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):

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

  1. Add vault_addr to AppState (default: http://vault.iam.svc.cluster.local:8200).
  2. Replace check_auth() with validate_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
    }
    
  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-adminmemory: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

  1. Add env vars: VAULT_ADDR, MEM_AUTH_MODE (vault/jwks/gateway/apikey).
  2. Update K8s deployment to inject VAULT_ADDR.
  3. Update ServiceAdapter CRD if needed.
  4. 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 testtests/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_fallbackMEM_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 — auth section, Authentik/Vault IAM stack