- 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>
- Add IssueSystemToken() method in internal/db/accounts.go that wraps
revoke-old, track-new, and upsert-system_tokens in a single SQLite
transaction
- Update handleTokenIssue in internal/server/server.go to use the new
atomic method instead of three separate DB calls
- Update IssueServiceToken in internal/grpcserver/tokenservice.go with
the same fix
- Add TestIssueSystemTokenAtomic test covering first issue and rotation
Security: token issuance now uses a single transaction to prevent
inconsistent state (e.g., old token revoked but new token not tracked)
if a crash occurs between operations. Follows the same pattern as
RenewToken which was already correctly transactional.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- proto/mcias/v1/: AdminService, AuthService, TokenService,
AccountService, CredentialService; generated Go stubs in gen/
- internal/grpcserver: full handler implementations sharing all
business logic (auth, token, db, crypto) with REST server;
interceptor chain: logging -> auth (JWT alg-first + revocation) ->
rate-limit (token bucket, 10 req/s, burst 10, per-IP)
- internal/config: optional grpc_addr field in [server] section
- cmd/mciassrv: dual-stack startup; gRPC/TLS listener on grpc_addr
when configured; graceful shutdown of both servers in 15s window
- cmd/mciasgrpcctl: companion gRPC CLI mirroring mciasctl commands
(health, pubkey, account, role, token, pgcreds) using TLS with
optional custom CA cert
- internal/grpcserver/grpcserver_test.go: 20 tests via bufconn covering
public RPCs, auth interceptor (no token, invalid, revoked -> 401),
non-admin -> 403, Login/Logout/RenewToken/ValidateToken flows,
AccountService CRUD, SetPGCreds/GetPGCreds AES-GCM round-trip,
credential fields absent from all responses
Security:
JWT validation path identical to REST: alg header checked before
signature, alg:none rejected, revocation table checked after sig.
Authorization metadata value never logged by any interceptor.
Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from
all proto response messages — enforced by proto design and confirmed
by test TestCredentialFieldsAbsentFromAccountResponse.
Login dummy-Argon2 timing guard preserves timing uniformity for
unknown users (same as REST handleLogin).
TLS required at listener level; cmd/mciassrv uses
credentials.NewServerTLSFromFile; no h2c offered.
137 tests pass, zero race conditions (go test -race ./...)