- Makefile: build/test/lint/generate/man/install/clean/dist/docker; CGO_ENABLED=1 throughout; VERSION from git describe --tags --always - Dockerfile: multi-stage (golang:1.26-bookworm builder -> debian:bookworm-slim runtime); non-root uid 10001 (mcias), VOLUME /data, EXPOSE 8443/9443; no toolchain in final image - dist/mcias.service: hardened systemd unit (ProtectSystem=strict, ProtectHome, PrivateTmp, NoNewPrivileges, MemoryDenyWriteExecute, CapabilityBoundingSet= empty, EnvironmentFile, LimitNOFILE=65536) - dist/mcias.env.example: passphrase env file template - dist/mcias.conf.example: fully-commented production TOML config - dist/mcias-dev.conf.example: local dev config (/tmp, short expiry) - dist/mcias.conf.docker.example: container config template - dist/install.sh: POSIX sh idempotent installer; creates mcias user/group, installs binaries, /etc/mcias, /var/lib/mcias, systemd unit, man pages; prints post-install instructions - man/man1/mciassrv.1: mdoc synopsis/config/API/signals/files - man/man1/mciasctl.1: mdoc all subcommands/env/examples - man/man1/mciasdb.1: mdoc trust model/safety/all subcommands - man/man1/mciasgrpcctl.1: mdoc gRPC commands/grpcurl example - README.md: user-facing quick-start, first-run setup, build instructions, CLI references, Docker deployment, security notes - .gitignore: added /bin/, dist/mcias_*.tar.gz, man/man1/*.gz
13 KiB
MCIAS Progress
Source of truth for current development state.
137 tests pass with zero race conditions. Phase 8 (operational artifacts) is complete. Phase 9 (client libraries) is designed and documented; implementation not yet started.
- Phase 0: Repository bootstrap (go.mod, .gitignore, docs)
- Phase 1: Foundational packages (model, config, crypto, db)
- Phase 2: Auth core (auth, token, middleware)
- Phase 3: HTTP server (server, mciassrv binary)
- Phase 4: Admin CLI (mciasctl binary)
- Phase 5: E2E tests, security hardening, commit
- Phase 6: mciasdb — direct SQLite maintenance tool
- Phase 7: gRPC interface (alternate transport; dual-stack with REST)
- Phase 8: Operational artifacts (Makefile, Dockerfile, systemd, man pages, install script)
- Phase 9: Client libraries (Go, Rust, Common Lisp, Python)
Makefile
- Targets: build, test, lint, generate, man, install, clean, dist, docker
- build: compiles all four binaries to bin/ with CGO_ENABLED=1 and -trimpath -ldflags="-s -w"
- dist: cross-compiled tarballs for linux/amd64 and linux/arm64
- docker: builds image tagged mcias:$(git describe --tags --always)
- VERSION derived from git describe --tags --always Dockerfile (multi-stage)
- Build stage: golang:1.26-bookworm with CGO_ENABLED=1
- Runtime stage: debian:bookworm-slim with only ca-certificates and libc6; no Go toolchain, no source, no build cache in final image
- Non-root user mcias (uid/gid 10001)
- EXPOSE 8443 (REST/TLS) and EXPOSE 9443 (gRPC/TLS)
- VOLUME /data for the SQLite database mount point
- ENTRYPOINT ["mciassrv"] CMD ["-config", "/etc/mcias/mcias.conf"] dist/ artifacts
- dist/mcias.service: hardened systemd unit with ProtectSystem=strict, ProtectHome=true, PrivateTmp=true, NoNewPrivileges=true, CapabilityBoundingSet= (no capabilities), ReadWritePaths=/var/lib/mcias, EnvironmentFile=/etc/mcias/env, Restart=on-failure, LimitNOFILE=65536
- dist/mcias.env.example: passphrase env file template
- dist/mcias.conf.example: fully-commented production TOML config reference
- dist/mcias-dev.conf.example: local dev config (127.0.0.1, short expiry)
- dist/mcias.conf.docker.example: container config template
- dist/install.sh: idempotent POSIX sh installer; creates user/group, installs binaries, creates /etc/mcias and /var/lib/mcias, installs systemd unit and man pages; existing configs not overwritten (placed .new) man/ pages (mdoc format)
- man/man1/mciassrv.1: synopsis, options, config, REST API, signals, files
- man/man1/mciasctl.1: all subcommands, env vars, examples
- man/man1/mciasdb.1: trust model warnings, all subcommands, examples
- man/man1/mciasgrpcctl.1: gRPC subcommands, grpcurl examples Documentation
- README.md: replaced dev-workflow notes with user-facing docs; quick-start, first-run setup, build instructions, CLI references, Docker deployment, man page index, security notes
- .gitignore: added /bin/, dist/mcias_.tar.gz, man/man1/.gz
2026-03-11 — Phase 7: gRPC dual-stack
proto/mcias/v1/
common.proto— shared types: Account, TokenInfo, PGCreds, Erroradmin.proto— AdminService: Health (public), GetPublicKey (public)auth.proto— AuthService: Login (public), Logout, RenewToken, EnrollTOTP, ConfirmTOTP, RemoveTOTP (admin)token.proto— TokenService: ValidateToken (public), IssueServiceToken (admin), RevokeToken (admin)account.proto— AccountService (CRUD + roles, all admin) + CredentialService (GetPGCreds, SetPGCreds, all admin)proto/generate.go— go:generate directive for protoc regeneration- Generated Go stubs in
gen/mcias/v1/via protoc + protoc-gen-go-grpc
internal/grpcserver
grpcserver.go— Server struct, interceptor chain (loggingInterceptor → authInterceptor → rateLimitInterceptor), GRPCServer() / GRPCServerWithCreds(creds) / buildServer() helpers, per-IP token-bucket rate limiter (same parameters as REST: 10 req/s, burst 10), extractBearerFromMD, requireAdminadmin.go— Health, GetPublicKey implementationsauth.go— Login (with dummy-Argon2 timing guard), Logout, RenewToken, EnrollTOTP, ConfirmTOTP, RemoveTOTPtokenservice.go— ValidateToken (returns valid=false on error, never an RPC error), IssueServiceToken, RevokeTokenaccountservice.go— ListAccounts, CreateAccount, GetAccount, UpdateAccount, DeleteAccount, GetRoles, SetRolescredentialservice.go— GetPGCreds (AES-GCM decrypt), SetPGCreds (AES-GCM encrypt)
Security invariants (same as REST server):
- Authorization metadata value never logged by any interceptor
- Credential fields (PasswordHash, TOTPSecret*, PGPassword) absent from all proto response messages by proto design + grpcserver enforcement
- JWT validation: alg-first, then signature, then revocation table lookup
- Public RPCs bypass auth: Health, GetPublicKey, ValidateToken, Login
- Admin-only RPCs checked in-handler via requireAdmin(ctx)
- Dummy Argon2 in Login for unknown users prevents timing enumeration
internal/config additions
GRPCAddr stringfield in ServerConfig (optional; omit to disable gRPC)
cmd/mciassrv updates
- Dual-stack: starts both HTTPS (REST) and gRPC/TLS listeners when grpc_addr is configured in [server] section
- gRPC listener uses same TLS cert/key as REST; credentials passed at server-construction time via GRPCServerWithCreds
- Graceful shutdown drains both listeners within 15s window
cmd/mciasgrpcctl
- New companion CLI for gRPC management
- Global flags: -server (host:port), -token (or MCIAS_TOKEN), -cacert
- Commands: health, pubkey, account (list/create/get/update/delete), role (list/set), token (validate/issue/revoke), pgcreds (get/set)
- Connects with TLS; custom CA cert support for self-signed certs
Tests
internal/grpcserver/grpcserver_test.go: 20 tests using bufconn (in-process, no network sockets); covers:- Health and GetPublicKey (public RPCs, no auth)
- Auth interceptor: no token, invalid token, revoked token all → 401
- Non-admin calling admin RPC → 403
- Login: success, wrong password, unknown user
- Logout and RenewToken
- ValidateToken: good token → valid=true; garbage → valid=false (no error)
- IssueServiceToken requires admin
- ListAccounts: non-admin → 403, admin → OK
- CreateAccount, GetAccount, UpdateAccount, SetRoles, GetRoles lifecycle
- SetPGCreds + GetPGCreds with AES-GCM round-trip verification
- PGCreds requires admin
- Credential fields absent from account responses (structural enforcement)
Dependencies added
google.golang.org/grpc v1.68.0google.golang.org/protobuf v1.36.0google.golang.org/grpc/test/bufconn(test only, included in grpc module)
Total: 137 tests, all pass, zero race conditions (go test -race ./...)
2026-03-11 — Phase 6: mciasdb
cmd/mciasdb
- Binary skeleton: config loading, master key derivation (identical to mciassrv for key compatibility), DB open + migrate on startup
schema verify/schema migrate— reports and applies pending migrationsaccount list/get/create/set-password/set-status/reset-totp— offline account management; set-password prompts interactively (no --password flag)role list/grant/revoke— direct role managementtoken list/revoke/revoke-all+prune tokens— token maintenanceaudit tail/query— audit log inspection with --json output flagpgcreds get/set— decrypt/encrypt Postgres credentials with master key; set prompts interactively; get prints warning before sensitive output- All write operations emit audit log entries tagged
actor:"mciasdb"
internal/db additions
ListTokensForAccount(accountID)— newest-first token list for an accountListAuditEvents(AuditQueryParams)— filtered audit query (account, type, since, limit)TailAuditEvents(n)— last n events, returned oldest-firstSchemaVersion(db)/LatestSchemaVersion— exported for mciasdb verify
Dependencies
- Added
golang.org/x/term v0.29.0for interactive password prompting (no-echo terminal reads); pinned to version compatible with local module cache golang.org/x/cryptopinned at v0.33.0 (compatible with term@v0.29.0)
Tests
internal/db/mciasdb_test.go: 4 tests covering ListTokensForAccount, ListAuditEvents filtering, TailAuditEvents ordering, combined filterscmd/mciasdb/mciasdb_test.go: 20 tests covering all subcommands via in-memory SQLite and stdout capture
Total: 117 tests, all pass, zero race conditions (go test -race ./...)
2026-03-11 — Initial Full Implementation
Phase 0: Bootstrap
- Wrote ARCHITECTURE.md (security model, crypto choices, DB schema, API design)
- Wrote PROJECT_PLAN.md (5 phases, 12 steps with acceptance criteria)
- Created go.mod with dependencies (golang-jwt/jwt/v5, uuid, go-toml/v2, golang.org/x/crypto, modernc.org/sqlite)
- Created .gitignore
Phase 1: Foundational Packages
internal/model
- Account (human/system), Role, TokenRecord, SystemToken, PGCredential, AuditEvent structs
- All credential fields tagged
json:"-"— never serialised to responses - Audit event type constants
internal/config
- TOML config parsing with validation
- Enforces OWASP 2023 Argon2id minimums (time≥2, memory≥64MiB)
- Requires exactly one of passphrase_env or keyfile for master key
- NewTestConfig() for test use
internal/crypto
- Ed25519 key generation, PEM marshal/parse
- AES-256-GCM seal/open with random nonces
- Argon2id KDF (DeriveKey) with OWASP-exceeding parameters
- NewSalt(), RandomBytes()
internal/db
- SQLite with WAL mode, FK enforcement, busy timeout
- Idempotent migrations (schema_version table)
- Migration 1: full schema (server_config, accounts, account_roles, token_revocation, system_tokens, pg_credentials, audit_log)
- Migration 2: master_key_salt column in server_config
- Full CRUD: accounts, roles, tokens, PG credentials, audit log
Phase 2: Auth Core
internal/auth
- Argon2id password hashing in PHC format
- Constant-time password verification (crypto/subtle)
- TOTP generation and validation (RFC 6238 ±1 window, constant-time)
- HOTP per RFC 4226
internal/token
- Ed25519/EdDSA JWT issuance with UUID JTI
- alg header validated BEFORE signature verification (alg confusion defence)
- alg:none explicitly rejected
- ErrWrongAlgorithm, ErrExpiredToken, ErrInvalidSignature, ErrMissingClaim
internal/middleware
- RequestLogger — never logs Authorization header
- RequireAuth — validates JWT, checks revocation table
- RequireRole — checks claims for required role
- RateLimit — per-IP token bucket
Phase 3: HTTP Server
internal/server
- Full REST API wired to middleware
- Handlers: health, public-key, login (dummy Argon2 on unknown user for timing uniformity), logout, renew, token validate/issue/revoke, account CRUD, roles, TOTP enrol/confirm/remove, PG credentials
- Strict JSON decoding (DisallowUnknownFields)
- Credential fields never appear in any response
cmd/mciassrv
- Config loading, master key derivation (passphrase via Argon2id KDF or key file), signing key load/generate (AES-256-GCM encrypted in DB), HTTPS listener with graceful shutdown
- TLS 1.2+ minimum, X25519+P256 curves
- 30s read/write timeouts, 5s header timeout
Phase 4: Admin CLI
cmd/mciasctl
- Subcommands: account (list/create/get/update/delete), role (list/set), token (issue/revoke), pgcreds (get/set)
- Auth via -token flag or MCIAS_TOKEN env var
- Custom CA cert support for self-signed TLS
Phase 5: Tests and Hardening
Test coverage:
- internal/model: 5 tests
- internal/config: 8 tests
- internal/crypto: 12 tests
- internal/db: 13 tests
- internal/auth: 13 tests
- internal/token: 9 tests (including alg confusion and alg:none attacks)
- internal/middleware: 12 tests
- internal/server: 14 tests
- test/e2e: 11 tests
Total: 97 tests — all pass, zero race conditions (go test -race ./...)
Security tests (adversarial):
- JWT alg:HS256 confusion attack → 401
- JWT alg:none attack → 401
- Revoked token reuse → 401
- Non-admin calling admin endpoint → 403
- Wrong password → 401 (same response as unknown user)
- Credential material absent from all API responses
Security hardening:
- go vet ./... — zero issues
- gofmt applied to all files
- golangci-lint v2 config updated (note: v2.6.2 built with go1.25.3 cannot analyse go1.26 source; go vet used as primary linter for now)
Architecture Decisions
- SQLite driver:
modernc.org/sqlite(pure Go, no CGo) - JWT:
github.com/golang-jwt/jwt/v5; alg validated manually before library dispatch to defeat algorithm confusion - No ORM:
database/sqlwith parameterized statements only - Master key salt: stored in server_config table for stable KDF across restarts; generated on first run
- Signing key: stored AES-256-GCM encrypted in server_config; generated on first run, decrypted each startup using master key
- Timing uniformity: unknown user login runs dummy Argon2 to match
timing of wrong-password path; all credential comparisons use
crypto/subtle.ConstantTimeCompare