Files
mcias/PROGRESS.md
Kyle Isom bbf9f6fe3f Release v1.0.0
- All phases complete: REST API, gRPC, admin CLIs, policy engine,
  account tags, Postgres credentials UI, policies UI, client libraries
  (Go, Rust, Common Lisp, Python)
- All tests pass; golangci-lint clean
- make docker now tags mcias:VERSION and mcias:latest
2026-03-11 23:29:29 -07:00

23 KiB

MCIAS Progress

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-11 — Postgres Credentials UI + Policy/Tags UI completion

internal/ui/

  • handlers_accounts.go: added handleSetPGCreds — validates form fields, encrypts password via crypto.SealAESGCM with fresh nonce, calls db.WritePGCredentials, writes EventPGCredUpdated audit entry, re-reads and renders pgcreds_form fragment; password never echoed in response
  • handlers_accounts.go: updated handleAccountDetail to load PG credentials for system accounts (non-fatal on ErrNotFound) and account tags for all accounts
  • handlers_policy.go: fixed handleSetAccountTags to render with AccountDetailData (removed AccountTagsData); field ordering fixed for fieldalignment linter
  • ui.go: added PGCred *model.PGCredential and Tags []string to AccountDetailData; added pgcreds_form.html and tags_editor.html to shared template set; registered PUT /accounts/{id}/pgcreds and PUT /accounts/{id}/tags routes; removed unused AccountTagsData struct; field alignment fixed on PolicyRuleView, PoliciesData, AccountDetailData
  • ui_test.go: added 5 new PG credential tests: TestSetPGCredsRejectsHumanAccount, TestSetPGCredsStoresAndDisplaysMetadata, TestSetPGCredsPasswordNotEchoed, TestSetPGCredsRequiresPassword, TestAccountDetailShowsPGCredsSection

web/templates/

  • fragments/pgcreds_form.html (new): displays current credential metadata (host:port, database, username, updated-at — no password); includes HTMX hx-put form for set/replace; system accounts only
  • fragments/tags_editor.html (new): newline-separated tag textarea with HTMX hx-put for atomic replacement; uses .Account.UUID for URL
  • fragments/policy_form.html: rewritten to use structured fields matching handleCreatePolicyRule parser: description, priority, effect (select), roles/account_types/actions (multi-select), resource_type, subject_uuid, service_names, required_tags, owner_matches_subject (checkbox)
  • policies.html (new): policies management page with create-form toggle and rules table (id="policies-tbody")
  • fragments/policy_row.html (new): HTMX table row with enable/disable toggle (hx-patch) and delete button (hx-delete)
  • account_detail.html: added Tags card (all accounts) and Postgres Credentials card (system accounts only)
  • base.html: added Policies nav link

internal/server/server.go

  • Removed ~220 lines of duplicate tag and policy handler code that had been inadvertently added; all real implementations live in handlers_policy.go

internal/policy/engine_wrapper.go

  • Fixed corrupted source file (invisible character preventing fmt usage from being recognized); rewrote to use errors.New for the denial error

internal/db/policy_test.go

  • Fixed CreateAccount call using string literal "human"model.AccountTypeHuman

cmd/mciasctl/main.go

  • Added //nolint:gosec to three int(os.Stdin.Fd()) conversions (safe: uintptr == int on all target platforms; term.ReadPassword requires int)

Linter fixes (all packages)

  • gofmt/goimports applied to internal/db/policy_test.go, internal/policy/defaults.go, internal/policy/engine_test.go, internal/ui/ui.go
  • fieldalignment fixed on model.PolicyRuleRecord, policy.Engine, policy.Rule, policy.RuleBody, ui.PolicyRuleView

All tests pass (go test ./...); golangci-lint run ./... reports 0 issues.

2026-03-11 — v1.0.0 release

  • Makefile: make docker now tags image as both mcias:$(VERSION) and mcias:latest in a single build invocation
  • Tagged v1.0.0 — first stable release

  • 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)
  • Phase 10: Policy engine — ABAC with machine/service gating

2026-03-11 — Phase 10: Policy engine (ABAC + machine/service gating)

