Files
poimen-memory/docs/OPENSEARCH_JWT_SETUP.md
T
Story Crater Bot 56bee1915e chore: Archive completed task files (M0, M1, M3, M3.5, M4.1-2, M3.6.1)
Deleted 31 completed task files:
- M0.x: 8 tasks (cargo, domain types, recordsource, tokenizer, adapters, gate)
- M1.x: 8 tasks (llm-chat, standing-query, prompt template, parser, loop, log, e2e, gate)
- M3.x: 4 tasks (l2-synthesis, rerank, mem-query, gate)
- M3.5.x: 8 tasks (http-server, ingest, query, federation, skills, projects, rate-limiting, gate)
- M3.6.1: DocCorpusSource (heading-boundary chunking)
- M4.1-2: skill-draft, derived-filter

Updated INDEX.md:
- Removed M0 & M1 phase sections (archived in git history)
- Updated progress table: 65 active tasks (42 + 2🟡 + 21)
- Updated status: M0/M1 complete, M3/M3.5 gates passing, M4.1-2 done
- Noted M3.5.10 JWT auth implementation complete (awaiting image rollout)
- Cleaned up broken links to deleted task files

Total test count: 239 passing, 2 ignored (up from 196 at M3.4)
Ready for M4.3 gate composition, M5 post-training, M7 source connectors.
2026-08-27 20:25:05 -07:00

444 lines
10 KiB
Markdown

