2026-03-11 11:26:47 -07:00
|
|
|
|
# MCIAS Project Plan
|
|
|
|
|
|
|
|
|
|
|
|
Discrete implementation steps with acceptance criteria.
|
|
|
|
|
|
See ARCHITECTURE.md for design rationale.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Status
|
|
|
|
|
|
|
|
|
|
|
|
**v1.0.0 tagged (2026-03-15). All phases complete.**
|
|
|
|
|
|
|
|
|
|
|
|
All packages pass `go test ./...`; `golangci-lint run ./...` clean.
|
|
|
|
|
|
See PROGRESS.md for the detailed development log.
|
|
|
|
|
|
|
|
|
|
|
|
Phases 0–9 match the original plan. Phases 10–13 document significant
|
|
|
|
|
|
features implemented beyond the original plan scope.
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 0 — Repository Bootstrap **[COMPLETE]**
|
2026-03-11 11:26:47 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 0.1: Go module and dependency setup
|
|
|
|
|
|
**Acceptance criteria:**
|
2026-03-27 02:03:46 -07:00
|
|
|
|
- `go.mod` exists with module path `git.wntrmute.dev/mc/mcias`
|
2026-03-11 11:26:47 -07:00
|
|
|
|
- Required dependencies declared: `modernc.org/sqlite` (CGo-free SQLite),
|
|
|
|
|
|
`golang.org/x/crypto` (Argon2, Ed25519 helpers), `github.com/golang-jwt/jwt/v5`,
|
|
|
|
|
|
`github.com/pelletier/go-toml/v2`, `github.com/google/uuid`,
|
|
|
|
|
|
`git.wntrmute.dev/kyle/goutils`
|
|
|
|
|
|
- `go mod tidy` succeeds; `go build ./...` succeeds on empty stubs
|
|
|
|
|
|
|
|
|
|
|
|
### Step 0.2: .gitignore
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `.gitignore` excludes: build output (`mciassrv`, `mciasctl`), `*.db`,
|
|
|
|
|
|
`*.db-wal`, `*.db-shm`, test coverage files, editor artifacts
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 1 — Foundational Packages **[COMPLETE]**
|
2026-03-11 11:26:47 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 1.1: `internal/model` — shared data types
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `Account`, `Role`, `Token`, `AuditEvent`, `PGCredential` structs defined
|
|
|
|
|
|
- Account types and statuses as typed constants
|
|
|
|
|
|
- No external dependencies (pure data definitions)
|
|
|
|
|
|
- Tests: struct instantiation and constant validation
|
|
|
|
|
|
|
|
|
|
|
|
### Step 1.2: `internal/config` — configuration loading
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- TOML config parsed into a `Config` struct matching ARCHITECTURE.md §11
|
|
|
|
|
|
- Validation: required fields present, listen_addr parseable, TLS paths
|
|
|
|
|
|
non-empty, Argon2 params within safe bounds (memory >= 64MB, time >= 2)
|
|
|
|
|
|
- Master key config: exactly one of `passphrase_env` or `keyfile` set
|
|
|
|
|
|
- Tests: valid config round-trips; missing required field → error; unsafe
|
|
|
|
|
|
Argon2 params rejected
|
|
|
|
|
|
|
|
|
|
|
|
### Step 1.3: `internal/crypto` — key management and encryption helpers
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `GenerateEd25519KeyPair() (crypto/ed25519.PublicKey, crypto/ed25519.PrivateKey, error)`
|
|
|
|
|
|
- `MarshalPrivateKeyPEM(key crypto/ed25519.PrivateKey) ([]byte, error)` (PKCS#8)
|
|
|
|
|
|
- `ParsePrivateKeyPEM(pemData []byte) (crypto/ed25519.PrivateKey, error)`
|
|
|
|
|
|
- `SealAESGCM(key, plaintext []byte) (ciphertext, nonce []byte, err error)`
|
|
|
|
|
|
- `OpenAESGCM(key, nonce, ciphertext []byte) ([]byte, error)`
|
|
|
|
|
|
- `DeriveKey(passphrase string, salt []byte) ([]byte, error)` — Argon2id KDF
|
|
|
|
|
|
for master key derivation (separate from password hashing)
|
|
|
|
|
|
- Key is always 32 bytes (AES-256)
|
|
|
|
|
|
- `crypto/rand` used for all nonce/salt generation
|
|
|
|
|
|
- Tests: seal/open round-trip; open with wrong key → error; PEM round-trip;
|
|
|
|
|
|
nonces are unique across calls
|
|
|
|
|
|
|
|
|
|
|
|
### Step 1.4: `internal/db` — database layer
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `Open(path string) (*DB, error)` opens/creates SQLite DB with WAL mode and
|
|
|
|
|
|
foreign keys enabled
|
|
|
|
|
|
- `Migrate(db *DB) error` applies schema from ARCHITECTURE.md §9 idempotently
|
|
|
|
|
|
(version table tracks applied migrations)
|
|
|
|
|
|
- CRUD for `accounts`, `account_roles`, `token_revocation`, `system_tokens`,
|
|
|
|
|
|
`pg_credentials`, `audit_log`, `server_config`
|
|
|
|
|
|
- All queries use prepared statements (no string concatenation)
|
|
|
|
|
|
- Tests: in-memory SQLite (`:memory:`); each CRUD operation; FK constraint
|
|
|
|
|
|
enforcement; migration idempotency
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 2 — Authentication Core **[COMPLETE]**
|
2026-03-11 11:26:47 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 2.1: `internal/token` — JWT issuance and validation
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `IssueToken(key ed25519.PrivateKey, claims Claims) (string, error)`
|
|
|
|
|
|
- Header: `{"alg":"EdDSA","typ":"JWT"}`
|
|
|
|
|
|
- Claims: `iss`, `sub`, `iat`, `exp`, `jti` (UUID), `roles`
|
|
|
|
|
|
- `ValidateToken(key ed25519.PublicKey, tokenString string) (Claims, error)`
|
|
|
|
|
|
- **Must** check `alg` header before any other processing; reject non-EdDSA
|
|
|
|
|
|
- Validates `exp`, `iat`, `iss`, `jti` presence
|
|
|
|
|
|
- Returns structured error types for: expired, invalid signature, wrong alg,
|
|
|
|
|
|
missing claim
|
|
|
|
|
|
- `RevokeToken(db *DB, jti string, reason string) error`
|
|
|
|
|
|
- `PruneExpiredTokens(db *DB) (int64, error)` — removes rows past expiry
|
|
|
|
|
|
- Tests: happy path; expired token rejected; wrong alg (none/HS256/RS256)
|
|
|
|
|
|
rejected; tampered signature rejected; missing jti rejected; revoked token
|
|
|
|
|
|
checked by caller; pruning removes only expired rows
|
|
|
|
|
|
|
|
|
|
|
|
### Step 2.2: `internal/auth` — login and credential verification
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `HashPassword(password string, params ArgonParams) (string, error)` — returns
|
|
|
|
|
|
PHC-format string
|
|
|
|
|
|
- `VerifyPassword(password, hash string) (bool, error)` — constant-time;
|
|
|
|
|
|
re-hashes if params differ (upgrade path)
|
|
|
|
|
|
- `ValidateTOTP(secret []byte, code string) (bool, error)` — RFC 6238,
|
|
|
|
|
|
1-window tolerance, constant-time
|
|
|
|
|
|
- `Login(ctx, db, key, cfg, req LoginRequest) (LoginResponse, error)`
|
|
|
|
|
|
- Orchestrates: load account → verify status → verify password → verify TOTP
|
|
|
|
|
|
(if required) → issue JWT → write token_revocation row → write audit_log
|
|
|
|
|
|
- On any failure: write audit_log (login_fail or login_totp_fail); return
|
|
|
|
|
|
generic error to caller (no information about which step failed)
|
|
|
|
|
|
- Tests: successful login; wrong password (timing: compare duration against
|
|
|
|
|
|
a threshold — at minimum assert no short-circuit); wrong TOTP; suspended
|
|
|
|
|
|
account; deleted account; TOTP enrolled but not provided; constant-time
|
|
|
|
|
|
comparison verified (both branches take comparable time)
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 3 — HTTP Server **[COMPLETE]**
|
2026-03-11 11:26:47 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 3.1: `internal/middleware` — HTTP middleware
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `RequestLogger` — logs method, path, status, duration, IP
|
|
|
|
|
|
- `RequireAuth(key ed25519.PublicKey, db *DB)` — extracts Bearer token,
|
|
|
|
|
|
validates, checks revocation, injects claims into context; returns 401 on
|
|
|
|
|
|
failure
|
|
|
|
|
|
- `RequireRole(role string)` — checks claims from context for role; returns
|
|
|
|
|
|
403 on failure
|
|
|
|
|
|
- `RateLimit(rps float64, burst int)` — per-IP token bucket; returns 429 on
|
|
|
|
|
|
limit exceeded
|
|
|
|
|
|
- Tests: logger captures fields; RequireAuth rejects missing/invalid/revoked
|
|
|
|
|
|
tokens; RequireRole rejects insufficient roles; RateLimit triggers after
|
|
|
|
|
|
burst exceeded
|
|
|
|
|
|
|
|
|
|
|
|
### Step 3.2: `internal/server` — HTTP handlers and router
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Router wired per ARCHITECTURE.md §8 (all endpoints listed)
|
|
|
|
|
|
- `POST /v1/auth/login` — delegates to `auth.Login`; returns `{token, expires_at}`
|
|
|
|
|
|
- `POST /v1/auth/logout` — revokes current token from context; 204
|
|
|
|
|
|
- `POST /v1/auth/renew` — validates current token, issues new one, revokes old
|
|
|
|
|
|
- `POST /v1/token/validate` — validates submitted token; returns claims
|
|
|
|
|
|
- `DELETE /v1/token/{jti}` — admin or role-scoped; revokes by JTI
|
|
|
|
|
|
- `GET /v1/health` — 200 `{"status":"ok"}`
|
|
|
|
|
|
- `GET /v1/keys/public` — returns Ed25519 public key as JWK
|
|
|
|
|
|
- `POST /v1/accounts` — admin; creates account
|
|
|
|
|
|
- `GET /v1/accounts` — admin; lists accounts (no password hashes in response)
|
|
|
|
|
|
- `GET /v1/accounts/{id}` — admin; single account
|
|
|
|
|
|
- `PATCH /v1/accounts/{id}` — admin; update status/fields
|
|
|
|
|
|
- `DELETE /v1/accounts/{id}` — admin; soft-delete + revoke all tokens
|
|
|
|
|
|
- `GET|PUT /v1/accounts/{id}/roles` — admin; get/replace role set
|
|
|
|
|
|
- `POST /v1/auth/totp/enroll` — returns TOTP secret + otpauth URI
|
|
|
|
|
|
- `POST /v1/auth/totp/confirm` — confirms TOTP enrollment
|
|
|
|
|
|
- `DELETE /v1/auth/totp` — admin; removes TOTP from account
|
|
|
|
|
|
- `GET|PUT /v1/accounts/{id}/pgcreds` — get/set Postgres credentials
|
2026-03-16 15:21:26 -07:00
|
|
|
|
- `GET /v1/pgcreds` — list all accessible credentials (owned + granted)
|
2026-03-11 11:26:47 -07:00
|
|
|
|
- Credential fields (password hash, TOTP secret, Postgres password) are
|
|
|
|
|
|
**never** included in any API response
|
|
|
|
|
|
- Tests: each endpoint happy path; auth middleware applied correctly; invalid
|
|
|
|
|
|
JSON body → 400; credential fields absent from all responses
|
|
|
|
|
|
|
|
|
|
|
|
### Step 3.3: `cmd/mciassrv` — server binary
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Reads config file path from `--config` flag
|
|
|
|
|
|
- Loads config, derives master key, opens DB, runs migrations, loads/generates
|
|
|
|
|
|
signing key
|
|
|
|
|
|
- Starts HTTPS listener on configured address
|
|
|
|
|
|
- Graceful shutdown on SIGINT/SIGTERM (30s drain)
|
|
|
|
|
|
- If no signing key exists in DB, generates one and stores it encrypted
|
|
|
|
|
|
- Integration test: start server on random port, hit `/v1/health`, assert 200
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 4 — Admin CLI **[COMPLETE]**
|
2026-03-11 11:26:47 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 4.1: `cmd/mciasctl` — admin CLI
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Subcommands:
|
2026-03-12 23:07:41 -07:00
|
|
|
|
- `mciasctl account create -username NAME -type human|system`
|
2026-03-11 11:26:47 -07:00
|
|
|
|
- `mciasctl account list`
|
2026-03-12 23:07:41 -07:00
|
|
|
|
- `mciasctl account update -id UUID -status active|inactive`
|
|
|
|
|
|
- `mciasctl account delete -id UUID`
|
|
|
|
|
|
- `mciasctl account get -id UUID`
|
|
|
|
|
|
- `mciasctl account set-password -id UUID`
|
|
|
|
|
|
- `mciasctl role list -id UUID`
|
|
|
|
|
|
- `mciasctl role set -id UUID -roles role1,role2`
|
|
|
|
|
|
- `mciasctl role grant -id UUID -role ROLE`
|
|
|
|
|
|
- `mciasctl role revoke -id UUID -role ROLE`
|
|
|
|
|
|
- `mciasctl token issue -id UUID` (system accounts)
|
|
|
|
|
|
- `mciasctl token revoke -jti JTI`
|
2026-03-16 15:21:26 -07:00
|
|
|
|
- `mciasctl pgcreds list`
|
2026-03-12 23:07:41 -07:00
|
|
|
|
- `mciasctl pgcreds set -id UUID -host H -port P -db D -user U`
|
|
|
|
|
|
- `mciasctl pgcreds get -id UUID`
|
|
|
|
|
|
- `mciasctl auth login`
|
|
|
|
|
|
- `mciasctl auth change-password`
|
|
|
|
|
|
- `mciasctl tag list -id UUID`
|
|
|
|
|
|
- `mciasctl tag set -id UUID -tags tag1,tag2`
|
|
|
|
|
|
- `mciasctl policy list|create|get|update|delete`
|
|
|
|
|
|
- CLI reads admin JWT from `MCIAS_TOKEN` env var or `-token` flag
|
|
|
|
|
|
- All commands make HTTPS requests to mciassrv (base URL from `-server` flag
|
2026-03-11 11:26:47 -07:00
|
|
|
|
or `MCIAS_SERVER` env var)
|
|
|
|
|
|
- Tests: flag parsing; missing required flags → error; help text complete
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 5 — End-to-End Tests and Hardening **[COMPLETE]**
|
2026-03-11 11:26:47 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 5.1: End-to-end test suite
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `TestE2ELoginLogout` — create account, login, validate token, logout,
|
|
|
|
|
|
validate token again (must fail)
|
|
|
|
|
|
- `TestE2ETokenRenewal` — login, renew, old token rejected, new token valid
|
|
|
|
|
|
- `TestE2EAdminFlow` — create account via CLI, assign role, login as user
|
|
|
|
|
|
- `TestE2ETOTPFlow` — enroll TOTP, login without code (fail), login with code
|
|
|
|
|
|
(succeed)
|
|
|
|
|
|
- `TestE2ESystemAccount` — create system account, issue token, validate token,
|
|
|
|
|
|
rotate token (old token rejected)
|
|
|
|
|
|
- `TestE2EAlgConfusion` — attempt login, forge token with alg=HS256/none,
|
|
|
|
|
|
submit to validate endpoint, assert 401
|
|
|
|
|
|
|
|
|
|
|
|
### Step 5.2: Security hardening review
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `golangci-lint run ./...` passes with zero warnings
|
|
|
|
|
|
- `go test -race ./...` passes with zero race conditions
|
|
|
|
|
|
- Manual review checklist:
|
|
|
|
|
|
- [ ] No password/token/secret in any log line (grep audit)
|
|
|
|
|
|
- [ ] All `crypto/rand` — no `math/rand` usage
|
|
|
|
|
|
- [ ] All token comparisons use `crypto/subtle`
|
|
|
|
|
|
- [ ] Argon2id params meet OWASP minimums
|
|
|
|
|
|
- [ ] JWT alg validated before signature check in all code paths
|
|
|
|
|
|
- [ ] All DB queries use parameterized statements
|
|
|
|
|
|
- [ ] TLS min version enforced in server config
|
|
|
|
|
|
|
|
|
|
|
|
### Step 5.3: Documentation and commit
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- README.md updated with: build instructions, config file example, first-run
|
|
|
|
|
|
steps
|
|
|
|
|
|
- Each phase committed separately with appropriate commit messages
|
|
|
|
|
|
- `PROGRESS.md` reflects completed state
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 6 — mciasdb: Database Maintenance Tool **[COMPLETE]**
|
2026-03-11 12:53:25 -07:00
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §16 for full design rationale, trust model, and command
|
|
|
|
|
|
surface.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.1: `cmd/mciasdb` — binary skeleton and config loading
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Binary at `cmd/mciasdb/main.go` parses `--config` flag
|
|
|
|
|
|
- Loads `[database]` and `[master_key]` sections from mciassrv config format
|
|
|
|
|
|
- Derives master key (passphrase or keyfile, identical logic to mciassrv)
|
|
|
|
|
|
- Opens SQLite DB with WAL mode and FK enforcement (reuses `internal/db`)
|
|
|
|
|
|
- Exits with error if DB cannot be opened (e.g., busy-timeout exceeded)
|
|
|
|
|
|
- Help text lists all subcommands
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.2: Schema subcommands
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `mciasdb schema verify` — opens DB, reports current schema version, exits 0
|
|
|
|
|
|
if up-to-date, exits 1 if migrations pending
|
|
|
|
|
|
- `mciasdb schema migrate` — applies any pending migrations, reports each one
|
|
|
|
|
|
applied, exits 0
|
|
|
|
|
|
- Tests: verify on fresh DB reports version 0; migrate advances to current
|
|
|
|
|
|
version; verify after migrate exits 0
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.3: Account and role subcommands
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `mciasdb account list` — prints uuid, username, type, status for all accounts
|
|
|
|
|
|
- `mciasdb account get --id UUID` — prints single account record
|
|
|
|
|
|
- `mciasdb account create --username NAME --type human|system` — inserts row,
|
|
|
|
|
|
prints new UUID
|
|
|
|
|
|
- `mciasdb account set-password --id UUID` — prompts twice (confirm), re-hashes
|
|
|
|
|
|
with Argon2id, updates row; no `--password` flag permitted
|
|
|
|
|
|
- `mciasdb account set-status --id UUID --status STATUS` — updates status
|
|
|
|
|
|
- `mciasdb account reset-totp --id UUID` — clears totp_required and
|
|
|
|
|
|
totp_secret_enc
|
|
|
|
|
|
- `mciasdb role list --id UUID` — prints roles
|
|
|
|
|
|
- `mciasdb role grant --id UUID --role ROLE` — inserts role row
|
|
|
|
|
|
- `mciasdb role revoke --id UUID --role ROLE` — deletes role row
|
|
|
|
|
|
- All write operations append an audit log row with actor tagged `mciasdb`
|
|
|
|
|
|
- Tests: each subcommand happy path against in-memory SQLite; unknown UUID
|
|
|
|
|
|
returns error; set-password with mismatched confirmation returns error
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.4: Token subcommands
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `mciasdb token list --id UUID` — prints jti, issued_at, expires_at,
|
|
|
|
|
|
revoked_at for account
|
|
|
|
|
|
- `mciasdb token revoke --jti JTI` — sets revoked_at = now on the row
|
|
|
|
|
|
- `mciasdb token revoke-all --id UUID` — revokes all non-revoked tokens for
|
|
|
|
|
|
account
|
|
|
|
|
|
- `mciasdb prune tokens` — deletes rows from `token_revocation` where
|
|
|
|
|
|
expires_at < now; prints count removed
|
|
|
|
|
|
- All write operations append an audit log row
|
|
|
|
|
|
- Tests: revoke on unknown JTI returns error; revoke-all on account with no
|
|
|
|
|
|
active tokens is a no-op (exits 0); prune removes only expired rows
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.5: Audit log subcommands
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `mciasdb audit tail [--n N]` — prints last N events (default 50), newest last
|
|
|
|
|
|
- `mciasdb audit query --account UUID` — filters by actor_id or target_id
|
|
|
|
|
|
- `mciasdb audit query --type EVENT_TYPE` — filters by event_type
|
|
|
|
|
|
- `mciasdb audit query --since TIMESTAMP` — filters by event_time >= RFC-3339
|
|
|
|
|
|
timestamp
|
|
|
|
|
|
- Flags are combinable (AND semantics)
|
|
|
|
|
|
- `--json` flag on any audit subcommand emits newline-delimited JSON
|
|
|
|
|
|
- Tests: tail returns correct count; query filters correctly; --json output is
|
|
|
|
|
|
valid JSON
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.6: Postgres credentials subcommands
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `mciasdb pgcreds get --id UUID` — decrypts and prints host, port, db,
|
|
|
|
|
|
username, password with a warning header that output is sensitive
|
|
|
|
|
|
- `mciasdb pgcreds set --id UUID --host H --port P --db D --user U` — prompts
|
|
|
|
|
|
for password interactively (no `--password` flag), encrypts with AES-256-GCM,
|
|
|
|
|
|
stores row
|
|
|
|
|
|
- All write operations append an audit log row
|
|
|
|
|
|
- Tests: get on account with no pgcreds returns error; set then get round-trips
|
|
|
|
|
|
correctly (decrypted value matches original)
|
|
|
|
|
|
|
|
|
|
|
|
### Step 6.7: .gitignore and documentation
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `.gitignore` updated to exclude `mciasdb` binary
|
|
|
|
|
|
- README.md updated with mciasdb usage section (when to use vs mciasctl,
|
|
|
|
|
|
config requirements, example commands)
|
|
|
|
|
|
- `PROGRESS.md` updated to reflect Phase 6 complete
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 7 — gRPC Interface **[COMPLETE]**
|
2026-03-11 14:05:08 -07:00
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §17 for full design rationale, proto definitions, and
|
|
|
|
|
|
transport security requirements.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 7.1: Protobuf definitions and generated code
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `proto/mcias/v1/` directory contains `.proto` files for all service groups:
|
2026-03-16 15:21:26 -07:00
|
|
|
|
`auth.proto`, `token.proto`, `account.proto`, `policy.proto`, `admin.proto`,
|
|
|
|
|
|
`common.proto`
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- All RPC methods mirror the REST API surface (see ARCHITECTURE.md §8 and §17)
|
2026-03-11 14:44:08 -07:00
|
|
|
|
- `proto/generate.go` contains a `//go:generate protoc ...` directive that
|
|
|
|
|
|
produces Go stubs under `gen/mcias/v1/` using `protoc-gen-go` and
|
|
|
|
|
|
`protoc-gen-go-grpc`
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- Protobuf field conventions: `snake_case` field names, `google.protobuf.Timestamp`
|
|
|
|
|
|
for all time fields, no credential fields in response messages (same exclusion
|
|
|
|
|
|
rules as JSON API)
|
2026-03-11 14:44:08 -07:00
|
|
|
|
- `go generate ./...` re-runs `protoc` idempotently
|
|
|
|
|
|
- Tests: generated code compiles cleanly (`go build ./...` succeeds)
|
2026-03-11 14:05:08 -07:00
|
|
|
|
|
|
|
|
|
|
### Step 7.2: `internal/grpcserver` — gRPC handler implementations
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Package `internal/grpcserver` implements all generated gRPC service interfaces
|
|
|
|
|
|
- Handlers delegate to the same `internal/auth`, `internal/token`, and
|
|
|
|
|
|
`internal/db` packages used by the REST server — no duplicated logic
|
|
|
|
|
|
- Authentication: Bearer token extracted from gRPC metadata key `authorization`,
|
|
|
|
|
|
validated via `internal/middleware` logic (same JWT validation path)
|
|
|
|
|
|
- Admin RPCs require the `admin` role (checked identically to REST middleware)
|
|
|
|
|
|
- Errors mapped to canonical gRPC status codes:
|
|
|
|
|
|
- Unauthenticated → `codes.Unauthenticated`
|
|
|
|
|
|
- Forbidden → `codes.PermissionDenied`
|
|
|
|
|
|
- Not found → `codes.NotFound`
|
|
|
|
|
|
- Invalid input → `codes.InvalidArgument`
|
|
|
|
|
|
- Internal error → `codes.Internal` (no internal detail leaked to caller)
|
|
|
|
|
|
- Tests: unit tests for each RPC handler; unauthenticated calls rejected;
|
|
|
|
|
|
credential fields absent from all response messages
|
|
|
|
|
|
|
|
|
|
|
|
### Step 7.3: TLS and interceptors
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- gRPC server uses the same TLS certificate and key as the REST server (loaded
|
|
|
|
|
|
from config); minimum TLS 1.2 enforced via `tls.Config`
|
|
|
|
|
|
- Unary server interceptor chain:
|
2026-03-16 15:21:26 -07:00
|
|
|
|
1. Sealed interceptor (blocks all RPCs when vault sealed, except Health)
|
|
|
|
|
|
2. Request logger (method name, peer IP, status, duration)
|
|
|
|
|
|
3. Auth interceptor (extracts Bearer token, validates, injects claims into
|
2026-03-11 14:05:08 -07:00
|
|
|
|
`context.Context`)
|
2026-03-16 15:21:26 -07:00
|
|
|
|
4. Rate-limit interceptor (per-IP token bucket, same parameters as REST)
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- No credential material logged by any interceptor
|
|
|
|
|
|
- Tests: interceptor chain applied correctly; rate-limit triggers after burst
|
|
|
|
|
|
|
|
|
|
|
|
### Step 7.4: `cmd/mciassrv` integration — dual-stack server
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- mciassrv starts both the REST listener and the gRPC listener on separate ports
|
|
|
|
|
|
(gRPC port configurable: `[server] grpc_addr`; defaults to same host, port+1)
|
|
|
|
|
|
- Both listeners share the same signing key, DB connection, and config
|
|
|
|
|
|
- Graceful shutdown drains both servers within the configured drain window
|
|
|
|
|
|
- Config: `grpc_addr` added to `[server]` section (optional; if absent, gRPC
|
|
|
|
|
|
listener is disabled)
|
|
|
|
|
|
- Integration test: start server, connect gRPC client, call `Health` RPC,
|
|
|
|
|
|
assert OK response
|
|
|
|
|
|
|
|
|
|
|
|
### Step 7.5: `cmd/mciasgrpcctl` — gRPC admin CLI (optional companion)
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Binary at `cmd/mciasgrpcctl/main.go` with subcommands mirroring `mciasctl`
|
|
|
|
|
|
- Uses generated gRPC client stubs; connects to `--server` address with TLS
|
|
|
|
|
|
- Auth via `--token` flag or `MCIAS_TOKEN` env var (sent as gRPC metadata)
|
|
|
|
|
|
- Custom CA cert support identical to mciasctl
|
|
|
|
|
|
- Tests: flag parsing; missing required flags → error; help text complete
|
|
|
|
|
|
|
|
|
|
|
|
### Step 7.6: Documentation and commit
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- ARCHITECTURE.md §17 written (proto service layout, transport security,
|
|
|
|
|
|
interceptor chain, dual-stack operation)
|
|
|
|
|
|
- README.md updated with gRPC section: enabling gRPC, connecting clients,
|
|
|
|
|
|
example `grpcurl` invocations
|
2026-03-11 14:44:08 -07:00
|
|
|
|
- `.gitignore` updated to exclude the `mciasgrpcctl` binary (using a
|
|
|
|
|
|
root-anchored path `/mciasgrpcctl`); generated code in `gen/` is committed
|
|
|
|
|
|
to the repository so that consumers do not need the protoc toolchain
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- `PROGRESS.md` updated to reflect Phase 7 complete
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 8 — Operational Artifacts **[COMPLETE]**
|
2026-03-11 14:05:08 -07:00
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §18 for full design rationale and artifact inventory.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 8.1: systemd service unit
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `dist/mcias.service` — a hardened systemd unit for mciassrv:
|
|
|
|
|
|
- `Type=notify` with `sd_notify` support (or `Type=simple` with explicit
|
|
|
|
|
|
`ExecStart` if notify is not implemented)
|
|
|
|
|
|
- `User=mcias` / `Group=mcias` — dedicated service account
|
|
|
|
|
|
- `CapabilityBoundingSet=` — no capabilities required (port > 1024 assumed;
|
|
|
|
|
|
document this assumption)
|
|
|
|
|
|
- `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`
|
|
|
|
|
|
- `ReadWritePaths=/var/lib/mcias` for the SQLite file
|
|
|
|
|
|
- `EnvironmentFile=/etc/mcias/env` for `MCIAS_MASTER_PASSPHRASE`
|
|
|
|
|
|
- `Restart=on-failure`, `RestartSec=5`
|
|
|
|
|
|
- `LimitNOFILE=65536`
|
|
|
|
|
|
- `dist/mcias.env.example` — template environment file with comments explaining
|
|
|
|
|
|
each variable
|
|
|
|
|
|
- Tests: `systemd-analyze verify dist/mcias.service` exits 0 (run in CI if
|
|
|
|
|
|
systemd available; skip gracefully if not)
|
|
|
|
|
|
|
|
|
|
|
|
### Step 8.2: Default configuration file
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `dist/mcias.conf.example` — fully-commented TOML config covering all sections
|
|
|
|
|
|
from ARCHITECTURE.md §11, including the new `grpc_addr` field from Phase 7
|
|
|
|
|
|
- Comments explain every field, acceptable values, and security implications
|
|
|
|
|
|
- Defaults match the ARCHITECTURE.md §11 reference config
|
|
|
|
|
|
- A separate `dist/mcias-dev.conf.example` suitable for local development
|
|
|
|
|
|
(localhost address, relaxed timeouts, short token expiry for testing)
|
|
|
|
|
|
|
|
|
|
|
|
### Step 8.3: Installation script
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `dist/install.sh` — POSIX shell script (no bash-isms) that:
|
|
|
|
|
|
- Creates `mcias` system user and group (idempotent)
|
|
|
|
|
|
- Copies binaries to `/usr/local/bin/`
|
|
|
|
|
|
- Creates `/etc/mcias/` and `/var/lib/mcias/` with correct ownership/perms
|
|
|
|
|
|
- Installs `dist/mcias.service` to `/etc/systemd/system/`
|
|
|
|
|
|
- Prints post-install instructions (key generation, first-run steps)
|
|
|
|
|
|
- Does **not** start the service automatically
|
|
|
|
|
|
- Script is idempotent (safe to re-run after upgrades)
|
|
|
|
|
|
- Tests: shellcheck passes with zero warnings
|
|
|
|
|
|
|
|
|
|
|
|
### Step 8.4: Man pages
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `man/man1/mciassrv.1` — man page for the server binary; covers synopsis,
|
|
|
|
|
|
description, options, config file format, signals, exit codes, and files
|
|
|
|
|
|
- `man/man1/mciasctl.1` — man page for the admin CLI; covers all subcommands
|
|
|
|
|
|
with examples
|
|
|
|
|
|
- `man/man1/mciasdb.1` — man page for the DB maintenance tool; includes trust
|
|
|
|
|
|
model and safety warnings
|
|
|
|
|
|
- `man/man1/mciasgrpcctl.1` — man page for the gRPC CLI (Phase 7)
|
|
|
|
|
|
- Man pages written in `mdoc` format (BSD/macOS compatible) or `troff` (Linux)
|
|
|
|
|
|
- `make man` target in Makefile generates compressed `.gz` versions
|
|
|
|
|
|
- Tests: `man --warnings -l man/man1/*.1` exits 0 (or equivalent lint)
|
|
|
|
|
|
|
|
|
|
|
|
### Step 8.5: Makefile
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `Makefile` at repository root with targets:
|
|
|
|
|
|
- `build` — compile all binaries to `bin/`
|
|
|
|
|
|
- `test` — `go test -race ./...`
|
|
|
|
|
|
- `lint` — `golangci-lint run ./...`
|
|
|
|
|
|
- `generate` — `go generate ./...` (proto stubs from Phase 7)
|
|
|
|
|
|
- `man` — build compressed man pages
|
|
|
|
|
|
- `install` — run `dist/install.sh`
|
2026-03-16 15:21:26 -07:00
|
|
|
|
- `docker` — `docker build -t mcias:$(VERSION) -t mcias:latest .`
|
|
|
|
|
|
- `docker-clean` — remove local `mcias:$(VERSION)` and `mcias:latest` images;
|
|
|
|
|
|
prune dangling images with the mcias label
|
|
|
|
|
|
- `clean` — remove `bin/`, compressed man pages, and local Docker images
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- `dist` — build release tarballs for linux/amd64 and linux/arm64 (using
|
|
|
|
|
|
`GOOS`/`GOARCH` cross-compilation)
|
|
|
|
|
|
- `make build` works from a clean checkout after `go mod download`
|
|
|
|
|
|
- Tests: `make build` produces binaries; `make test` passes; `make lint` passes
|
|
|
|
|
|
|
2026-03-11 14:47:07 -07:00
|
|
|
|
### Step 8.6: Dockerfile
|
2026-03-11 14:05:08 -07:00
|
|
|
|
**Acceptance criteria:**
|
2026-03-11 14:47:07 -07:00
|
|
|
|
- `Dockerfile` at repository root using a multi-stage build:
|
|
|
|
|
|
- Build stage: `golang:1.26-bookworm` — compiles all four binaries with
|
|
|
|
|
|
`CGO_ENABLED=1` (required for SQLite via `modernc.org/sqlite`) and
|
|
|
|
|
|
`-trimpath -ldflags="-s -w"` to strip debug info
|
|
|
|
|
|
- Runtime stage: `debian:bookworm-slim` — installs only `ca-certificates`
|
|
|
|
|
|
and `libc6`; copies binaries from the build stage
|
|
|
|
|
|
- Final image runs as a non-root user (`uid=10001`, `gid=10001`; named `mcias`)
|
|
|
|
|
|
- `EXPOSE 8443` (REST) and `EXPOSE 9443` (gRPC); both are overridable via env
|
|
|
|
|
|
- `VOLUME /data` — operator mounts the SQLite database here
|
|
|
|
|
|
- `ENTRYPOINT ["mciassrv"]` with `CMD ["-config", "/etc/mcias/mcias.conf"]`
|
|
|
|
|
|
- Image must not contain the Go toolchain, source code, or build cache
|
|
|
|
|
|
- `dist/mcias.conf.docker.example` — config template suitable for container
|
|
|
|
|
|
deployment: `listen_addr = "0.0.0.0:8443"`, `grpc_addr = "0.0.0.0:9443"`,
|
|
|
|
|
|
`db_path = "/data/mcias.db"`, TLS cert/key paths under `/etc/mcias/`
|
|
|
|
|
|
- Tests:
|
|
|
|
|
|
- `docker build .` completes without error (run in CI if Docker available;
|
|
|
|
|
|
skip gracefully if not)
|
|
|
|
|
|
- `docker run --rm mcias:latest mciassrv --help` exits 0
|
|
|
|
|
|
|
|
|
|
|
|
### Step 8.7: Documentation
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `README.md` updated with: quick-start section referencing both the install
|
|
|
|
|
|
script and the Docker image, links to man pages, configuration walkthrough
|
|
|
|
|
|
- ARCHITECTURE.md §18 updated to include the Dockerfile in the artifact
|
|
|
|
|
|
inventory and document the container deployment model
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- `PROGRESS.md` updated to reflect Phase 8 complete
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 9 — Client Libraries **[COMPLETE]**
|
2026-03-11 14:05:08 -07:00
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §19 for full design rationale, API surface, and per-language
|
|
|
|
|
|
implementation notes.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 9.1: API surface definition and shared test fixtures
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `clients/README.md` — defines the canonical client API surface that all
|
|
|
|
|
|
language implementations must expose:
|
|
|
|
|
|
- `Client` type: configured with server URL, optional CA cert, optional token
|
|
|
|
|
|
- `Client.Login(username, password, totp_code) → (token, expires_at, error)`
|
|
|
|
|
|
- `Client.Logout() → error`
|
|
|
|
|
|
- `Client.RenewToken() → (token, expires_at, error)`
|
|
|
|
|
|
- `Client.ValidateToken(token) → (claims, error)`
|
|
|
|
|
|
- `Client.GetPublicKey() → (public_key_jwk, error)`
|
|
|
|
|
|
- `Client.Health() → error`
|
|
|
|
|
|
- Account management methods (admin only): `CreateAccount`, `ListAccounts`,
|
|
|
|
|
|
`GetAccount`, `UpdateAccount`, `DeleteAccount`
|
|
|
|
|
|
- Role management: `GetRoles`, `SetRoles`
|
|
|
|
|
|
- Token management: `IssueServiceToken`, `RevokeToken`
|
|
|
|
|
|
- PG credentials: `GetPGCreds`, `SetPGCreds`
|
|
|
|
|
|
- `clients/testdata/` — shared JSON fixtures for mock server responses (used
|
|
|
|
|
|
by all language test suites)
|
|
|
|
|
|
- A mock MCIAS server (`test/mock/`) in Go that can be started by integration
|
|
|
|
|
|
tests in any language via subprocess or a pre-built binary
|
|
|
|
|
|
|
|
|
|
|
|
### Step 9.2: Go client library
|
|
|
|
|
|
**Acceptance criteria:**
|
2026-03-27 02:03:46 -07:00
|
|
|
|
- `clients/go/` — Go module `git.wntrmute.dev/mc/mcias/clients/go`
|
2026-03-11 14:05:08 -07:00
|
|
|
|
- Package `mciasgoclient` exposes the canonical API surface from Step 9.1
|
|
|
|
|
|
- Uses `net/http` with `crypto/tls`; custom CA cert supported via `x509.CertPool`
|
|
|
|
|
|
- Token stored in-memory; `Client.Token()` accessor returns current token
|
|
|
|
|
|
- Thread-safe: concurrent calls from multiple goroutines are safe
|
|
|
|
|
|
- All JSON decoding uses `DisallowUnknownFields`
|
|
|
|
|
|
- Tests:
|
|
|
|
|
|
- Unit tests with `httptest.Server` for all methods
|
|
|
|
|
|
- Integration test against mock server (Step 9.1) covering full login →
|
|
|
|
|
|
validate → logout flow
|
|
|
|
|
|
- `go test -race ./...` passes with zero race conditions
|
|
|
|
|
|
- `go doc` comments on all exported types and methods
|
|
|
|
|
|
|
|
|
|
|
|
### Step 9.3: Rust client library
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `clients/rust/` — Rust crate `mcias-client`
|
|
|
|
|
|
- Uses `reqwest` (async, TLS-enabled) and `serde` / `serde_json`
|
|
|
|
|
|
- Exposes the canonical API surface from Step 9.1 as an async Rust API
|
|
|
|
|
|
(`tokio`-compatible)
|
|
|
|
|
|
- Custom CA cert supported via `reqwest::Certificate`
|
|
|
|
|
|
- Token stored in `Arc<Mutex<Option<String>>>` for async-safe sharing
|
|
|
|
|
|
- Errors: typed `MciasError` enum covering `Unauthenticated`, `Forbidden`,
|
|
|
|
|
|
`NotFound`, `InvalidInput`, `Transport`, `Server`
|
|
|
|
|
|
- Tests:
|
|
|
|
|
|
- Unit tests with `wiremock` or `mockito` for all methods
|
|
|
|
|
|
- Integration test against mock server covering full login → validate → logout
|
|
|
|
|
|
- `cargo test` passes; `cargo clippy -- -D warnings` passes
|
|
|
|
|
|
- `cargo doc` with `#[doc]` comments on all public items
|
|
|
|
|
|
|
|
|
|
|
|
### Step 9.4: Common Lisp client library
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `clients/lisp/` — ASDF system `mcias-client`
|
|
|
|
|
|
- Uses `dexador` for HTTP and `yason` (or `cl-json`) for JSON
|
|
|
|
|
|
- TLS handled by the underlying Dexador/Usocket stack; custom CA cert
|
|
|
|
|
|
documented (platform-specific)
|
|
|
|
|
|
- Exposes the canonical API surface from Step 9.1 as synchronous functions
|
|
|
|
|
|
with keyword arguments
|
|
|
|
|
|
- Errors signalled as conditions: `mcias-error`, `mcias-unauthenticated`,
|
|
|
|
|
|
`mcias-forbidden`, `mcias-not-found`
|
|
|
|
|
|
- Token stored as a slot on the `mcias-client` CLOS object
|
|
|
|
|
|
- Tests:
|
|
|
|
|
|
- Unit tests using `fiveam` with mock HTTP responses (via `dexador` mocking
|
|
|
|
|
|
or a local test server)
|
|
|
|
|
|
- Integration test against mock server covering full login → validate → logout
|
|
|
|
|
|
- Tests run with SBCL; `(asdf:test-system :mcias-client)` passes
|
|
|
|
|
|
- Docstrings on all exported symbols; `(describe 'mcias-client)` is informative
|
|
|
|
|
|
|
|
|
|
|
|
### Step 9.5: Python client library
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `clients/python/` — Python package `mcias_client`; supports Python 3.11+
|
|
|
|
|
|
- Uses `httpx` (sync and async variants) or `requests` (sync-only acceptable
|
|
|
|
|
|
for v1)
|
|
|
|
|
|
- Exposes the canonical API surface from Step 9.1 as a `MciasClient` class
|
|
|
|
|
|
- Custom CA cert supported via `ssl.create_default_context()` with `cafile`
|
|
|
|
|
|
- Token stored as an instance attribute; `client.token` property
|
|
|
|
|
|
- Errors: `MciasError` base class with subclasses `MciasAuthError`,
|
|
|
|
|
|
`MciasForbiddenError`, `MciasNotFoundError`
|
|
|
|
|
|
- Typed: full `py.typed` marker; all public symbols annotated with PEP 526
|
|
|
|
|
|
type annotations; `mypy --strict` passes
|
|
|
|
|
|
- Tests:
|
|
|
|
|
|
- Unit tests with `pytest` and `respx` (httpx mock) or `responses` (requests)
|
|
|
|
|
|
for all methods
|
|
|
|
|
|
- Integration test against mock server covering full login → validate → logout
|
|
|
|
|
|
- `pytest` passes; `ruff check` and `mypy --strict` pass
|
|
|
|
|
|
- Docstrings on all public classes and methods; `help(MciasClient)` is
|
|
|
|
|
|
informative
|
|
|
|
|
|
|
|
|
|
|
|
### Step 9.6: Documentation and commit
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Each client library has its own `README.md` with: installation instructions,
|
|
|
|
|
|
quickstart example, API reference summary, error handling guide
|
|
|
|
|
|
- ARCHITECTURE.md §19 written (client library design, per-language notes,
|
|
|
|
|
|
versioning strategy)
|
|
|
|
|
|
- `PROGRESS.md` updated to reflect Phase 9 complete
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 15:21:26 -07:00
|
|
|
|
## Phase 10 — Web UI (HTMX) **[COMPLETE]**
|
|
|
|
|
|
|
|
|
|
|
|
Not in the original plan. Implemented alongside and after Phase 3.
|
|
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §8 (Web Management UI) for design details.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 10.1: `internal/ui` — HTMX web interface
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Go `html/template` pages embedded at compile time via `web/embed.go`
|
|
|
|
|
|
- CSRF protection: HMAC-signed double-submit cookie (`mcias_csrf`)
|
|
|
|
|
|
- Session: JWT stored as `HttpOnly; Secure; SameSite=Strict` cookie
|
|
|
|
|
|
- Security headers: `Content-Security-Policy: default-src 'self'`,
|
|
|
|
|
|
`X-Frame-Options: DENY`, `Referrer-Policy: strict-origin`
|
|
|
|
|
|
- Pages: login, dashboard, account list/detail, role editor, tag editor,
|
|
|
|
|
|
pgcreds, audit log viewer, policy rules, user profile, service-accounts
|
|
|
|
|
|
- HTMX partial-page updates for mutations (role updates, tag edits, policy
|
|
|
|
|
|
toggles, access grants)
|
|
|
|
|
|
- Empty-state handling on all list pages (zero records case tested)
|
|
|
|
|
|
|
|
|
|
|
|
### Step 10.2: Swagger UI at `/docs`
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `GET /docs` serves Swagger UI for `openapi.yaml`
|
|
|
|
|
|
- swagger-ui-bundle.js and swagger-ui.css bundled locally in `web/static/`
|
|
|
|
|
|
(CDN blocked by CSP `default-src 'self'`)
|
|
|
|
|
|
- `GET /docs/openapi.yaml` serves the OpenAPI spec
|
|
|
|
|
|
- `openapi.yaml` kept in sync with REST API surface
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 11 — Authorization Policy Engine **[COMPLETE]**
|
|
|
|
|
|
|
|
|
|
|
|
Not in the original plan (CLI subcommands for policy were planned in Phase 4,
|
|
|
|
|
|
but the engine itself was not a discrete plan phase).
|
|
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §20 for full design, evaluation algorithm, and built-in
|
|
|
|
|
|
default rules.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 11.1: `internal/policy` — in-process ABAC engine
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Pure evaluation: `Evaluate(input PolicyInput, rules []Rule) (Effect, *Rule)`
|
|
|
|
|
|
- Deny-wins: any explicit deny overrides all allows
|
|
|
|
|
|
- Default-deny: no matching rule → deny
|
|
|
|
|
|
- Built-in default rules (IDs -1 … -7) compiled in; reproduce previous
|
|
|
|
|
|
binary admin/non-admin behavior exactly; cannot be disabled via API
|
|
|
|
|
|
- Match fields: roles, account types, subject UUID, actions, resource type,
|
|
|
|
|
|
owner-matches-subject, service names, required tags (all ANDed; zero value
|
|
|
|
|
|
= wildcard)
|
|
|
|
|
|
- Temporal constraints on DB-backed rules: `not_before`, `expires_at`
|
|
|
|
|
|
- `Engine` wrapper: caches rule set in memory; reloads on policy mutations
|
|
|
|
|
|
- Tests: all built-in rules; deny-wins over allow; default-deny fallback;
|
|
|
|
|
|
temporal filtering; concurrent access
|
|
|
|
|
|
|
|
|
|
|
|
### Step 11.2: Middleware and REST integration
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `RequirePolicy(engine, action, resourceType)` middleware replaces
|
|
|
|
|
|
`RequireRole("admin")` where policy-gated
|
|
|
|
|
|
- Every explicit deny produces a `policy_deny` audit event
|
|
|
|
|
|
- REST endpoints: `GET|POST /v1/policy/rules`, `GET|PATCH|DELETE /v1/policy/rules/{id}`
|
|
|
|
|
|
- DB schema: `policy_rules` and `account_tags` tables (migrations 000004,
|
|
|
|
|
|
000006)
|
|
|
|
|
|
- `PATCH /v1/policy/rules/{id}` supports updating `priority`, `enabled`,
|
|
|
|
|
|
`not_before`, `expires_at`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 12 — Vault Seal/Unseal Lifecycle **[COMPLETE]**
|
|
|
|
|
|
|
|
|
|
|
|
Not in the original plan.
|
|
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §8 (Vault Endpoints) for the API surface.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 12.1: `internal/vault` — master key lifecycle
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Thread-safe `Vault` struct with `sync.RWMutex`-protected state
|
|
|
|
|
|
- Methods: `IsSealed()`, `Unseal(passphrase)`, `Seal()`, `MasterKey()`,
|
|
|
|
|
|
`PrivKey()`, `PubKey()`
|
|
|
|
|
|
- `Seal()` zeroes all key material before nilling (memguard-style cleanup)
|
|
|
|
|
|
- `DeriveFromPassphrase()` and `DecryptSigningKey()` extracted to `derive.go`
|
|
|
|
|
|
for reuse by unseal handlers
|
|
|
|
|
|
- Tests: state transitions; key zeroing verified; concurrent read/write safety
|
|
|
|
|
|
|
|
|
|
|
|
### Step 12.2: REST and UI integration
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `POST /v1/vault/unseal` — rate-limited (3/s burst 5); derives key, unseals
|
|
|
|
|
|
- `GET /v1/vault/status` — always accessible; returns `{"sealed": bool}`
|
|
|
|
|
|
- `POST /v1/vault/seal` — admin only; zeroes key material
|
|
|
|
|
|
- `GET /v1/health` returns `{"status":"sealed"}` when sealed
|
|
|
|
|
|
- All other `/v1/*` endpoints return 503 `vault_sealed` when sealed
|
|
|
|
|
|
- UI redirects all paths to `/unseal` when sealed (except `/static/`)
|
|
|
|
|
|
- gRPC: `sealedInterceptor` first in chain; blocks all RPCs except Health
|
|
|
|
|
|
- Startup: server may start in sealed state if passphrase env var is absent
|
|
|
|
|
|
- Audit events: `vault_sealed`, `vault_unsealed`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## Phase 13 — Token Delegation and pgcred Access Grants **[COMPLETE]**
|
|
|
|
|
|
|
|
|
|
|
|
Not in the original plan.
|
|
|
|
|
|
|
|
|
|
|
|
See ARCHITECTURE.md §21 (Token Issuance Delegation) for design details.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 13.1: Service account token delegation
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- DB migration 000008: `service_account_delegates` table
|
|
|
|
|
|
- `POST /accounts/{id}/token/delegates` — admin grants delegation
|
|
|
|
|
|
- `DELETE /accounts/{id}/token/delegates/{grantee}` — admin revokes delegation
|
|
|
|
|
|
- `POST /accounts/{id}/token` — accepts admin or delegate (not admin-only)
|
|
|
|
|
|
- One-time token download: nonce stored in `sync.Map` with 5-minute TTL;
|
|
|
|
|
|
`GET /token/download/{nonce}` serves token as attachment, deletes nonce
|
|
|
|
|
|
- `/service-accounts` page for non-admin delegates
|
|
|
|
|
|
- Audit events: `token_delegate_granted`, `token_delegate_revoked`
|
|
|
|
|
|
|
|
|
|
|
|
### Step 13.2: pgcred fine-grained access grants
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- DB migration 000005: `pgcred_access_grants` table
|
|
|
|
|
|
- `POST /accounts/{id}/pgcreds/access` — owner grants read access to grantee
|
|
|
|
|
|
- `DELETE /accounts/{id}/pgcreds/access/{grantee}` — owner revokes access
|
|
|
|
|
|
- `GET /v1/pgcreds` — lists all credentials accessible to caller (owned +
|
|
|
|
|
|
granted); includes credential ID for reference
|
|
|
|
|
|
- Grantees may view connection metadata; password is never decrypted for them
|
|
|
|
|
|
- Audit events: `pgcred_access_granted`, `pgcred_access_revoked`
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-16 16:12:59 -07:00
|
|
|
|
## Phase 14 — FIDO2/WebAuthn and Passkey Authentication
|
|
|
|
|
|
|
|
|
|
|
|
**Goal:** Add FIDO2/WebAuthn support for passwordless passkey login and hardware
|
|
|
|
|
|
security key 2FA. Discoverable credentials enable passwordless login;
|
|
|
|
|
|
non-discoverable credentials serve as 2FA. Either WebAuthn or TOTP satisfies
|
|
|
|
|
|
the 2FA requirement.
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.1: Dependency, config, and model types
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `github.com/go-webauthn/webauthn` dependency added
|
|
|
|
|
|
- `WebAuthnConfig` struct in config with RPID, RPOrigin, DisplayName
|
|
|
|
|
|
- Validation: if any field set, RPID+RPOrigin required; RPOrigin must be HTTPS
|
|
|
|
|
|
- `WebAuthnCredential` model type with encrypted-at-rest fields
|
|
|
|
|
|
- Audit events: `webauthn_enrolled`, `webauthn_removed`, `webauthn_login_ok`, `webauthn_login_fail`
|
|
|
|
|
|
- Policy actions: `ActionEnrollWebAuthn`, `ActionRemoveWebAuthn`
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.2: Database migration and CRUD
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Migration 000009: `webauthn_credentials` table with encrypted credential fields
|
|
|
|
|
|
- Full CRUD: Create, Get (by ID, by account), Delete (ownership-checked and admin),
|
|
|
|
|
|
DeleteAll, UpdateSignCount, UpdateLastUsed, Has, Count
|
|
|
|
|
|
- DB tests for all operations including ownership checks and cascade behavior
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.3: WebAuthn adapter package
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `internal/webauthn/` package with adapter, user, and converter
|
|
|
|
|
|
- `NewWebAuthn(cfg)` factory wrapping library initialization
|
|
|
|
|
|
- `AccountUser` implementing `webauthn.User` interface
|
|
|
|
|
|
- `EncryptCredential`/`DecryptCredential`/`DecryptCredentials` round-trip encryption
|
|
|
|
|
|
- Tests for encrypt/decrypt, interface compliance, wrong-key rejection
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.4: REST endpoints
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `POST /v1/auth/webauthn/register/begin` — password re-auth, returns creation options
|
|
|
|
|
|
- `POST /v1/auth/webauthn/register/finish` — completes registration, encrypts credential
|
|
|
|
|
|
- `POST /v1/auth/webauthn/login/begin` — discoverable and username-scoped flows
|
|
|
|
|
|
- `POST /v1/auth/webauthn/login/finish` — validates assertion, issues JWT
|
|
|
|
|
|
- `GET /v1/accounts/{id}/webauthn` — admin, returns metadata only
|
|
|
|
|
|
- `DELETE /v1/accounts/{id}/webauthn/{credentialId}` — admin remove
|
|
|
|
|
|
- Challenge store: `sync.Map` with 120s TTL, background cleanup
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.5: Web UI
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Profile page: passkey enrollment form, credential list with delete
|
|
|
|
|
|
- Login page: "Sign in with passkey" button with discoverable flow
|
|
|
|
|
|
- Account detail page: passkey section with admin remove
|
|
|
|
|
|
- CSP-compliant `webauthn.js` (external script, base64url helpers)
|
|
|
|
|
|
- Empty state handling for zero credentials
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.6: gRPC handlers
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- Proto messages and RPCs: `ListWebAuthnCredentials`, `RemoveWebAuthnCredential`
|
|
|
|
|
|
- gRPC handler implementation delegating to shared packages
|
|
|
|
|
|
- Regenerated protobuf stubs
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.7: mciasdb offline management
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- `mciasdb webauthn list --id UUID`
|
|
|
|
|
|
- `mciasdb webauthn delete --id UUID --credential-id N`
|
|
|
|
|
|
- `mciasdb webauthn reset --id UUID` (deletes all)
|
|
|
|
|
|
- `mciasdb account reset-webauthn --id UUID` alias
|
|
|
|
|
|
- All operations write audit events
|
|
|
|
|
|
|
|
|
|
|
|
### Step 14.8: OpenAPI and documentation
|
|
|
|
|
|
**Acceptance criteria:**
|
|
|
|
|
|
- All 6 REST endpoints documented in openapi.yaml
|
|
|
|
|
|
- `WebAuthnCredentialInfo` schema, `webauthn_enabled`/`webauthn_count` on Account
|
|
|
|
|
|
- ARCHITECTURE.md §22 with design details
|
|
|
|
|
|
- PROJECT_PLAN.md Phase 14
|
|
|
|
|
|
- PROGRESS.md updated
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-03-11 11:26:47 -07:00
|
|
|
|
## Implementation Order
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
Phase 0 → Phase 1 (1.1, 1.2, 1.3, 1.4 in parallel or sequence)
|
|
|
|
|
|
→ Phase 2 (2.1 then 2.2)
|
|
|
|
|
|
→ Phase 3 (3.1, 3.2, 3.3 in sequence)
|
|
|
|
|
|
→ Phase 4
|
|
|
|
|
|
→ Phase 5
|
2026-03-11 12:53:25 -07:00
|
|
|
|
→ Phase 6 (6.1 → 6.2 → 6.3 → 6.4 → 6.5 → 6.6 → 6.7)
|
2026-03-11 14:05:08 -07:00
|
|
|
|
→ Phase 7 (7.1 → 7.2 → 7.3 → 7.4 → 7.5 → 7.6)
|
|
|
|
|
|
→ Phase 8 (8.1 → 8.2 → 8.3 → 8.4 → 8.5 → 8.6)
|
|
|
|
|
|
→ Phase 9 (9.1 → 9.2 → 9.3 → 9.4 → 9.5 → 9.6)
|
2026-03-16 15:21:26 -07:00
|
|
|
|
→ Phase 10 (interleaved with Phase 3 and later phases)
|
|
|
|
|
|
→ Phase 11 (interleaved with Phase 3–4)
|
|
|
|
|
|
→ Phase 12 (post Phase 3)
|
|
|
|
|
|
→ Phase 13 (post Phase 3 and 11)
|
2026-03-16 16:12:59 -07:00
|
|
|
|
→ Phase 14 (post v1.0.0)
|
2026-03-11 11:26:47 -07:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
Each step must have passing tests before the next step begins.
|
2026-03-16 16:12:59 -07:00
|
|
|
|
Phases 0–13 complete as of v1.0.0 (2026-03-15).
|
|
|
|
|
|
Phase 14 complete as of 2026-03-16.
|