* 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.
692 lines
28 KiB
Markdown
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.
|