miroir/docs/operations/secrets-setup.md
jedarden 04f1d47909 P3.3.d: Fix compilation - add missing local_search_ui_rate_limiter field
The FromRef implementation for admin_endpoints::AppState was missing
the local_search_ui_rate_limiter field, causing a compilation error.

This completes P3.3.d Redis backend extras, which were already fully
implemented:
- Rate-limit keys with EXPIRE (miroir:ratelimit:searchui:<ip>,
  miroir:ratelimit:adminlogin:<ip>, miroir:ratelimit:adminlogin:backoff:<ip>)
- Scoped-key coordination (miroir:search_ui_scoped_key:<index>,
  miroir:search_ui_scoped_key_observed:<pod>:<index> with EXPIRE 60s)
- Pub/Sub for admin session revocation (miroir:admin_session:revoked)
- CDC overflow buffer (miroir:cdc:overflow:<sink> with LPUSH + LTRIM)

All acceptance criteria verified by existing tests:
- test_redis_rate_limit_searchui verifies EXPIRE is set
- test_redis_pubsub_session_invalidation verifies <100ms propagation
- test_redis_cdc_overflow verifies LLEN matches bytes published

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 11:18:02 -04:00

247 lines
6.4 KiB
Markdown

# Miroir Secrets Setup Guide (plan §9)
This guide covers setting up Miroir secrets with OpenBao and External Secrets Operator (ESO).
## Prerequisites
- OpenBao deployed in the cluster with KV v2 secrets engine enabled
- External Secrets Operator installed
- Kubernetes cluster with miroir namespace
## Secret Inventory (plan §9)
| Secret | OpenBao Key | ESO Target | Rotation |
|--------|-------------|------------|----------|
| masterKey | master_key | miroir-secret | Manual |
| nodeMasterKey | node_master_key | miroir-secret | Zero-downtime |
| adminApiKey | admin_api_key | miroir-secret | Manual |
| adminSessionSealKey | admin_session_seal_key | miroir-secret | Manual |
| searchUiJwtSecret | search_ui_jwt_secret | miroir-secret | Zero-downtime |
| searchUiJwtSecretPrevious | search_ui_jwt_secret_previous | miroir-secret | Overlap only |
| searchUiSharedKey | search_ui_shared_key | miroir-secret | Manual |
| redis-password | redis_password | miroir-secret | Manual |
## Step 1: Create OpenBao Policy
Create the least-privilege policy for Miroir:
```bash
# Apply the policy to OpenBao
bao policy write miroir-policy docs/operations/openbao-policy.hcl
```
Or via the OpenBao UI:
1. Navigate to Policies
2. Create new policy named `miroir-policy`
3. Paste the contents of `docs/operations/openbao-policy.hcl`
## Step 2: Create OpenBao Role and Token
```bash
# Create the role
bao write auth/kubernetes/role/miroir \
bound_service_account_names=miroir \
bound_service_account_namespaces=search \
policies=miroir-policy \
ttl=24h
# Verify the role
bao read auth/kubernetes/role/miroir
```
## Step 3: Populate Secrets in OpenBao
Enable KV v2 secrets engine (if not already enabled):
```bash
bao secrets enable -path=kv kv-v2
```
Write the Miroir secrets:
```bash
# Generate secrets (use your preferred method)
bao kv put kv/search/miroir \
master_key="$(openssl rand -base64 32)" \
node_master_key="$(openssl rand -base64 32)" \
admin_api_key="$(openssl rand -base64 32)" \
admin_session_seal_key="$(openssl rand -base64 64)" \
search_ui_jwt_secret="$(openssl rand -base64 64)"
```
For shared key mode (if using `search_ui.auth.mode: shared_key`):
```bash
bao kv patch kv/search/miroir \
search_ui_shared_key="$(openssl rand -base64 32)"
```
For Redis password (if `redis.auth.enabled: true`):
```bash
bao kv patch kv/search/miroir \
redis_password="$(openssl rand -base64 32)"
```
## Step 4: Configure ESO ClusterSecretStore
Create the ClusterSecretStore for OpenBao:
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: openbao-backend
spec:
provider:
vault:
server: "http://openbao.openbao.svc:8200"
path: "kv"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "miroir"
```
Apply:
```bash
kubectl apply -f - <<EOF
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: openbao-backend
spec:
provider:
vault:
server: "http://openbao.openbao.svc:8200"
path: "kv"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "miroir"
EOF
```
## Step 5: Deploy Miroir with ESO Enabled
Update your Helm values:
```yaml
eso:
enabled: true
secretPath: "kv/search/miroir"
includePreviousJwt: false # Set true during JWT rotation
includeSharedKey: false # Set true when using shared_key mode
includeRedisPassword: false # Set true when Redis auth is enabled
miroir:
existingSecret: "miroir-secret" # ESO-managed Secret
```
Deploy:
```bash
helm install miroir ./charts/miroir -f values.yaml
```
## Rotation Procedures
### nodeMasterKey Rotation (Zero-Downtime)
1. Generate new admin-scoped key on each Meilisearch node:
```bash
kubectl exec -it statefulset/meilisearch -- bash -c '
curl -X POST http://localhost:7700/keys \
-H "Authorization: Bearer $MEILI_MASTER_KEY" \
-H "Content-Type: application/json" \
-d '"'"'{"actions": ["*"], "indexes": ["*"], "description": "miroir node key v2"}'"'"'
'
```
2. Update OpenBao with the new key:
```bash
bao kv patch kv/search/miroir node_master_key="<new-key>"
```
3. ESO will sync the update within `refreshInterval` (default 15m)
4. Rolling restart Miroir pods:
```bash
kubectl rollout restart deployment/miroir -n search
```
5. Delete old keys from all nodes:
```bash
kubectl exec -it statefulset/meilisearch -- bash -c '
curl -X DELETE http://localhost:7700/keys/<old-uid> \
-H "Authorization: Bearer $MEILI_MASTER_KEY"
'
```
### JWT Secret Rotation (Zero-Downtime)
Automated via `miroir-ctl ui rotate-jwt-secret`:
```bash
miroir-ctl ui rotate-jwt-secret \
--namespace=search \
--secret-name=miroir-secret \
--deployment-name=miroir
```
Or use the CronJob (quarterly, suspended by default):
```bash
# Enable the CronJob for automated rotation
kubectl patch cronjob miroir-rotate-jwt -n search -p '{"spec":{"suspend":false}}'
# Trigger manual rotation
kubectl create job --from=cronjob/miroir-rotate-jwt manual-rotation-$(date +%s) -n search
```
## Leak Response
If a secret is leaked, immediate revocation:
1. **JWT secret leaked** - Set `SEARCH_UI_JWT_SECRET_PREVIOUS` to empty string:
```bash
kubectl patch secret miroir-secret -n search -p '{"stringData":{"searchUiJwtSecretPrevious":""}}'
kubectl rollout restart deployment/miroir -n search
```
2. **Admin API key leaked** - Rotate immediately:
```bash
# Generate new key
bao kv patch kv/search/miroir admin_api_key="$(openssl rand -base64 32)"
# Wait for ESO sync (or force delete Secret)
kubectl delete secret miroir-secret -n search
```
3. **Node master key leaked** - Follow nodeMasterKey rotation procedure above
## Validation
Verify secrets are loaded correctly:
```bash
# Check ESO sync status
kubectl get externalsecret miroir-eso -n search -o yaml
# Verify Secret exists
kubectl get secret miroir-secret -n search
# Check Miroir pods have secrets
kubectl logs -l app.kubernetes.io/name=miroir -n search --tail=20 | grep -i jwt
```
## Security Notes
- The OpenBao policy is least-privilege: read-only access to `kv/search/miroir`
- Miroir never writes to OpenBao; it only reads via ESO
- All secrets are base64-encoded in Kubernetes Secrets
- Use separate OpenBao policies per namespace/environment
- Rotate keys quarterly or immediately on leak
- Monitor ESO sync errors via Prometheus alerts