- Replace stale "service" role option with correct set:
admin, user, guest, viewer, editor, commenter (matches model.go)
- Add Form/JSON tab toggle to policy create form
- JSON tab accepts raw RuleBody JSON with description/priority
- Handler detects rule_json field and parses/validates it
directly, falling back to field-by-field form mode otherwise
- Download swagger-ui-dist@5.32.0 and embed
swagger-ui-bundle.js and swagger-ui.css into
web/static/ so they are served from the same origin
- Update docs.html to reference /static/ paths instead
of unpkg.com CDN URLs
- Add GET /static/swagger-ui-bundle.js and
GET /static/swagger-ui.css handlers serving the
embedded bytes with correct Content-Type headers
- Fixes /docs breakage caused by CSP default-src 'self'
blocking external CDN scripts and stylesheets
Co-authored-by: Junie <junie@jetbrains.com>
- errorlint: use errors.Is for ErrSealed comparisons in vault_test.go
- gofmt: reformat config, config_test, middleware_test with goimports
- govet/fieldalignment: reorder struct fields in vault.go, csrf.go,
detail_test.go, middleware_test.go for optimal alignment
- unused: remove unused newCSRFManager in csrf.go (superseded by
newCSRFManagerFromVault)
- revive/early-return: invert sealed-vault condition in main.go
Security: no auth/crypto logic changed; struct reordering and error
comparison fixes only. newCSRFManager removal is safe — it was never
called; all CSRF construction goes through newCSRFManagerFromVault.
Co-authored-by: Junie <junie@jetbrains.com>
- Replace type: [string, "null"] array syntax with
type: string + nullable: true on AuditEvent.actor_id,
AuditEvent.target_id, PolicyRule.not_before, and
PolicyRule.expires_at; Swagger UI 5 cannot parse the
JSON Schema array form
- Add missing username field to /v1/token/validate response
schema (added to handler in d6cc827 but never synced)
- Add missing GET /v1/pgcreds endpoint to spec
- Sync web/static/openapi.yaml (served file) with root;
the static copy was many commits out of date, missing
all policy/tags schemas and endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace requireAdmin (role-based) guards on all REST endpoints
with RequirePolicy middleware backed by the existing policy engine;
built-in admin wildcard rule (-1) preserves existing admin behaviour
while operator rules can now grant targeted access to non-admin
accounts (e.g. a system account allowed to list accounts)
- Wire policy engine into Server: loaded from DB at startup,
reloaded after every policy-rule create/update/delete so changes
take effect immediately without a server restart
- Add service_account_delegates table (migration 000008) so a human
account can be delegated permission to issue tokens for a specific
system account without holding the admin role
- Add token-download nonce mechanism: a short-lived (5 min),
single-use random nonce is stored server-side after token issuance;
the browser downloads the token as a file via
GET /token/download/{nonce} (Content-Disposition: attachment)
instead of copying from a flash message
- Add /service-accounts UI page for non-admin delegates
- Add TestPolicyEnforcement and TestPolicyDenyRule integration tests
Security:
- Policy engine uses deny-wins, default-deny semantics; admin wildcard
is a compiled-in built-in and cannot be deleted via the API
- Token download nonces are 128-bit crypto/rand values, single-use,
and expire after 5 minutes; a background goroutine evicts stale entries
- alg header validation and Ed25519 signing unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Include username field in validateResponse struct
- Look up account by UUID and populate username on success
- Add username field to Go client TokenClaims struct
- Fix OpenAPI nullable type syntax (use array form)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- internal/db/accounts.go: add ListAccountsWithTOTP,
ListAllPGCredentials, TOTPRekeyRow, PGRekeyRow, and
Rekey — atomic transaction that replaces master_key_salt,
signing_key_enc/nonce, all TOTP enc/nonce, and all
pg_password enc/nonce in one SQLite BEGIN/COMMIT
- cmd/mciasdb/rekey.go: runRekey — decrypts all secrets
under old master key, prompts for new passphrase (with
confirmation), derives new key from fresh Argon2id salt,
re-encrypts everything, and commits atomically
- cmd/mciasdb/main.go: wire "rekey" command + update usage
- Tests: DB-layer tests for ListAccountsWithTOTP,
ListAllPGCredentials, Rekey (happy path, empty DB, salt
replacement); command-level TestRekeyCommandRoundTrip
verifies full round-trip and adversarially confirms old
key no longer decrypts after rekey
Security: fresh random salt is always generated so a
reused passphrase still produces an independent key; old
and new master keys are zeroed via defer; no passphrase or
key material appears in logs or audit events; the entire
re-encryption is done in-memory before the single atomic
DB write so the database is never in a mixed state.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add id="login-card" to the .card wrapper div
- Change hx-target to #login-card (was #login-form)
- Add hx-select="#login-card" so htmx extracts only
the card element from the full-page response
Without hx-select, htmx replaced the form's outerHTML
with the entire page response, inserting a new .card
inside the existing .card on every failed attempt.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New internal/vault package: thread-safe Vault struct with
seal/unseal state, key material zeroing, and key derivation
- REST: POST /v1/vault/unseal, POST /v1/vault/seal,
GET /v1/vault/status; health returns sealed status
- UI: /unseal page with passphrase form, redirect when sealed
- gRPC: sealedInterceptor rejects RPCs when sealed
- Middleware: RequireUnsealed blocks all routes except exempt
paths; RequireAuth reads pubkey from vault at request time
- Startup: server starts sealed when passphrase unavailable
- All servers share single *vault.Vault by pointer
- CSRF manager derives key lazily from vault
Security: Key material is zeroed on seal. Sealed middleware
runs before auth. Handlers fail closed if vault becomes sealed
mid-request. Unseal endpoint is rate-limited (3/s burst 5).
No CSRF on unseal page (no session to protect; chicken-and-egg
with master key). Passphrase never logged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PEN-01: fix extractBearerFromRequest to validate Bearer prefix
using strings.SplitN + EqualFold; add TestExtractBearerFromRequest
- PEN-02: security headers confirmed present after redeploy (live
probe 2026-03-15)
- PEN-03: accepted — Swagger UI self-hosting disproportionate to risk
- PEN-04: accepted — OpenAPI spec intentionally public
- PEN-05: accepted — gRPC port 9443 intentionally public
- PEN-06: remove RecordLoginFailure from REST TOTP-missing branch
to match gRPC handler (DEF-08); add
TestTOTPMissingDoesNotIncrementLockout
- PEN-07: accepted — per-account hard lockout covers the same threat
- Update AUDIT.md: all 7 PEN findings resolved (4 fixed, 3 accepted)
Security: PEN-01 removed a defence-in-depth gap where any 8+ char
Authorization value was accepted as a Bearer token. PEN-06 closed an
account-lockout-via-omission attack vector on TOTP-enrolled accounts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mark SEC-01 through SEC-12 as fixed with fix descriptions
- Update executive summary to reflect full remediation
- Move original finding descriptions to collapsible section
- Replace remediation priority table with status section
Security: documentation-only change, no code modifications
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update package declaration in client.go
- Update error message strings to reference new package name
- Update test package and imports to use new name
- Update README.md documentation and examples with new package name
- All tests pass
Increase token lifetime from 2s to 4s in TestRenewToken to prevent
the token from expiring before the gRPC call completes through bufconn.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- REST handleTOTPEnroll now requires password field in request body
- gRPC EnrollTOTP updated with password field in proto message
- Both handlers check lockout status and record failures on bad password
- Updated Go, Python, and Rust client libraries to pass password
- Updated OpenAPI specs with new requestBody schema
- Added TestTOTPEnrollRequiresPassword with no-password, wrong-password,
and correct-password sub-tests
Security: TOTP enrollment now requires the current password to prevent
session-theft escalation to persistent account takeover. Lockout and
failure recording use the same Argon2id constant-time path as login.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add internal/audit package with JSON() and JSONWithRoles() helpers
that use json.Marshal instead of fmt.Sprintf with %q
- Replace all fmt.Sprintf audit detail construction in:
- internal/server/server.go (10 occurrences)
- internal/ui/handlers_auth.go (4 occurrences)
- internal/grpcserver/auth.go (4 occurrences)
- Add tests for the helpers including edge-case Unicode,
null bytes, special characters, and odd argument counts
- Fix broken {"roles":%v} formatting that produced invalid JSON
Security: Audit log detail strings are now constructed via
json.Marshal, which correctly handles all Unicode edge cases
(U+2028, U+2029, null bytes, etc.) that fmt.Sprintf with %q
may mishandle. This prevents potential log injection or parsing
issues in audit event consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add 50% lifetime elapsed check to REST handleRenew and gRPC RenewToken
- Reject renewal attempts before 50% of token lifetime has elapsed
- Update existing renewal tests to use short-lived tokens with sleep
- Add TestRenewTokenTooEarly tests for both REST and gRPC
Security: Tokens can only be renewed after 50% of their lifetime has
elapsed, preventing indefinite renewal of stolen tokens.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add IsAdmin bool to PageData (embedded in all page view structs)
- Remove redundant IsAdmin from DashboardData
- Add isAdmin() helper to derive admin status from request claims
- Set IsAdmin in all page-level handlers that populate PageData
- Wrap admin-only nav links in base.html with {{if .IsAdmin}}
- Add tests: non-admin dashboard/profile hide admin links,
admin dashboard shows them
Security: navigation links to /accounts, /audit, /policies,
and /pgcreds are now only rendered for admin users. Server-side
authorization (requireAdminRole middleware) was already in place;
this change removes the information leak of showing links that
return 403 to non-admin users.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- REST login: change locked account response from HTTP 429
"account_locked" to HTTP 401 "invalid credentials"
- gRPC login: change from ResourceExhausted to Unauthenticated
with "invalid credentials" message
- UI login: change from "account temporarily locked" to
"invalid credentials"
- REST password-change endpoint: same normalization
- Audit logs still record "account_locked" internally
- Added tests in all three layers verifying locked-account
responses are indistinguishable from wrong-password responses
Security: lockout responses now return identical status codes and
messages as wrong-password failures across REST, gRPC, and UI,
preventing user-enumeration via lockout differentiation. Internal
audit logging of lockout events is preserved for operational use.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change default_expiry from 720h (30 days) to 168h (7 days)
in dist/mcias.conf.example and dist/mcias.conf.docker.example
- Update man page, ARCHITECTURE.md, and config.go comment
- Max ceiling validation remains at 30 days (unchanged)
Security: Shorter default token lifetime reduces the window of
exposure if a token is leaked. 7 days balances convenience and
security for a personal SSO. The 30-day max ceiling is preserved
so operators can still override if needed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add IssueSystemToken() method in internal/db/accounts.go that wraps
revoke-old, track-new, and upsert-system_tokens in a single SQLite
transaction
- Update handleTokenIssue in internal/server/server.go to use the new
atomic method instead of three separate DB calls
- Update IssueServiceToken in internal/grpcserver/tokenservice.go with
the same fix
- Add TestIssueSystemTokenAtomic test covering first issue and rotation
Security: token issuance now uses a single transaction to prevent
inconsistent state (e.g., old token revoked but new token not tracked)
if a crash occurs between operations. Follows the same pattern as
RenewToken which was already correctly transactional.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add grpcClientIP() helper that mirrors middleware.ClientIP
for proxy-aware IP extraction from gRPC metadata
- Update rateLimitInterceptor to use grpcClientIP with the
TrustedProxy config setting
- Only trust x-forwarded-for/x-real-ip metadata when the
peer address matches the configured trusted proxy
- Add 7 unit tests covering: no proxy, xff, x-real-ip
preference, untrusted peer ignoring headers, no headers
fallback, invalid header fallback, and no peer
Security: gRPC rate limiter now extracts real client IPs
behind a reverse proxy using the same trust model as the
REST middleware (DEF-03). Headers from untrusted peers are
ignored, preventing IP-spoofing for rate-limit bypass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap r.Body with http.MaxBytesReader (1 MiB) in decodeJSON so all
REST API endpoints reject oversized JSON payloads
- Add MaxPasswordLen = 128 constant and enforce it in validate.Password()
to prevent Argon2id DoS via multi-MB passwords
- Add test for oversized JSON body rejection (>1 MiB -> 400)
- Add test for password max length enforcement
Security: decodeJSON now applies the same body size limit the UI layer
already uses, closing the asymmetry. MaxPasswordLen caps Argon2id input
to a reasonable length, preventing CPU-exhaustion attacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add globalSecurityHeaders middleware wrapping root handler
- Sets X-Content-Type-Options, Strict-Transport-Security, Cache-Control
on all responses (API and UI)
- Add tests verifying headers on /v1/health and /v1/auth/login
Security: API responses previously lacked HSTS, nosniff, and
cache-control headers. The new middleware applies these universally.
Headers are safe for all content types and do not conflict with
the UI's existing securityHeaders middleware.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add noDirListing handler wrapper that returns 404 for directory
requests (paths ending with "/" or empty path) instead of delegating
to http.FileServerFS which would render an index page
- Wrap the static file server in Register() with noDirListing
- Add tests verifying GET /static/ returns 404 and GET /static/style.css
still returns 200
Security: directory listings exposed the names of all static assets,
leaking framework details. The wrapper blocks directory index responses
while preserving normal file serving.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add Permissions-Policy header disabling camera, microphone,
geolocation, and payment browser features
- Update assertSecurityHeaders test helper to verify the new header
Security: Permissions-Policy restricts browser APIs that this
application does not use, reducing attack surface from content
injection vulnerabilities. No crypto or auth flow changes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change dashboard route from adminGet to authed middleware
- Show account counts and audit events only for admin users
- Show welcome message for non-admin authenticated users
Security: non-admin users cannot access account lists or audit
events; admin-only data is gated by claims.HasRole("admin") in
the handler, not just at the route level.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add requireAdminRole middleware to web UI that checks
claims.HasRole("admin") and returns 403 if absent
- Apply middleware to all admin routes (accounts, policies,
audit, dashboard, credentials)
- Remove redundant inline admin check from handleAdminResetPassword
- Profile routes correctly require only authentication, not admin
Security: The admin/adminGet middleware wrappers only called
requireCookieAuth (JWT validation) but never verified the admin
role. Any authenticated user could access admin endpoints
including role assignment. Fixed by inserting requireAdminRole
into the middleware chain for all admin routes.
- Update knownRoles to include guest, viewer, editor, and commenter
- Replace hardcoded role strings with model constants
- Remove obsolete 'service' role from UI
- All tests pass
- Add RoleGuest, RoleViewer, RoleEditor, and RoleCommenter constants
- Update allowedRoles map to include new roles
- Update ValidateRole error message with complete role list
- All tests pass; build verified
- Add POST /v1/accounts/{id}/roles and DELETE /v1/accounts/{id}/roles/{role} REST endpoints
- Add GrantRole and RevokeRole RPCs to AccountService in gRPC API
- Update OpenAPI specification with new endpoints
- Add grant and revoke subcommands to mciasctl
- Add grant and revoke subcommands to mciasgrpcctl
- Regenerate proto files with new message types and RPCs
- Implement gRPC server methods for granular role management
- All existing tests pass; build verified with goimports
Security: Role changes are audited via EventRoleGranted and EventRoleRevoked events,
consistent with existing SetRoles implementation.
- Add auth/login and auth/logout to mciasgrpcctl, calling
the existing AuthService.Login/Logout RPCs; password is
always prompted interactively (term.ReadPassword), never
accepted as a flag, raw bytes zeroed after use
- Add proto/mcias/v1/policy.proto with PolicyService
(List, Create, Get, Update, Delete policy rules)
- Regenerate gen/mcias/v1/ stubs to include policy
- Implement internal/grpcserver/policyservice.go delegating
to the same db layer as the REST policy handlers
- Register PolicyService in grpcserver.go
- Add policy list/create/get/update/delete to mciasgrpcctl
- Update mciasgrpcctl man page with new commands
Security: auth login uses the same interactive password
prompt pattern as mciasctl; password never appears in
process args, shell history, or logs; raw bytes zeroed
after string conversion (same as REST CLI and REST server).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add TOTP enrollment/confirmation/removal to all clients
- Add password change and admin set-password endpoints
- Add account listing, status update, and tag management
- Add audit log listing with filter support
- Add policy rule CRUD operations
- Expand test coverage for all new endpoints across clients
- Fix .gitignore to exclude built binaries
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>