Add vault seal/unseal lifecycle

- New internal/vault package: thread-safe Vault struct with
  seal/unseal state, key material zeroing, and key derivation
- REST: POST /v1/vault/unseal, POST /v1/vault/seal,
  GET /v1/vault/status; health returns sealed status
- UI: /unseal page with passphrase form, redirect when sealed
- gRPC: sealedInterceptor rejects RPCs when sealed
- Middleware: RequireUnsealed blocks all routes except exempt
  paths; RequireAuth reads pubkey from vault at request time
- Startup: server starts sealed when passphrase unavailable
- All servers share single *vault.Vault by pointer
- CSRF manager derives key lazily from vault

Security: Key material is zeroed on seal. Sealed middleware
runs before auth. Handlers fail closed if vault becomes sealed
mid-request. Unseal endpoint is rate-limited (3/s burst 5).
No CSRF on unseal page (no session to protect; chicken-and-egg
with master key). Passphrase never logged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 23:55:37 -07:00
parent 5c242f8abb
commit d87b4b4042
28 changed files with 1292 additions and 119 deletions

View File

@@ -4,6 +4,50 @@ Source of truth for current development state.
---
All phases complete. **v1.0.0 tagged.** All packages pass `go test ./...`; `golangci-lint run ./...` clean.
### 2026-03-14 — Vault seal/unseal lifecycle
**Problem:** `mciassrv` required the master passphrase at startup and refused to start without it. Operators needed a way to start the server in a degraded state and provide the passphrase at runtime, plus the ability to re-seal at runtime.
**Solution:** Implemented a `Vault` abstraction that manages key material lifecycle with seal/unseal state transitions.
**New package: `internal/vault/`**
- `vault.go`: Thread-safe `Vault` struct with `sync.RWMutex`-protected state. Methods: `IsSealed()`, `Unseal()`, `Seal()`, `MasterKey()`, `PrivKey()`, `PubKey()`. `Seal()` zeroes all key material before nilling.
- `derive.go`: Extracted `DeriveFromPassphrase()` and `DecryptSigningKey()` from `cmd/mciassrv/main.go` for reuse by unseal handlers.
- `vault_test.go`: Tests for state transitions, key zeroing, concurrent access.
**REST API (`internal/server/`):**
- `POST /v1/vault/unseal`: Accept passphrase, derive key, unseal (rate-limited 3/s burst 5)
- `POST /v1/vault/seal`: Admin-only, seals vault and zeroes key material
- `GET /v1/vault/status`: Returns `{"sealed": bool}`
- `GET /v1/health`: Now returns `{"status":"sealed"}` when sealed
- All other `/v1/*` endpoints return 503 `vault_sealed` when sealed
**Web UI (`internal/ui/`):**
- New unseal page at `/unseal` with passphrase form (same styling as login)
- All UI routes redirect to `/unseal` when sealed (except `/static/`)
- CSRF manager now derives key lazily from vault
**gRPC (`internal/grpcserver/`):**
- New `sealedInterceptor` first in interceptor chain — returns `codes.Unavailable` for all RPCs except Health
- Health RPC returns `status: "sealed"` when sealed
**Startup (`cmd/mciassrv/main.go`):**
- When passphrase env var is empty/unset (and not first run): starts in sealed state
- When passphrase is available: backward-compatible unsealed startup
- First run still requires passphrase to generate signing key
**Refactoring:**
- All three servers (REST, UI, gRPC) share a single `*vault.Vault` by pointer
- Replaced static `privKey`, `pubKey`, `masterKey` fields with vault accessor calls
- `middleware.RequireAuth` now reads pubkey from vault at request time
- New `middleware.RequireUnsealed` middleware wired before request logger
**Audit events:** Added `vault_sealed` and `vault_unsealed` event types.
**OpenAPI:** Updated `openapi.yaml` with vault endpoints and sealed health response.
**Files changed:** 19 files (3 new packages, 3 new handlers, 1 new template, extensive refactoring across all server packages and tests).
### 2026-03-13 — Make pgcreds discoverable via CLI and UI
**Problem:** Users had no way to discover which pgcreds were available to them or what their credential IDs were, making it functionally impossible to use the system without manual database inspection.