Files
mcias/ARCHITECTURE.md
Kyle Isom 14083b82b4 Fix linting: golangci-lint v2 config, nolint annotations
* Rewrite .golangci.yaml to v2 schema: linters-settings ->
  linters.settings, issues.exclude-rules -> issues.exclusions.rules,
  issues.exclude-dirs -> issues.exclusions.paths
* Drop deprecated revive exported/package-comments rules: personal
  project, not a public library; godoc completeness is not a CI req
* Add //nolint:gosec G101 on PassphraseEnv default in config.go:
  environment variable name is not a credential value
* Add //nolint:gosec G101 on EventPGCredUpdated in model.go:
  audit event type string, not a credential

Security: no logic changes. gosec G101 suppressions are false
positives confirmed by code inspection: neither constant holds a
credential value.
2026-03-11 12:53:25 -07:00

692 lines
28 KiB
Markdown

# MCIAS Architecture
Metacircular Identity and Access System — Technical Design Document
---
## 1. System Overview
MCIAS is a self-hosted SSO and IAM service for a single developer's personal
applications. It is deliberately small-scope: no federation, no multi-tenant
complexity, no external IdP delegation. The security model is simple but
rigorous: all trust flows from the MCIAS server; applications are relying
parties that delegate authentication decisions to it.
### Components
```
┌────────────────────────────────────────────────────┐
│ MCIAS Server (mciassrv) │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Auth │ │ Token │ │ Account / Role │ │
│ │ Handler │ │ Manager │ │ Manager │ │
│ └────┬─────┘ └────┬─────┘ └─────────┬─────────┘ │
│ └─────────────┴─────────────────┘ │
│ │ │
│ ┌─────────▼──────────┐ │
│ │ SQLite Database │ │
│ └────────────────────┘ │
└────────────────────────────────────────────────────┘
▲ ▲ ▲
│ HTTPS/REST │ HTTPS/REST │ direct file I/O
│ │ │
┌──────┴──────┐ ┌────┴─────┐ ┌──────┴──────┐
│ Personal │ │ mciasctl │ │ mciasdb │
│ Apps │ │ (admin │ │ (DB tool) │
└─────────────┘ │ CLI) │ └─────────────┘
└──────────┘
```
**mciassrv** — The authentication server. Exposes a REST API over HTTPS/TLS.
Handles login, token issuance, token validation, token renewal, and token
revocation.
**mciasctl** — The administrator CLI. Communicates with mciassrv's REST API
using an admin JWT. Creates/manages human accounts, system accounts, roles,
and Postgres credential records.
**mciasdb** — The database maintenance tool. Operates directly on the SQLite
file, bypassing the server API. Intended for break-glass recovery, offline
inspection, schema verification, and maintenance tasks that cannot be
performed through the live server. Requires the same master key material as
mciassrv (passphrase or keyfile) to decrypt secrets at rest.
---
## 2. Security Model
### Threat Model
- **Attacker capabilities assumed:** Network interception (mitigated by TLS),
credential guessing (mitigated by Argon2id, account lockout), stolen JWT
(mitigated by short expiry + revocation), stolen DB file (mitigated by
hashed/encrypted credentials at rest).
- **Out of scope:** Physical access to the server host, OS-level compromise,
supply-chain attacks on Go dependencies.
- **Trust boundary:** The MCIAS server is the single root of trust. Applications
must not make authorization decisions without first validating a JWT from
MCIAS. All signing keys live exclusively on the MCIAS server.
### Key Principles
1. **Defense in depth.** Passwords are hashed with Argon2id; JWTs are signed
with Ed25519; all transport uses TLS 1.2+ (TLS 1.3 preferred).
2. **Least privilege.** System accounts have no interactive login path. Human
accounts have only the roles explicitly granted. Admin operations require
the `admin` role.
3. **Fail closed.** Invalid, expired, or unrecognized tokens must be rejected
immediately. Missing claims are not assumed; they are treated as invalid.
4. **No credential leakage.** Passwords, raw tokens, and private keys must
never appear in logs, error messages, API responses, or stack traces.
5. **Constant-time comparisons.** All equality checks on secret material
(tokens, password hashes, TOTP codes) use `crypto/subtle.ConstantTimeCompare`
to prevent timing side-channels.
---
## 3. Cryptographic Primitives
| Purpose | Algorithm | Rationale |
|---|---|---|
| Password hashing | Argon2id | OWASP-recommended; memory-hard; resists GPU/ASIC attacks. Parameters: time=3, memory=64MB, threads=4 (meets OWASP 2023 minimum of time=2, memory=64MB). |
| JWT signing | Ed25519 (EdDSA) | Fast, short signatures, no parameter malleability, immune to invalid-curve attacks. RFC 8037. |
| JWT key storage | Raw Ed25519 private key in PEM-encoded PKCS#8 file, chmod 0600. | |
| TOTP | HMAC-SHA1 per RFC 6238 (industry standard). Shared secret stored encrypted with AES-256-GCM using a server-side key. | |
| Credential storage | AES-256-GCM with a server-side master key. | |
| Random values | `crypto/rand` exclusively. Never `math/rand`. | |
### JWT Security Rules (non-negotiable)
- Algorithm in header **must** be `EdDSA`. Any other value (including `none`,
`HS256`, `RS256`, `ES256`) must cause immediate rejection before any
signature verification is attempted.
- The public key used to verify a JWT is taken from the server's keystore,
never from the token itself.
- All standard claims are validated: `exp` (required, enforced), `iat`
(required), `nbf` (optional but enforced if present), `iss` (must match
configured issuer), `jti` (required; checked against revocation list).
- Tokens are opaque to relying-party apps; they validate tokens by calling
the MCIAS `/v1/token/validate` endpoint (or, for trusted apps, by verifying
the Ed25519 signature against the published public key).
---
## 4. Account Model
### Account Types
**Human accounts** — interactive users. Can authenticate via:
- Username + password (Argon2id hash stored in DB)
- Optional TOTP (RFC 6238); if enrolled, required on every login
- Future: FIDO2/WebAuthn, Yubikey (not in scope for v1)
**System accounts** — non-interactive service identities. Have:
- A single active bearer token at a time (rotating the token revokes the old one)
- No password, no TOTP
- An associated Postgres credential record (optional)
### Roles
Roles are simple string labels stored in the `account_roles` table.
Reserved roles:
- `admin` — superuser; can manage all accounts, tokens, and credentials
- Any role named identically to a system account — grants that human account
the ability to issue/revoke tokens and retrieve Postgres credentials for that
system account
Role assignment requires admin privileges.
### Account Lifecycle
```
Human: [created by admin] → active → [password change] → active
→ [TOTP enroll] → active (TOTP required)
→ [suspended] → inactive
→ [deleted] → soft-deleted, tokens revoked
System: [created by admin] → active → [token rotated] → active (old token revoked)
→ [deleted] → soft-deleted, token revoked
```
---
## 5. Token Lifecycle
### Token Types
| Type | Subject | Expiry (default) | Renewable | Revocable |
|---|---|---|---|---|
| Session JWT | human user | 30 days | yes | yes |
| Service token | system account | 365 days | yes (rotate) | yes |
| Admin JWT | human user (admin role) | 8 hours | yes | yes |
### Issuance Flow — Human Login
```
Client mciassrv
│ │
├─ POST /v1/auth/login ───▶│
│ {username, password, │
│ totp_code (opt)} │
│ ├─ 1. Load account record; verify status=active
│ ├─ 2. Argon2id verify(password, stored_hash)
│ │ → constant-time; failed → 401, log event
│ ├─ 3. If TOTP enrolled: verify TOTP code
│ │ → constant-time; failed → 401, log event
│ ├─ 4. Generate JWT:
│ │ header: {"alg":"EdDSA","typ":"JWT"}
│ │ claims: {iss, sub (user UUID), iat, exp,
│ │ jti (UUID), roles:[...]}
│ ├─ 5. Sign with Ed25519 private key
│ ├─ 6. Store jti + exp in token_revocation table
│ ├─ 7. Log audit event (login_ok, user, IP)
│◀─ 200 {token, expires_at}│
```
### Token Validation Flow
```
Client App mciassrv
│ │
├─ POST /v1/token/validate▶│
│ Authorization: Bearer │
│ ├─ 1. Parse JWT; extract alg header
│ │ → if alg != "EdDSA": reject 401
│ ├─ 2. Verify Ed25519 signature
│ ├─ 3. Validate claims: exp, iat, iss, jti
│ ├─ 4. Check jti against revocation table
│ │ → if revoked: reject 401
│ ├─ 5. Return {valid: true, sub, roles, exp}
│◀─ 200 {valid, sub, roles}│
```
### Token Renewal
A valid, non-expired, non-revoked token may be exchanged for a new token with
a fresh expiry window. The old token's `jti` is added to the revocation table
(marked revoked) upon successful renewal.
### Token Revocation
Revoked tokens are stored in the `token_revocation` table with their `jti`
and original `exp`. A background task (or on-demand sweep) removes rows whose
`exp` is in the past, since expired tokens are inherently invalid.
Admin users can revoke any token. Users with the role matching a system account
can revoke that system account's token. Human users can revoke their own tokens
(logout).
---
## 6. Session Management
MCIAS is stateless at the HTTP level — there are no server-side sessions.
"Session state" is encoded in the JWT itself (roles, user ID, expiry). The
revocation table provides the statefulness needed for logout and forced
invalidation.
Key properties:
- Concurrent logins are permitted (multiple live JTIs per user)
- Logout revokes only the presented token (single-device logout)
- Admin can revoke all tokens for a user (e.g., on account suspension)
- Token expiry is enforced at validation time, regardless of revocation table
---
## 7. Multi-App Trust Boundaries
Each personal application that relies on MCIAS for authentication is a
**relying party**. Trust boundaries:
1. **MCIAS is the sole issuer.** Apps must not issue their own identity tokens.
2. **Apps validate tokens via MCIAS.** Either by calling `/v1/token/validate`
(recommended; gets revocation checking) or by verifying the Ed25519
signature against the published public key (skips revocation check).
3. **Role-based access.** Apps use the `roles` claim in the validated JWT to
make authorization decisions. MCIAS does not know about app-specific
permissions; it only knows about global roles.
4. **Audience scoping (future).** In v1 tokens are not audience-scoped. A
future `aud` claim may restrict tokens to specific apps.
5. **Service accounts per app.** Each personal app should have a corresponding
system account. The app may authenticate to MCIAS using its service token
to call protected management endpoints.
---
## 8. API Design
Base path: `/v1`
All endpoints use JSON request/response bodies. All responses include a
`Content-Type: application/json` header. Errors follow a uniform structure:
```json
{"error": "human-readable message", "code": "machine_readable_code"}
```
### Authentication Endpoints
| Method | Path | Auth required | Description |
|---|---|---|---|
| POST | `/v1/auth/login` | none | Username/password (+TOTP) login → JWT |
| POST | `/v1/auth/logout` | bearer JWT | Revoke current token |
| POST | `/v1/auth/renew` | bearer JWT | Exchange token for new token |
### Token Endpoints
| Method | Path | Auth required | Description |
|---|---|---|---|
| POST | `/v1/token/validate` | none | Validate a JWT (passed as Bearer header) |
| POST | `/v1/token/issue` | admin JWT or role-scoped JWT | Issue service account token |
| DELETE | `/v1/token/{jti}` | admin JWT or role-scoped JWT | Revoke token by JTI |
### Account Endpoints (admin only)
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/v1/accounts` | admin JWT | List all accounts |
| POST | `/v1/accounts` | admin JWT | Create human or system account |
| GET | `/v1/accounts/{id}` | admin JWT | Get account details |
| PATCH | `/v1/accounts/{id}` | admin JWT | Update account (status, roles, etc.) |
| DELETE | `/v1/accounts/{id}` | admin JWT | Soft-delete account |
### Role Endpoints (admin only)
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/v1/accounts/{id}/roles` | admin JWT | List roles for account |
| PUT | `/v1/accounts/{id}/roles` | admin JWT | Replace role set |
### TOTP Endpoints
| Method | Path | Auth required | Description |
|---|---|---|---|
| POST | `/v1/auth/totp/enroll` | bearer JWT | Begin TOTP enrollment (returns secret + QR URI) |
| POST | `/v1/auth/totp/confirm` | bearer JWT | Confirm TOTP enrollment with code |
| DELETE | `/v1/auth/totp` | admin JWT | Remove TOTP from account (admin) |
### Postgres Credential Endpoints
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/v1/accounts/{id}/pgcreds` | admin JWT or role-scoped JWT | Retrieve Postgres credentials |
| PUT | `/v1/accounts/{id}/pgcreds` | admin JWT | Set/update Postgres credentials |
### Admin / Server Endpoints
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/v1/health` | none | Health check |
| GET | `/v1/keys/public` | none | Ed25519 public key (JWK format) |
---
## 9. Database Schema
Database: SQLite 3, WAL mode enabled, `PRAGMA foreign_keys = ON`.
All tables use `INTEGER PRIMARY KEY` surrogate keys (SQLite rowid alias).
UUIDs used for external identifiers (stored as TEXT).
```sql
-- Server-side secrets (one row always)
CREATE TABLE server_config (
id INTEGER PRIMARY KEY CHECK (id = 1),
-- Ed25519 private key, PEM PKCS#8, encrypted at rest with AES-256-GCM
-- using a master key derived from the startup passphrase.
signing_key_enc BLOB NOT NULL,
signing_key_nonce BLOB NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
-- Human and system accounts
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
uuid TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
account_type TEXT NOT NULL CHECK (account_type IN ('human','system')),
-- NULL for system accounts; PHC-format Argon2id string for human accounts
password_hash TEXT,
status TEXT NOT NULL DEFAULT 'active'
CHECK (status IN ('active','inactive','deleted')),
-- 1 if TOTP is enrolled and required; human accounts only
totp_required INTEGER NOT NULL DEFAULT 0 CHECK (totp_required IN (0,1)),
-- AES-256-GCM encrypted TOTP secret; NULL if not enrolled
totp_secret_enc BLOB,
totp_secret_nonce BLOB,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
deleted_at TEXT
);
CREATE INDEX idx_accounts_username ON accounts (username);
CREATE INDEX idx_accounts_uuid ON accounts (uuid);
CREATE INDEX idx_accounts_status ON accounts (status);
-- Role assignments
CREATE TABLE account_roles (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
role TEXT NOT NULL,
granted_by INTEGER REFERENCES accounts(id),
granted_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
UNIQUE (account_id, role)
);
CREATE INDEX idx_account_roles_account ON account_roles (account_id);
-- Token tracking table. Tracks all issued tokens by JTI for revocation.
-- Rows where both revoked_at IS NULL and expires_at is in the future represent
-- currently-valid tokens. Rows are pruned when expires_at < now.
-- The token value itself is NEVER stored here.
CREATE TABLE token_revocation (
id INTEGER PRIMARY KEY,
jti TEXT NOT NULL UNIQUE,
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
expires_at TEXT NOT NULL,
revoked_at TEXT,
revoke_reason TEXT,
issued_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
CREATE INDEX idx_token_jti ON token_revocation (jti);
CREATE INDEX idx_token_account ON token_revocation (account_id);
CREATE INDEX idx_token_expires ON token_revocation (expires_at);
-- Current active service token for each system account (one per account).
-- When rotated, the old JTI is marked revoked in token_revocation.
CREATE TABLE system_tokens (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL UNIQUE REFERENCES accounts(id) ON DELETE CASCADE,
jti TEXT NOT NULL UNIQUE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
-- Postgres credentials for system accounts, encrypted at rest.
CREATE TABLE pg_credentials (
id INTEGER PRIMARY KEY,
account_id INTEGER NOT NULL UNIQUE REFERENCES accounts(id) ON DELETE CASCADE,
pg_host TEXT NOT NULL,
pg_port INTEGER NOT NULL DEFAULT 5432,
pg_database TEXT NOT NULL,
pg_username TEXT NOT NULL,
pg_password_enc BLOB NOT NULL,
pg_password_nonce BLOB NOT NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
);
-- Audit log — append-only. Never contains credentials or secret material.
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY,
event_time TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now')),
event_type TEXT NOT NULL,
actor_id INTEGER REFERENCES accounts(id),
target_id INTEGER REFERENCES accounts(id),
ip_address TEXT,
details TEXT -- JSON blob; never contains secrets
);
CREATE INDEX idx_audit_time ON audit_log (event_time);
CREATE INDEX idx_audit_actor ON audit_log (actor_id);
CREATE INDEX idx_audit_event ON audit_log (event_type);
```
### Schema Notes
- Passwords are stored as PHC-format Argon2id strings (e.g.,
`$argon2id$v=19$m=65536,t=3,p=4$<salt>$<hash>`), embedding algorithm
parameters. Future parameter upgrades are transparent.
- TOTP secrets and Postgres passwords are encrypted with AES-256-GCM using a
master key held only in server memory (derived at startup from a passphrase
or keyfile). The nonce is stored adjacent to the ciphertext.
- The signing key encryption is layered: the Ed25519 private key is wrapped
with AES-256-GCM using the startup master key. Operators must supply the
passphrase/keyfile on each server restart.
- The audit log is append-only and must never be pruned without explicit
operator action.
---
## 10. TLS Configuration
mciassrv requires TLS. Configuration:
- Minimum version: TLS 1.2 (TLS 1.3 preferred)
- Certificate: operator-supplied PEM (path in config file)
- Cipher suites (TLS 1.2 only): ECDHE+AESGCM, ECDHE+CHACHA20
- Development/testing: self-signed cert acceptable; production must use a
CA-signed cert (Let's Encrypt recommended)
---
## 11. Configuration
The server is configured via a TOML config file. Sensitive values (master key
passphrase) may be supplied via environment variable (`MCIAS_MASTER_PASSPHRASE`)
or a keyfile path — never inline in the config file.
```toml
[server]
listen_addr = "0.0.0.0:8443"
tls_cert = "/etc/mcias/server.crt"
tls_key = "/etc/mcias/server.key"
[database]
path = "/var/lib/mcias/mcias.db"
[tokens]
issuer = "https://auth.example.com"
default_expiry = "720h" # 30 days
admin_expiry = "8h"
service_expiry = "8760h" # 365 days
[argon2]
time = 3
memory = 65536 # KiB (64 MB)
threads = 4
[master_key]
# Exactly one of: passphrase_env or keyfile
passphrase_env = "MCIAS_MASTER_PASSPHRASE"
```
---
## 12. Directory / Package Structure
```
mcias/
├── cmd/
│ ├── mciassrv/ # server binary entrypoint
│ │ └── main.go
│ └── mciasctl/ # admin CLI entrypoint
│ └── main.go
├── internal/
│ ├── auth/ # login flow, TOTP verification, account lockout
│ ├── config/ # config file parsing and validation
│ ├── crypto/ # key management, AES-GCM helpers, master key derivation
│ ├── db/ # SQLite access layer (schema, migrations, queries)
│ ├── middleware/ # HTTP middleware (auth extraction, logging, rate-limit)
│ ├── model/ # shared data types (Account, Token, Role, etc.)
│ ├── server/ # HTTP handlers, router setup
│ └── token/ # JWT issuance, validation, revocation
└── go.mod
```
All implementation packages are under `internal/` to prevent external import.
The `cmd/` packages are thin wrappers that wire dependencies and call into
`internal/`.
---
## 13. Error Handling and Logging
- All errors are wrapped with `fmt.Errorf("context: %w", err)`.
- Structured logging uses `log/slog` (or goutils wrapper).
- Log levels: DEBUG (dev only), INFO (normal ops), WARN (recoverable), ERROR
(unexpected failures).
- Authentication events (success and failure) are always logged at INFO with:
`{event, username (not password), ip, user_agent, timestamp, result}`.
- **Never log:** passwords, raw tokens, TOTP codes, master key material,
Postgres credentials.
---
## 14. Audit Events
| Event type | Trigger |
|---|---|
| `login_ok` | Successful login |
| `login_fail` | Failed login (wrong password, unknown user) |
| `login_totp_fail` | Correct password, wrong TOTP code |
| `token_issued` | JWT issued (login or service token) |
| `token_renewed` | Token exchanged for a fresh one |
| `token_revoked` | Token explicitly revoked |
| `token_expired` | Attempt to use an expired token (at validation time) |
| `account_created` | New account created |
| `account_updated` | Account modified (status, roles) |
| `account_deleted` | Account soft-deleted |
| `role_granted` | Role assigned to account |
| `role_revoked` | Role removed from account |
| `totp_enrolled` | TOTP enrollment completed |
| `totp_removed` | TOTP removed from account |
| `pgcred_accessed` | Postgres credentials retrieved |
| `pgcred_updated` | Postgres credentials stored/updated |
---
## 15. Operational Considerations
- **Backups:** Use SQLite's online backup API or filesystem snapshot with WAL
checkpointing. The master key/passphrase must be backed up separately and
securely.
- **Key rotation:** Rotating the Ed25519 signing key requires re-issuing tokens
for all users (old tokens become unverifiable). A dual-key grace period is
not in v1 scope.
- **Rate limiting:** Login endpoints are rate-limited by IP (token bucket: 10
attempts/minute). Implemented in middleware. In v1, an in-memory rate limiter
is acceptable (single-instance deployment).
- **Master key loss:** Loss of the master key means all encrypted secrets
(TOTP, Postgres passwords, signing key) are unrecoverable. Operators must
back up the passphrase/keyfile securely.
---
## 16. mciasdb — Database Maintenance Tool
### Rationale
`mciasctl` is an API client: it requires a running mciassrv, a valid admin
JWT, and network access. This is appropriate for normal administration but
rules it out for several important scenarios:
- The server is down and accounts need to be inspected or repaired.
- Bootstrap: creating the first admin account before any JWT can exist.
- Offline forensics: reading the audit log without starting the server.
- Maintenance: pruning expired token rows, verifying schema integrity.
- Recovery: resetting a locked-out admin password when no other admin exists.
Adding direct DB access to `mciasctl` would blur the API-client / DB-operator
trust boundary and create pressure to use the bypass path for routine tasks.
A separate binary (`mciasdb`) makes the distinction explicit: it is a
break-glass tool that requires local filesystem access to the SQLite file and
the master key, and should only be used when the API is unavailable or
insufficient.
### Trust Model
`mciasdb` is a privileged, local-only tool. It assumes:
- The operator has filesystem access to the SQLite database file.
- The operator has the master key (passphrase env var or keyfile), same as
mciassrv.
- No network connection is required or used.
- Audit events written by mciasdb are tagged with actor `mciasdb` (no UUID)
so they are distinguishable from API-driven events in the audit log.
### Configuration
`mciasdb` accepts a subset of the mciassrv config file (the `[database]` and
`[master_key]` sections) via `--config` flag, identical in format to
mciassrv's config. This avoids a separate config format and ensures key
derivation is identical.
### Command Surface
```
mciasdb --config PATH <subcommand> [flags]
```
**Schema / maintenance:**
| Command | Description |
|---|---|
| `mciasdb schema verify` | Open DB, run migrations in dry-run mode, report version |
| `mciasdb schema migrate` | Apply any pending migrations and exit |
| `mciasdb prune tokens` | Delete expired rows from `token_revocation` and `system_tokens` |
**Account management (offline):**
| Command | Description |
|---|---|
| `mciasdb account list` | Print all accounts (uuid, username, type, status) |
| `mciasdb account get --id UUID` | Print single account record |
| `mciasdb account create --username NAME --type human\|system` | Insert account row directly |
| `mciasdb account set-password --id UUID` | Prompt for new password, re-hash with Argon2id, update row |
| `mciasdb account set-status --id UUID --status active\|inactive\|deleted` | Update account status |
| `mciasdb account reset-totp --id UUID` | Clear TOTP fields (totp_required=0, totp_secret_enc=NULL) |
**Role management (offline):**
| Command | Description |
|---|---|
| `mciasdb role list --id UUID` | List roles for account |
| `mciasdb role grant --id UUID --role ROLE` | Insert role row |
| `mciasdb role revoke --id UUID --role ROLE` | Delete role row |
**Token management (offline):**
| Command | Description |
|---|---|
| `mciasdb token list --id UUID` | List token_revocation rows for account |
| `mciasdb token revoke --jti JTI` | Mark JTI as revoked in token_revocation |
| `mciasdb token revoke-all --id UUID` | Revoke all active tokens for account |
**Audit log:**
| Command | Description |
|---|---|
| `mciasdb audit tail [--n N]` | Print last N audit events (default 50) |
| `mciasdb audit query --account UUID` | Print audit events for account |
| `mciasdb audit query --type EVENT_TYPE` | Print audit events of given type |
| `mciasdb audit query --since TIMESTAMP` | Print audit events since RFC-3339 time |
**Postgres credentials (offline):**
| Command | Description |
|---|---|
| `mciasdb pgcreds get --id UUID` | Decrypt and print Postgres credentials |
| `mciasdb pgcreds set --id UUID ...` | Encrypt and store Postgres credentials |
### Security Constraints
- `mciasdb account set-password` must prompt interactively (no `--password`
flag) so the password is never present in shell history or process listings.
- Decrypted secrets (TOTP secrets, Postgres passwords) are printed only when
explicitly requested and include a warning that output should not be logged.
- All writes produce an audit log entry tagged with actor `mciasdb`.
- `mciasdb` must not start mciassrv or bind any network port.
- mciasdb must refuse to open the DB if mciassrv holds an exclusive WAL lock;
SQLite busy-timeout handles this gracefully (5s then error).
### Output Format
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.