Add `miroir-ctl key rotate-node-master` command implementing plan §9 4-step zero-downtime rotation: create new admin-scoped key on all Meilisearch nodes, print K8s Secret update instructions, wait for rolling restart confirmation, delete old key. Supports --dry-run, node auto-discovery via topology API, and rollback on step 1 failure. Add `address` field to topology API NodeInfo for CLI node discovery. Add runbooks for both nodeMasterKey (zero-downtime) and startup master key (maintenance window required) rotation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
147 lines
4.9 KiB
Markdown
147 lines
4.9 KiB
Markdown
# nodeMasterKey Zero-Downtime Rotation
|
|
|
|
> Rotates the admin-scoped child key (`nodeMasterKey`) that Miroir uses to
|
|
> authenticate with Meilisearch nodes. **No maintenance window required.**
|
|
>
|
|
> This is NOT the startup master key (`MEILI_MASTER_KEY`). For that, see
|
|
> [startup-master-key-rotation.md](startup-master-key-rotation.md).
|
|
|
|
## Background (plan §9)
|
|
|
|
Meilisearch allows multiple admin-scoped keys (created via `POST /keys` with
|
|
`actions: ["*"]`, `indexes: ["*"]`) to coexist. The `nodeMasterKey` in Miroir
|
|
config is one such key. Because old and new keys are both valid until the old
|
|
one is explicitly deleted, rotation is zero-downtime.
|
|
|
|
## Prerequisites
|
|
|
|
- `miroir-ctl` binary built from this repo
|
|
- Admin API key (`MIROIR_ADMIN_API_KEY` env var, credentials file, or `--admin-key`)
|
|
- Current `nodeMasterKey` value (`--current-key` or `MIROIR_NODE_MASTER_KEY` env var)
|
|
- Miroir admin API reachable (default `http://localhost:8080`, override with `--api-url`)
|
|
- `kubectl` access to update the K8s secret and restart Miroir pods
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# Dry-run — prints the plan without executing
|
|
miroir-ctl key rotate-node-master --dry-run \
|
|
--current-key "$MIROIR_NODE_MASTER_KEY"
|
|
|
|
# Live rotation with auto-discovered nodes
|
|
miroir-ctl key rotate-node-master \
|
|
--current-key "$MIROIR_NODE_MASTER_KEY"
|
|
|
|
# Live rotation with explicit nodes
|
|
miroir-ctl key rotate-node-master \
|
|
--current-key "$MIROIR_NODE_MASTER_KEY" \
|
|
--node http://meili-0.search.svc:7700 \
|
|
--node http://meili-1.search.svc:7700 \
|
|
--node http://meili-2.search.svc:7700
|
|
```
|
|
|
|
## What the CLI Does (4 steps)
|
|
|
|
### Step 1 — Create new admin-scoped key on every Meilisearch node
|
|
|
|
`POST /keys` with `actions: ["*"]`, `indexes: ["*"]`. If any node fails, the
|
|
CLI rolls back by deleting the new key from all nodes where creation succeeded.
|
|
|
|
### Step 2 — Print K8s Secret update instructions
|
|
|
|
The CLI prints a `kubectl patch secret` command. Apply it:
|
|
|
|
```bash
|
|
kubectl -n search patch secret miroir-keys \
|
|
-p '{"stringData":{"nodeMasterKey":"<new-key>"}}'
|
|
```
|
|
|
|
Or update your ExternalSecret / OpenBao source and wait for ESO to sync.
|
|
|
|
### Step 3 — Rolling restart Miroir pods
|
|
|
|
```bash
|
|
kubectl -n search rollout restart deployment/miroir
|
|
kubectl -n search rollout status deployment/miroir
|
|
```
|
|
|
|
During rollout, pods with the old key and pods with the new key both
|
|
authenticate against Meilisearch — no downtime.
|
|
|
|
The CLI pauses and waits for you to confirm all pods are running.
|
|
|
|
### Step 4 — Delete old admin-scoped key
|
|
|
|
The CLI finds the old key UID via `GET /keys` (matching by prefix) and deletes
|
|
it from all Meilisearch nodes with `DELETE /keys/{uid}`.
|
|
|
|
## CLI Flags
|
|
|
|
| Flag | Default | Description |
|
|
|------|---------|-------------|
|
|
| `--dry-run` | false | Print plan without executing |
|
|
| `--current-key` | env `MIROIR_NODE_MASTER_KEY` | Current key (required) |
|
|
| `--node` | auto-discovered | Meilisearch node URLs (repeatable) |
|
|
| `--key-name` | `miroir-node-master` | Name for the new key |
|
|
| `--expires-at` | none | Optional ISO 8601 expiration |
|
|
| `--namespace` | `search` | K8s namespace |
|
|
| `--secret-name` | `miroir-keys` | K8s Secret name |
|
|
| `--yes` | false | Skip confirmation prompts |
|
|
|
|
## Manual Steps (if CLI is unavailable)
|
|
|
|
1. **Create new key** on each Meilisearch node:
|
|
```bash
|
|
for i in 0 1 2 3; do
|
|
curl -s -X POST "http://meili-${i}.search.svc:7700/keys" \
|
|
-H "Authorization: Bearer $CURRENT_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"name":"miroir-node-master","description":"rotated key","actions":["*"],"indexes":["*"]}' \
|
|
| jq '{uid,key}'
|
|
done
|
|
```
|
|
|
|
2. **Update secret** with the new key value from step 1.
|
|
|
|
3. **Rolling restart** Miroir deployment.
|
|
|
|
4. **Delete old key** — list keys, find the old one by prefix match, delete by UID:
|
|
```bash
|
|
curl -s http://meili-0.search.svc:7700/keys \
|
|
-H "Authorization: Bearer $NEW_KEY" | jq '.results[] | {uid,key,name}'
|
|
# Then DELETE /keys/{old-uid} on each node
|
|
```
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
# Confirm Miroir is healthy
|
|
curl -s http://miroir.search.svc:7700/health
|
|
|
|
# Check topology
|
|
miroir-ctl status
|
|
|
|
# Test search
|
|
curl -s http://miroir.search.svc:7700/indexes/test-index/search \
|
|
-H "Authorization: Bearer $MIROIR_MASTER_KEY" \
|
|
-d '{"q": ""}'
|
|
```
|
|
|
|
## Cadence
|
|
|
|
- Rotate on suspected compromise (immediately)
|
|
- Rotate proactively every 90 days
|
|
- Chain after startup-master rotation (see below)
|
|
|
|
## Relationship to Startup Master Rotation
|
|
|
|
If you have just rotated `MEILI_MASTER_KEY` (see
|
|
[startup-master-key-rotation.md](startup-master-key-rotation.md)), the new
|
|
Meilisearch nodes have no admin-scoped child keys yet. Create one using the
|
|
new master key, then run this zero-downtime flow to rotate it.
|
|
|
|
## See Also
|
|
|
|
- [startup-master-key-rotation.md](startup-master-key-rotation.md) — startup master (requires maintenance window)
|
|
- Plan §9 — full secrets handling documentation
|
|
- `miroir-ctl key rotate-node-master --dry-run` — preview the rotation plan
|