internal/policy/ (new package)

  • policy.go — types: Action, ResourceType, Effect, Resource, PolicyInput, Rule, RuleBody; 22 Action constants covering all API operations
  • engine.goEvaluate(input, operatorRules) (Effect, *Rule): pure function; merges operator rules with default rules, sorts by priority, deny-wins, then first allow, then default-deny
  • defaults.go — 6 compiled-in rules (IDs -1 to -6, Priority 0): admin wildcard, self-service logout/renew, self-service TOTP, system account own pgcreds, system account own service token, public login/validate endpoints
  • engine_wrapper.goEngine struct with sync.RWMutex; SetRules() decodes DB records; PolicyRecord type avoids import cycle
  • engine_test.go — 11 tests: DefaultDeny, AdminWildcard, SelfService*, SystemOwn*, DenyWins, ServiceNameGating, MachineTagGating, OwnerMatchesSubject, PriorityOrder, MultipleRequiredTags, AccountTypeGating

internal/db/

  • migrate.go: migration id=4 — account_tags (account_id+tag PK, FK cascade) and policy_rules (id, priority, description, rule_json, enabled, created_by, timestamps) tables
  • tags.go (new): GetAccountTags, AddAccountTag, RemoveAccountTag, SetAccountTags (atomic DELETE+INSERT transaction); sorted alphabetically
  • policy.go (new): CreatePolicyRule, GetPolicyRule, ListPolicyRules, UpdatePolicyRule, SetPolicyRuleEnabled, DeletePolicyRule
  • tags_test.go, policy_test.go (new): comprehensive DB-layer tests

internal/model/

  • PolicyRuleRecord struct added
  • New audit event constants: EventTagAdded, EventTagRemoved, EventPolicyRuleCreated, EventPolicyRuleUpdated, EventPolicyRuleDeleted, EventPolicyDeny

internal/middleware/

  • RequirePolicy middleware: assembles PolicyInput from JWT claims + AccountTypeLookup closure (DB-backed, avoids JWT schema change) + ResourceBuilder closure; calls engine.Evaluate; logs deny via PolicyDenyLogger

internal/server/

  • New REST endpoints (all require admin):
    • GET/PUT /v1/accounts/{id}/tags
    • GET/POST /v1/policy/rules
    • GET/PATCH/DELETE /v1/policy/rules/{id}
  • handlers_policy.go: handleGetTags, handleSetTags, handleListPolicyRules, handleCreatePolicyRule, handleGetPolicyRule, handleUpdatePolicyRule, handleDeletePolicyRule, policyRuleToResponse, loadPolicyRule

internal/ui/

  • handlers_policy.go (new): handlePoliciesPage, handleCreatePolicyRule, handleTogglePolicyRule, handleDeletePolicyRule, handleSetAccountTags
  • ui.go: registered 5 policy UI routes; added PolicyRuleView, PoliciesData, AccountTagsData view types; added new fragment templates to shared set

web/templates/

  • policies.html (new): policies management page
  • fragments/policy_row.html (new): HTMX table row with enable/disable toggle and delete button
  • fragments/policy_form.html (new): create form with JSON textarea and action reference chips
  • fragments/tags_editor.html (new): newline-separated tag editor with HTMX PUT for atomic replacement
  • account_detail.html: added Tags card section using tags_editor fragment
  • base.html: added Policies nav link

cmd/mciasctl/

  • policy subcommands: list, create -description STR -json FILE [-priority N], get -id ID, update -id ID [-priority N] [-enabled true|false], delete -id ID
  • tag subcommands: list -id UUID, set -id UUID -tags tag1,tag2,...

openapi.yaml

  • New schemas: TagsResponse, RuleBody, PolicyRule
  • New paths: GET/PUT /v1/accounts/{id}/tags, GET/POST /v1/policy/rules, GET/PATCH/DELETE /v1/policy/rules/{id}
  • New tag: Admin — Policy

Design highlights:

  • Deny-wins + default-deny: explicit Deny beats any Allow; no match = Deny
  • AccountType resolved via DB lookup (not JWT) to avoid breaking 29 IssueToken call sites
  • RequirePolicy wired alongside RequireRole("admin") for belt-and-suspenders during migration; defaults reproduce current binary behavior exactly
  • policy.PolicyRecord type avoids circular import between policy/db/model