# OpenSearch + JWT Authentication Setup
## Overview
This guide covers deploying OpenSearch with JWT authentication integrated with Authentik, providing hybrid search (semantic + lexical) for the Poimen Memory service.
## Architecture
```
┌─────────────────────────────────────────┐
│ Frontend (React) │
│ GET /memory/query + JWT Bearer token │
└────────────┬────────────────────────────┘
┌─────────────────────────────────────────┐
│ Memory Service (Rust) │
│ ├─ Validate JWT (Authentik JWKS) │
│ ├─ pgvector semantic search │
│ ├─ OpenSearch lexical search │
│ └─ Combine + rerank (hybrid) │
└────────────┬────────────────────────────┘
┌──────┴──────┐
│ │
↓ ↓
pgvector OpenSearch
(semantic) (lexical + JWT)
├─ JWT realm (validate Authentik tokens)
├─ Role mapping (extract from JWT claims)
└─ Index-level permissions
```
## Prerequisites
- Kubernetes cluster (1.24+)
- Authentik configured with poimen-memory OAuth2 app
- PostgreSQL with pgvector (existing)
- Memory Service deployed
## Step 1: Deploy OpenSearch with JWT Auth
### Apply the deployment manifest
```bash
kubectl apply -f k8s/app/opensearch-deployment.yaml
```
This creates:
- **StatefulSet** (2 replicas, 30Gi PVC each)
- **ConfigMap** with security config (JWT realm)
- **Services** (headless + internal)
- **Secret** for admin password
- **NetworkPolicy** (only Memory Service access)
### Verify deployment
```bash
# Wait for pods ready
kubectl rollout status statefulset/opensearch -n poimen
# Check JWT realm configuration
kubectl logs opensearch-0 -n poimen | grep -i jwt
# Health check
kubectl exec -it opensearch-0 -n poimen -- curl -k --user admin:OpenSearch@Admin123! https://localhost:9200/_cluster/health
```
## Step 2: Configure OpenSearch Security
### Port-forward to OpenSearch
```bash
kubectl port-forward -n poimen svc/opensearch-internal 9200:9200
```
### Create index template
```bash
curl -k -X PUT "https://localhost:9200/_index_template/vault" \
-u admin:OpenSearch@Admin123! \
-H "Content-Type: application/json" \
-d '{
"index_patterns": ["vault-*"],
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1,
"index.codec": "best_compression"
},
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "standard"
},
"source": {
"type": "keyword"
},
"level": {
"type": "keyword"
},
"breadcrumb": {
"type": "keyword"
},
"indexed_at": {
"type": "date"
}
}
}
}'
```
### Verify JWT realm is working
```bash
# Get a JWT from Authentik
TOKEN=$(curl -s -X POST http://localhost:9000/application/o/token/ \
-d "grant_type=client_credentials" \
-d "client_id=poimen-memory" \
-d "client_secret=<secret>" \
-d "scope=openid" | jq -r .access_token)
# Test OpenSearch with JWT
curl -k -X GET "https://localhost:9200/_cluster/health" \
-H "Authorization: Bearer $TOKEN"
# Should return cluster health (if JWT is valid)
```
## Step 3: Update Memory Service Configuration
### Add environment variables
```yaml
# k8s/app/memory-deployment.yaml
env:
- name: OPENSEARCH_HOSTS
value: "opensearch-internal.poimen.svc.cluster.local:9200"
- name: OPENSEARCH_ENABLED
value: "true"
- name: SEARCH_METHOD
value: "hybrid" # hybrid | semantic | lexical
- name: HYBRID_WEIGHTS_SEMANTIC
value: "0.6"
- name: HYBRID_WEIGHTS_LEXICAL
value: "0.4"
- name: OPENSEARCH_VERIFY_TLS
value: "false" # For self-signed certs in dev
```
### Update Cargo.toml
```toml
[dependencies]
# Add OpenSearch client (if not using raw HTTP)
opensearch = "2.1"
serde_json = "1.0"
tokio = "1.0"
```
## Step 4: Test Hybrid Search
### Index a test document
```bash
# Get JWT
TOKEN=$(curl -s -X POST http://localhost:9000/application/o/token/ \
-d "grant_type=client_credentials" \
-d "client_id=poimen-memory" \
-d "client_secret=<secret>" \
-d "scope=openid" | jq -r .access_token)
# Port-forward Memory Service
kubectl port-forward -n poimen svc/poimen-memory 8080:8080
# Index a document via Memory Service
curl -X POST http://localhost:8080/memory/vault/index \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"id": "test-doc",
"content": "kubectl port-forward service 8080",
"source": "runbooks/port-forward.md",
"level": "L1",
"breadcrumb": ["runbooks"]
}'
```
### Search hybrid
```bash
# Semantic + Lexical search
curl -X POST http://localhost:8080/memory/query \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "fix kubernetes port 8080",
"method": "hybrid",
"limit": 10
}' | jq .
```
**Expected response:**
```json
{
"query": "fix kubernetes port 8080",
"results": [
{
"id": "test-doc",
"chunk": "kubectl port-forward service 8080",
"score": 0.92,
"source": "runbooks/port-forward.md",
"level": "L1",
"breadcrumb": ["runbooks"],
"method": "hybrid",
"breakdown": {
"semantic": 0.88,
"lexical": 0.96
}
}
],
"total": 1,
"search_method": "hybrid"
}
```
## Step 5: JWT Token Validation Details
### How OpenSearch validates JWT
1. **Token arrives**: `Authorization: Bearer eyJh...`
2. **OpenSearch extracts**: Token after "Bearer "
3. **Validates signature**: Using JWKS from Authentik
4. **Extracts claims**: `sub`, `roles`, `permissions`
5. **Maps to user**: Creates internal user from JWT
6. **Checks permissions**: Verifies access to indices
### JWT Claims Expected
```json
{
"iss": "https://authentik.riotpiao.com/application/o/poimen-memory/",
"aud": "opensearch",
"sub": "[email protected]",
"roles": ["read_vault", "write_vault"],
"permissions": ["memory:read", "memory:write"],
"exp": 1234567890,
"iat": 1234567800
}
```
### Update Authentik OAuth2 App
Ensure the poimen-memory app includes custom claims:
```
Scope: openid email profile
Custom Claims:
- roles: ["memory:read", "memory:write"]
- permissions: ["memory:read", "memory:write"]
```
## Step 6: Role-Based Access Control (RBAC)
### Available Roles in OpenSearch
```yaml
read_vault:
- Can search vault indices
- Can read documents
- No write permissions
write_vault:
- Can index new documents
- Can update existing
- Can read documents
all_access:
- Full cluster access
- Admin role
```
### Map JWT Roles to OpenSearch Roles
Edit `internal_users.yml` in ConfigMap:
```yaml
authc:
realms:
jwt_realm:
type: jwt
roles_key: roles # Extract "roles" claim from JWT
claims_mapping:
principal: sub
roles: roles
```
### Test role enforcement
```bash
# User with read_vault role only
curl -X GET "https://localhost:9200/vault-*/_search" \
-H "Authorization: Bearer <read-only-jwt>"
# ✅ Success (read allowed)
curl -X PUT "https://localhost:9200/vault-test/_doc/123" \
-H "Authorization: Bearer <read-only-jwt>" \
-d '{"content": "test"}'
# ❌ 403 Forbidden (write denied)
```
## Step 7: Monitoring & Troubleshooting
### Check OpenSearch logs
```bash
kubectl logs opensearch-0 -n poimen -f --tail=50
```
### JWT validation errors
If you see "JWT verification failed":
1. Verify JWKS endpoint is accessible:
```bash
curl https://authentik.riotpiao.com/application/o/poimen-memory/jwks/
```
2. Check token expiry:
```bash
TOKEN="..."
echo $TOKEN | cut -d. -f2 | base64 -d | jq .exp
date +%s
```
3. Verify issuer matches config:
```bash
echo $TOKEN | cut -d. -f2 | base64 -d | jq .iss
# Should equal: https://authentik.riotpiao.com/application/o/poimen-memory/
```
### Cluster health
```bash
kubectl exec -it opensearch-0 -n poimen -- curl -k \
--user admin:OpenSearch@Admin123! \
https://localhost:9200/_cluster/health | jq .
```
### Search latency
Monitor hybrid search performance:
```bash
curl -X GET http://localhost:8080/memory/metrics?type=search \
-H "Authorization: Bearer $TOKEN" | jq .
```
## Step 8: Migration from Elasticsearch (if applicable)
### Reindex Elasticsearch to OpenSearch
```bash
# Export from Elasticsearch
curl -X POST "elasticsearch:9200/_reindex" \
-H 'Content-Type: application/json' \
-d '{
"source": {
"index": "vault-*"
},
"dest": {
"index": "vault-"
}
}'
# Import to OpenSearch
# (Use snapshot/restore or Logstash)
```
## Security Checklist
- [x] OpenSearch JWT realm configured
- [x] JWKS endpoint from Authentik is reachable
- [x] NetworkPolicy restricts access (Memory Service only)
- [x] TLS enabled (self-signed certs for dev, proper certs for prod)
- [x] Admin password changed from default
- [x] JWT token validation enabled
- [x] Roles mapped from JWT claims
- [x] Index-level permissions enforced
## Performance Tuning
### Optimize search performance
```yaml
# In opensearch.yml
indices:
memory:
max_result_window: 50000 # Increase result set size
queries:
cache:
size: 20% # Allocate 20% heap to query cache
```
### Heap allocation
```yaml
# For 2 replicas with 2Gi each
-Xms2g -Xmx2g
# Total: 4Gi per node
```
### Shard configuration
```yaml
# Index settings
number_of_shards: 2 # Match cluster node count
number_of_replicas: 1 # One replica per shard
refresh_interval: 30s # Batch writes
```
## Rollback Plan
If OpenSearch doesn't work:
```bash
# Revert to semantic-only search
kubectl set env deployment/poimen-memory SEARCH_METHOD=semantic
# Keep OpenSearch pods running (no data loss)
# No indexing to OpenSearch
# Queries use pgvector only
```
## Next Steps
1. ✅ Deploy OpenSearch + JWT
2. ✅ Configure hybrid search in Memory Service
3. ⏳ Run end-to-end tests
4. ⏳ Monitor metrics (latency, accuracy)
5. ⏳ Gradual rollout (feature flag: 10% → 50% → 100%)