- 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>
- 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>
- Trusted proxy config option for proxy-aware IP extraction
used by rate limiting and audit logs; validates proxy IP
before trusting X-Forwarded-For / X-Real-IP headers
- TOTP replay protection via counter-based validation to
reject reused codes within the same time step (±30s)
- RateLimit middleware updated to extract client IP from
proxy headers without IP spoofing risk
- New tests for ClientIP proxy logic (spoofed headers,
fallback) and extended rate-limit proxy coverage
- HTMX error banner script integrated into web UI base
- .gitignore updated for mciasdb build artifact
Security: resolves CRIT-01 (TOTP replay attack) and
DEF-03 (proxy-unaware rate limiting); gRPC TOTP
enrollment aligned with REST via StorePendingTOTP
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- db/accounts.go: add StorePendingTOTP() which writes
totp_secret_enc and totp_secret_nonce but leaves
totp_required=0; add comment explaining two-phase flow
- server.go (handleTOTPEnroll): switch from SetTOTP() to
StorePendingTOTP() so the required flag is only set after
the user confirms a valid TOTP code via handleTOTPConfirm,
which still calls SetTOTP()
- server_test.go: TestTOTPEnrollDoesNotRequireTOTP verifies
that after POST /v1/auth/totp/enroll, TOTPRequired is false
and the encrypted secret is present; confirms that a
subsequent login without a TOTP code still succeeds (no
lockout)
- AUDIT.md: mark F-01 and F-11 as fixed
Security: without this fix an admin who enrolls TOTP but
abandons before confirmation is permanently locked out
because totp_required=1 but no confirmed secret exists.
StorePendingTOTP() keeps the secret pending until the user
proves possession by confirming a valid code.
- AUDIT.md: security audit report with 16 findings (F-01..F-16)
- F-04 (server.go): wire loginRateLimit (10 req/s, burst 10) to
POST /v1/auth/login and POST /v1/token/validate; no limit on
/v1/health or public-key endpoints
- F-04 (server_test.go): TestLoginRateLimited uses concurrent
goroutines (sync.WaitGroup) to fire burst+1 requests before
Argon2id completes, sidestepping token-bucket refill timing;
TestTokenValidateRateLimited; TestHealthNotRateLimited
- F-11 (ui.go): refactor Register() so all UI routes are mounted
on a child mux wrapped with securityHeaders middleware; five
headers set on every response: Content-Security-Policy,
X-Content-Type-Options, X-Frame-Options, HSTS, Referrer-Policy
- F-11 (ui_test.go): 7 new tests covering login page, dashboard
redirect, root redirect, static assets, CSP directives,
HSTS min-age, and middleware unit behaviour
Security: rate limiter on login prevents brute-force credential
stuffing; security headers mitigate clickjacking (X-Frame-Options
DENY), MIME sniffing (nosniff), and protocol downgrade (HSTS)