Add HTMX-based UI templates and handlers for account and audit management

- Introduced `web/templates/` for HTMX-fragmented pages (`dashboard`, `accounts`, `account_detail`, `error_fragment`, etc.).
- Implemented UI routes for account CRUD, audit log display, and login/logout with CSRF protection.
- Added `internal/ui/` package for handlers, CSRF manager, session validation, and token issuance.
- Updated documentation to include new UI features and templates directory structure.
- Security: Double-submit CSRF cookies, constant-time HMAC validation, login password/Argon2id re-verification at all steps to prevent bypass.
This commit is contained in:
2026-03-11 18:02:53 -07:00
parent 0c441f5c4f
commit a80242ae3e
21 changed files with 1425 additions and 20 deletions

View File

@@ -88,7 +88,7 @@ mciassrv (passphrase or keyfile) to decrypt secrets at rest.
| 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). |
| 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). Master key derivation uses time=3, memory=128MB, threads=4 (higher cost acceptable at startup). |
| 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. | |
@@ -278,8 +278,8 @@ All endpoints use JSON request/response bodies. All responses include a
| 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 |
| POST | `/v1/token/issue` | admin JWT | Issue service account token |
| DELETE | `/v1/token/{jti}` | admin JWT | Revoke token by JTI |
### Account Endpoints (admin only)
@@ -310,9 +310,15 @@ All endpoints use JSON request/response bodies. All responses include a
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/v1/accounts/{id}/pgcreds` | admin JWT or role-scoped JWT | Retrieve Postgres credentials |
| GET | `/v1/accounts/{id}/pgcreds` | admin JWT | Retrieve Postgres credentials |
| PUT | `/v1/accounts/{id}/pgcreds` | admin JWT | Set/update Postgres credentials |
### Audit Endpoints (admin only)
| Method | Path | Auth required | Description |
|---|---|---|---|
| GET | `/v1/audit` | admin JWT | List audit log events |
### Admin / Server Endpoints
| Method | Path | Auth required | Description |
@@ -335,8 +341,11 @@ 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,
signing_key_enc BLOB,
signing_key_nonce BLOB,
-- Argon2id salt for master key derivation; stable across restarts so the
-- passphrase always yields the same key. Generated on first run.
master_key_salt 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'))
);
@@ -444,6 +453,8 @@ CREATE INDEX idx_audit_event ON audit_log (event_type);
- 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 master key salt is stored in `server_config.master_key_salt` so the
Argon2id KDF produces the same key on every restart. Generated on first run.
- 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.
@@ -472,6 +483,7 @@ or a keyfile path — never inline in the config file.
```toml
[server]
listen_addr = "0.0.0.0:8443"
grpc_addr = "0.0.0.0:9443" # optional; omit to disable gRPC
tls_cert = "/etc/mcias/server.crt"
tls_key = "/etc/mcias/server.key"
@@ -518,7 +530,11 @@ mcias/
│ ├── 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
── token/ # JWT issuance, validation, revocation
│ └── ui/ # web UI context, CSRF, session, template handlers
├── web/
│ ├── static/ # CSS and static assets
│ └── templates/ # HTML templates (base layout, pages, HTMX fragments)
├── proto/
│ └── mcias/v1/ # Protobuf service definitions (Phase 7)
├── gen/
@@ -798,7 +814,7 @@ mciassrv starts both listeners in the same process:
│ ┌────────────────┐ ┌────────────────────┐ │
│ │ REST listener │ │ gRPC listener │ │
│ │ (net/http) │ │ (google.golang. │ │
│ │ :8443 │ │ org/grpc) :8444 │ │
│ │ :8443 │ │ org/grpc) :9443 │ │
│ └───────┬─────────┘ └──────────┬─────────┘ │
│ └──────────────┬─────────┘ │
│ ▼ │
@@ -818,7 +834,7 @@ configured window.
```toml
[server]
listen_addr = "0.0.0.0:8443"
grpc_addr = "0.0.0.0:8444" # optional; omit to disable gRPC
grpc_addr = "0.0.0.0:9443" # optional; omit to disable gRPC
tls_cert = "/etc/mcias/server.crt"
tls_key = "/etc/mcias/server.key"
```
@@ -916,7 +932,7 @@ FROM debian:bookworm-slim
Security properties of the runtime image:
- No shell, no package manager, no Go toolchain — minimal attack surface
- No Go toolchain, no build cache, no source code — minimal attack surface
- Non-root user (`mcias`, uid 10001) — no escalation path
- TLS termination happens inside the container (same cert/key as bare-metal
deployment); the operator mounts `/etc/mcias/` as a read-only volume
@@ -953,7 +969,7 @@ The Makefile `docker` target automates the build step with the version tag.
| `man` | Build man pages; compress to `.gz` in `man/` |
| `install` | Run `dist/install.sh` |
| `docker` | `docker build -t mcias:$(VERSION) .` |
| `clean` | Remove `bin/`, `gen/`, compressed man pages |
| `clean` | Remove `bin/` and compressed man pages |
| `dist` | Cross-compile release tarballs for linux/amd64 and linux/arm64 |
### Upgrade Path