All tests pass; go test ./... clean; golangci-lint run ./... clean.

2026-03-11 — Fix test failures and lockout logic

  • internal/db/accounts.go (IsLockedOut): corrected window-expiry check from LockoutWindow+LockoutDuration to LockoutWindow; stale failures outside the rolling window now correctly return not-locked regardless of count
  • internal/grpcserver/grpcserver_test.go (TestUpdateAccount, TestSetAndGetRoles): updated test passwords from 9-char "pass12345" to 13-char "pass123456789" to satisfy the 12-character minimum (F-13)
  • Reformatted import blocks in both files with goimports to resolve gci lint warnings

All 5 packages pass go test ./...; golangci-lint run ./... clean.

2026-03-11 — Phase 9: Client libraries

clients/testdata/ — shared JSON fixtures

  • login_response.json, account_response.json, accounts_list_response.json
  • validate_token_response.json, public_key_response.json, pgcreds_response.json
  • error_response.json, roles_response.json

clients/go/ — Go client library

  • Module: git.wntrmute.dev/kyle/mcias/clients/go; package mciasgoclient
  • Typed errors: MciasAuthError, MciasForbiddenError, MciasNotFoundError, MciasInputError, MciasConflictError, MciasServerError
  • TLS 1.2+ enforced via tls.Config{MinVersion: tls.VersionTLS12}
  • Token state guarded by sync.RWMutex for concurrent safety
  • JSON decoded with DisallowUnknownFields on all responses
  • 25 tests in client_test.go; all pass with go test -race

clients/rust/ — Rust async client library

  • Crate: mcias-client; tokio async, reqwest + rustls-tls (no OpenSSL dep)
  • MciasError enum via thiserror; Arc<RwLock<Option<String>>> for token
  • 23 integration tests using wiremock; cargo clippy -- -D warnings clean

clients/lisp/ — Common Lisp client library

  • ASDF system mcias-client; HTTP via dexador, JSON via yason
  • CLOS class mcias-client; plain functions for all operations
  • Conditions: mcias-error base + 6 typed subclasses
  • Mock server: Hunchentoot mock-dispatcher subclass (port 0, random per test)
  • 37 fiveam checks; all pass on SBCL 2.6.1
  • Fixed: yason decodes JSON false as :false; validate-token normalises to t/nil before returning

clients/python/ — Python 3.11+ client library

  • Package mcias_client (setuptools, pyproject.toml); dep: httpx >= 0.27
  • Client context manager; py.typed marker; all symbols fully annotated
  • Dataclasses: Account, PublicKey, PGCreds
  • 32 pytest tests using respx mock transport; mypy --strict clean; ruff clean

test/mock/mockserver.go — Go in-memory mock server

  • Server struct with sync.RWMutex; used by Go client integration test
  • NewServer(), AddAccount(), ServeHTTP() for httptest.Server use

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, Error
  • admin.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, requireAdmin
  • admin.go — Health, GetPublicKey implementations
  • auth.go — Login (with dummy-Argon2 timing guard), Logout, RenewToken, EnrollTOTP, ConfirmTOTP, RemoveTOTP
  • tokenservice.go — ValidateToken (returns valid=false on error, never an RPC error), IssueServiceToken, RevokeToken
  • accountservice.go — ListAccounts, CreateAccount, GetAccount, UpdateAccount, DeleteAccount, GetRoles, SetRoles
  • credentialservice.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 string field 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.0
  • google.golang.org/protobuf v1.36.0
  • google.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 migrations
  • account 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 management
  • token list/revoke/revoke-all + prune tokens — token maintenance
  • audit tail/query — audit log inspection with --json output flag
  • pgcreds 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 account
  • ListAuditEvents(AuditQueryParams) — filtered audit query (account, type, since, limit)
  • TailAuditEvents(n) — last n events, returned oldest-first
  • SchemaVersion(db) / LatestSchemaVersion — exported for mciasdb verify

Dependencies

  • Added golang.org/x/term v0.29.0 for interactive password prompting (no-echo terminal reads); pinned to version compatible with local module cache
  • golang.org/x/crypto pinned 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 filters
  • cmd/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/sql with 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