From 094741b56d8c1ddb3325c173ca23fde3fc5587fb Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Wed, 11 Mar 2026 14:15:27 -0700 Subject: [PATCH] Planning updates. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Document gRPC interface, operational artifacts, and client libraries for Phases 7–9 planning. + Update PROGRESS.md to reflect completed design and pending implementation. --- .junie/guidelines.md | 1 + ARCHITECTURE.md | 343 +++++++++++++++++++++++++++++++++++++++++++ PROGRESS.md | 11 +- 3 files changed, 353 insertions(+), 2 deletions(-) create mode 120000 .junie/guidelines.md diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fbcb004..04e2cf8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -689,3 +689,346 @@ By default all output is human-readable text. `--json` flag switches to newline-delimited JSON for scripting. Credential fields follow the same `json:"-"` exclusion rules as the API — they are only printed when the specific `get` or `pgcreds get` command is invoked, never in list output. + +--- + +## 17. gRPC Interface (Phase 7) + +### Rationale + +The REST API is the primary interface and will remain so. A gRPC interface is +added as an alternate transport for clients that prefer strongly-typed stubs, +streaming, or lower per-request overhead. The two interfaces are strictly +equivalent in capability and security posture; they share all business logic +in the `internal/` packages. + +gRPC is not a replacement for REST. Both listeners run concurrently. Operators +may disable the gRPC listener by omitting `grpc_addr` from config. + +### Proto Package Layout + +``` +proto/ +└── mcias/ + └── v1/ + ├── auth.proto # Login, Logout, Renew, TOTP enroll/confirm/remove + ├── token.proto # Validate, Issue, Revoke + ├── account.proto # CRUD for accounts and roles + ├── admin.proto # Health, public-key retrieval + └── common.proto # Shared message types (Error, Timestamp wrappers) + +gen/ +└── mcias/ + └── v1/ # Generated Go stubs (buf generate output) +``` + +Generated code is committed to the repository under `gen/`. `buf generate` +is idempotent and is re-run via `go generate ./...`. + +### Service Definitions (summary) + +| Service | RPCs | +|---|---| +| `AuthService` | `Login`, `Logout`, `RenewToken`, `EnrollTOTP`, `ConfirmTOTP`, `RemoveTOTP` | +| `TokenService` | `ValidateToken`, `IssueServiceToken`, `RevokeToken` | +| `AccountService` | `ListAccounts`, `CreateAccount`, `GetAccount`, `UpdateAccount`, `DeleteAccount`, `GetRoles`, `SetRoles` | +| `CredentialService` | `GetPGCreds`, `SetPGCreds` | +| `AdminService` | `Health`, `GetPublicKey` | + +All request/response messages follow the same credential-exclusion rules as +the JSON API: `PasswordHash`, `TOTPSecret*`, and `PGPassword` fields are +never present in any response message. + +### Transport Security + +- The gRPC server uses the same TLS certificate and key as the REST server. + TLS 1.2 minimum is enforced via `tls.Config` (identical to the REST server). +- Mutual TLS is out of scope for v1 but is architecturally compatible (the + `tls.Config` can be extended). +- No plaintext (h2c) mode is provided. Connecting without TLS is refused. + +### Authentication and Authorization + +Authentication in gRPC uses the same JWT validation logic as the REST +middleware: + +1. The gRPC unary interceptor extracts the `authorization` metadata key. +2. It expects the value `Bearer ` (case-insensitive prefix). +3. The token is validated via `internal/token.ValidateToken` — same alg-first + check, same revocation table lookup. +4. Claims are injected into the `context.Context` for downstream handlers. +5. Admin RPCs are guarded by a second interceptor that checks the `admin` role + in the injected claims — identical to the REST `RequireRole` middleware. + +A missing or invalid token returns `codes.Unauthenticated`. Insufficient role +returns `codes.PermissionDenied`. No credential material is included in error +details. + +### Interceptor Chain + +``` +[Request Logger] → [Auth Interceptor] → [Rate Limiter] → [Handler] +``` + +- **Request Logger**: logs method, peer IP, status code, duration; never logs + the `authorization` metadata value. +- **Auth Interceptor**: validates Bearer JWT, injects claims. Public RPCs + (`Health`, `GetPublicKey`, `ValidateToken`) bypass auth. +- **Rate Limiter**: per-IP token bucket with the same parameters as the REST + rate limiter (10 req/s burst). Exceeding the limit returns `codes.ResourceExhausted`. + +### Dual-Stack Operation + +mciassrv starts both listeners in the same process: + +``` +┌──────────────────────────────────────────────┐ +│ mciassrv process │ +│ │ +│ ┌────────────────┐ ┌────────────────────┐ │ +│ │ REST listener │ │ gRPC listener │ │ +│ │ (net/http) │ │ (google.golang. │ │ +│ │ :8443 │ │ org/grpc) :8444 │ │ +│ └───────┬─────────┘ └──────────┬─────────┘ │ +│ └──────────────┬─────────┘ │ +│ ▼ │ +│ ┌─────────────────────────────┐ │ +│ │ Shared: signing key, DB, │ │ +│ │ config, rate-limit state │ │ +│ └─────────────────────────────┘ │ +└──────────────────────────────────────────────┘ +``` + +Both listeners share a single `*db.DB` connection, the same in-memory signing +key, and the same rate-limiter state. Graceful shutdown drains both within the +configured window. + +### Configuration Addition + +```toml +[server] +listen_addr = "0.0.0.0:8443" +grpc_addr = "0.0.0.0:8444" # optional; omit to disable gRPC +tls_cert = "/etc/mcias/server.crt" +tls_key = "/etc/mcias/server.key" +``` + +### `cmd/mciasgrpcctl` — gRPC Admin CLI + +An optional companion CLI (`mciasgrpcctl`) provides the same subcommands as +`mciasctl` but over gRPC. It is a thin client that wraps the generated stubs. +Auth and CA-cert flags are identical to `mciasctl`. Both CLIs can coexist; +neither depends on the other. + +--- + +## 18. Operational Artifacts (Phase 8) + +### Artifact Inventory + +| Artifact | Path | Purpose | +|---|---|---| +| systemd unit | `dist/mcias.service` | Production service management | +| Environment template | `dist/mcias.env.example` | Master key and other secrets | +| Reference config | `dist/mcias.conf.example` | Annotated production config | +| Dev config | `dist/mcias-dev.conf.example` | Local development defaults | +| Install script | `dist/install.sh` | First-time setup on a Linux host | +| Man page: mciassrv | `man/man1/mciassrv.1` | Server binary reference | +| Man page: mciasctl | `man/man1/mciasctl.1` | Admin CLI reference | +| Man page: mciasdb | `man/man1/mciasdb.1` | DB tool reference | +| Man page: mciasgrpcctl | `man/man1/mciasgrpcctl.1` | gRPC CLI reference | +| Makefile | `Makefile` | Build, test, lint, install, release targets | + +### systemd Unit Design + +The service unit applies a conservative sandboxing profile: + +- `User=mcias` / `Group=mcias` — no root privileges required +- `ProtectSystem=strict` — filesystem read-only except declared `ReadWritePaths` +- `ReadWritePaths=/var/lib/mcias` — SQLite database directory only +- `PrivateTmp=true` — isolated `/tmp` +- `NoNewPrivileges=true` — seccomp/capability escalation blocked +- `CapabilityBoundingSet=` — empty; no Linux capabilities needed (port ≥ 1024) +- `EnvironmentFile=/etc/mcias/env` — secrets injected from file, not inline + +The unit does not start the service on install. Operators must run +`systemctl enable --now mcias` explicitly after verifying configuration. + +### Filesystem Layout (post-install) + +``` +/usr/local/bin/ + mciassrv + mciasctl + mciasdb + mciasgrpcctl (if gRPC phase installed) + +/etc/mcias/ + mcias.conf (config file; mode 0640, owner root:mcias) + env (environment file with MCIAS_MASTER_PASSPHRASE; mode 0640) + server.crt (TLS certificate; mode 0644) + server.key (TLS private key; mode 0640, owner root:mcias) + +/var/lib/mcias/ + mcias.db (SQLite database; mode 0660, owner mcias:mcias) + +/usr/share/man/man1/ + mciassrv.1.gz + mciasctl.1.gz + mciasdb.1.gz + mciasgrpcctl.1.gz +``` + +### Makefile Targets + +| Target | Action | +|---|---| +| `build` | Compile all binaries to `bin/` using current GOOS/GOARCH | +| `test` | `go test -race ./...` | +| `lint` | `golangci-lint run ./...` | +| `generate` | `go generate ./...` (re-generates proto stubs) | +| `man` | Build man pages; compress to `.gz` in `man/` | +| `install` | Run `dist/install.sh` | +| `clean` | Remove `bin/`, `gen/`, compressed man pages | +| `dist` | Cross-compile release tarballs for linux/amd64 and linux/arm64 | + +### Upgrade Path + +The install script is idempotent. Running it again after a new release: +1. Overwrites binaries in `/usr/local/bin/` +2. Does **not** overwrite `/etc/mcias/mcias.conf` or `/etc/mcias/env` (backs + them up with a `.bak` suffix and skips if unchanged) +3. Does **not** run `mciasdb schema migrate` automatically — the operator + must do this manually before restarting the service + +--- + +## 19. Client Libraries (Phase 9) + +### Design Goals + +Client libraries exist to make it easy for relying-party applications to +authenticate users via MCIAS without needing to understand JWT handling, TLS +configuration, or the HTTP API wire format. Each library: + +1. Exposes the canonical API surface (defined in `clients/README.md`). +2. Handles token storage, renewal, and error classification internally. +3. Enforces TLS (no plaintext) and validates the server certificate by default. +4. Never logs or exposes credential material. +5. Is independently versioned and testable. + +### Canonical API Surface + +Every language implementation must expose: + +``` +Client(server_url, [ca_cert], [token]) + +# Authentication +client.login(username, password, [totp_code]) → (token, expires_at) +client.logout() → void +client.renew_token() → (token, expires_at) + +# Token operations +client.validate_token(token) → claims +client.get_public_key() → jwk + +# Health +client.health() → void # raises/errors on failure + +# Account management (admin) +client.create_account(username, type) → account +client.list_accounts() → [account] +client.get_account(id) → account +client.update_account(id, updates) → account +client.delete_account(id) → void + +# Role management (admin) +client.get_roles(account_id) → [role] +client.set_roles(account_id, roles) → void + +# Token management (admin or role-scoped) +client.issue_service_token(account_id) → (token, expires_at) +client.revoke_token(jti) → void + +# PG credentials (admin or role-scoped) +client.get_pg_creds(account_id) → pg_creds +client.set_pg_creds(account_id, pg_creds) → void +``` + +Error types exposed by every library: + +| Error | Meaning | +|---|---| +| `MciasAuthError` / `Unauthenticated` | Token missing, invalid, or expired | +| `MciasForbiddenError` / `PermissionDenied` | Insufficient role | +| `MciasNotFoundError` / `NotFound` | Resource does not exist | +| `MciasInputError` / `InvalidArgument` | Malformed request | +| `MciasServerError` / `Internal` | Unexpected server error | +| `MciasTransportError` | Network/TLS failure | + +### Per-Language Implementation Notes + +#### Go (`clients/go/`) + +- Module: `git.wntrmute.dev/kyle/mcias/clients/go` +- Package: `mciasgoclient` +- HTTP: `net/http` with custom `*tls.Config` for CA cert +- Token state: guarded by `sync.RWMutex` +- JSON: `encoding/json` with `DisallowUnknownFields` on all decoders +- Error wrapping: `fmt.Errorf("mciasgoclient: %w", err)` preserving cause + +#### Rust (`clients/rust/`) + +- Crate: `mcias-client` (published to crates.io when stable) +- Runtime: `tokio`-async; `reqwest` for HTTP +- TLS: `rustls` backend (no OpenSSL dependency); custom CA via + `reqwest::Certificate` +- Error type: `MciasError` enum deriving `thiserror::Error` +- Serialization: `serde` + `serde_json`; strict unknown-field rejection + via `#[serde(deny_unknown_fields)]` +- Token state: `Arc>>` + +#### Common Lisp (`clients/lisp/`) + +- ASDF system: `mcias-client` +- HTTP: `dexador` +- JSON: `yason` (or `cl-json`; prefer `yason` for streaming) +- TLS: delegated to Dexador/Usocket; custom CA documented per platform +- API: CLOS class `mcias-client` with slot `token`; methods are generic + functions +- Conditions: `mcias-error`, `mcias-unauthenticated`, `mcias-forbidden`, + `mcias-not-found` — all subclasses of `mcias-error` +- Tests: `fiveam` test suite; mock responses via local TCP server or + Dexador's mock facility if available +- Compatibility: SBCL primary; CCL secondary + +#### Python (`clients/python/`) + +- Package: `mcias_client` (PEP 517 build; `pyproject.toml`) +- HTTP: `httpx` (provides both sync `MciasClient` and async `AsyncMciasClient`) +- TLS: `ssl.create_default_context(cafile=...)` for custom CA +- Types: `py.typed` marker; all public symbols fully annotated; `mypy --strict` +- Errors: `MciasError(Exception)` base with subclasses as listed above +- Token state: `_token: str | None` instance attribute; thread-safe in sync + variant via `threading.Lock` +- Python version support: 3.11+ +- Linting: `ruff check` (replaces flake8/isort); `ruff format` for style + +### Versioning Strategy + +Each client library follows the MCIAS server's minor version. Breaking changes +to the API surface increment the major version. The proto definitions (Phase 7) +serve as the source of truth for the canonical interface; client libraries +implement a subset matching the REST API. + +Client libraries are not coupled to each other. A user of the Python library +does not need the Go library installed. + +### Mock Server + +`test/mock/` contains a Go binary (`mcias-mock`) that implements a minimal +in-memory MCIAS server for use in client library integration tests. It +supports the full REST API surface with configurable fixture responses. +Language-specific test suites start `mcias-mock` as a subprocess and connect +to it over localhost TLS (using a bundled self-signed test certificate). diff --git a/PROGRESS.md b/PROGRESS.md index 71a25a3..00a4fb9 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -4,9 +4,10 @@ Source of truth for current development state. --- -## Current Status: Phase 6 Complete — Full Implementation +## Current Status: Phase 6 Complete — Phases 7–9 Planned -All phases complete. 117 tests pass with zero race conditions. +117 tests pass with zero race conditions. Phases 7–9 are designed and +documented; implementation not yet started. ### Completed Phases @@ -18,6 +19,12 @@ All phases complete. 117 tests pass with zero race conditions. - [x] Phase 5: E2E tests, security hardening, commit - [x] Phase 6: mciasdb — direct SQLite maintenance tool +### Planned Phases + +- [ ] Phase 7: gRPC interface (alternate transport; dual-stack with REST) +- [ ] Phase 8: Operational artifacts (systemd unit, man pages, Makefile, install script) +- [ ] Phase 9: Client libraries (Go, Rust, Common Lisp, Python) + --- ## Implementation Log