- 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>
- 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 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>
- 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>
* web/templates/pgcreds.html: New Credentials card is now always
rendered; Add Credentials toggle button reveals the create form
(hidden by default). Shows a message when all system accounts
already have credentials. Previously the card was hidden when
UncredentialedAccounts was empty.
* internal/ui/ui.go: added ActorName string field to PageData;
added actorName(r) helper resolving username from JWT claims
via DB lookup, returns empty string if unauthenticated.
* internal/ui/handlers_*.go: all full-page PageData constructors
now pass ActorName: u.actorName(r).
* web/templates/base.html: nav bar renders actor username as a
muted label before the Logout button when logged in.
* web/static/style.css: added .nav-actor rule (muted grey, 0.85rem).
- Corrected lockout logic (`IsLockedOut`) to properly evaluate failed login thresholds within the rolling window, ensuring stale attempts outside the window do not trigger lockout.
- Updated test passwords in `grpcserver_test.go` to comply with 12-character minimum requirement.
- Reformatted import blocks with `goimports` to address lint warnings.
- Verified all tests pass and linter is clean.
- Added failed login tracking for account lockout enforcement in `db` and `ui` layers; introduced `failed_logins` table to store attempts, window start, and attempt count.
- Updated login checks in `grpcserver/auth.go` and `ui/handlers_auth.go` to reject requests if the account is locked.
- Added immediate failure counter reset on successful login.
- Implemented username length and character set validation (F-12) and minimum password length enforcement (F-13) in shared `validate` package.
- Updated account creation and edit flows in `ui` and `grpcserver` layers to apply validation before hashing/processing.
- Added comprehensive unit tests for lockout, validation, and related edge cases.
- Updated `AUDIT.md` to mark F-08, F-12, and F-13 as fixed.
- Updated `openapi.yaml` to reflect new validation and lockout behaviors.
Security: Prevents brute-force attacks via lockout mechanism and strengthens defenses against weak and invalid input.
- ui/ui.go: add pendingLogin struct and pendingLogins sync.Map
to UIServer; add issueTOTPNonce (generates 128-bit random nonce,
stores accountID with 90s TTL) and consumeTOTPNonce (single-use,
expiry-checked LoadAndDelete); add dummyHash() method
- ui/handlers_auth.go: split handleLoginPost into step 1
(password verify → issue nonce) and step 2 (handleTOTPStep,
consume nonce → validate TOTP) via a new finishLogin helper;
password never transmitted or stored after step 1
- ui/ui_test.go: refactor newTestMux to reuse new
newTestUIServer; add TestTOTPNonceIssuedAndConsumed,
TestTOTPNonceUnknownRejected, TestTOTPNonceExpired, and
TestLoginPostPasswordNotInTOTPForm; 11/11 tests pass
- web/templates/fragments/totp_step.html: replace
'name=password' hidden field with 'name=totp_nonce'
- db/accounts.go: add GetAccountByID for TOTP step lookup
- AUDIT.md: mark F-02 as fixed
Security: the plaintext password previously survived two HTTP
round-trips and lived in the browser DOM during the TOTP step.
The nonce approach means the password is verified once and
immediately discarded; only an opaque random token tied to an
account ID (never a credential) crosses the wire on step 2.
Nonces are single-use and expire after 90 seconds to limit
the window if one is captured.
- db/accounts.go: add RenewToken(oldJTI, reason, newJTI,
accountID, issuedAt, expiresAt) which wraps RevokeToken +
TrackToken in a single BEGIN/COMMIT transaction; if either
step fails the whole tx rolls back, so the user is never
left with neither old nor new token valid
- server.go (handleRenewToken): replace separate RevokeToken +
TrackToken calls with single RenewToken call; failure now
returns 500 instead of silently losing revocation
- grpcserver/auth.go (RenewToken): same replacement
- db/db_test.go: TestRenewTokenAtomic verifies old token is
revoked with correct reason, new token is tracked and not
revoked, and a second renewal on the already-revoked old
token returns an error
- AUDIT.md: mark F-03 as fixed
Security: without atomicity a crash/error between revoke and
track could leave the old token active alongside the new one
(two live tokens) or revoke the old token without tracking
the new one (user locked out). The transaction ensures
exactly one of the two tokens is valid at all times.
- 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)
The package-level defaultRateLimiter drained its token bucket
across all test cases, causing later tests to hit ResourceExhausted.
Move rateLimiter from a package-level var to a *grpcRateLimiter field
on Server; New() allocates a fresh instance (10 req/s, burst 10) per
server. Each test's newTestEnv() constructs its own Server, so tests
no longer share limiter state.
Production behaviour is unchanged: a single Server is constructed at
startup and lives for the process lifetime.
- Added `web/templates/{dashboard,audit,base,accounts,account_detail}.html` for a consistent UI.
- Implemented new audit log endpoint (`GET /v1/audit`) with filtering and pagination via `ListAuditEventsPaged`.
- Extended `AuditQueryParams`, added `AuditEventView` for joined actor/target usernames.
- Updated configuration (`goimports` preference), linting rules, and E2E tests.
- No logic changes to existing APIs.
* 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.
- Add test/e2e: 11 end-to-end tests covering full login/logout,
token renewal, admin account management, credential-never-in-response,
unauthorised access, JWT alg confusion and alg:none attacks,
revoked token rejection, system account token issuance,
wrong-password vs unknown-user indistinguishability
- Apply gofmt to all source files (formatting only, no logic changes)
- Update .golangci.yaml for golangci-lint v2 (version field required,
gosimple merged into staticcheck, formatters section separated)
- Update PROGRESS.md to reflect Phase 5 completion
Security:
All 97 tests pass with go test -race ./... (zero race conditions).
Adversarial JWT tests (alg confusion, alg:none) confirm the
ValidateToken alg-first check is effective against both attack classes.
Credential fields (PasswordHash, TOTPSecret*, PGPassword) confirmed
absent from all API responses via both unit and e2e tests.
go vet ./... clean. golangci-lint v2.6.2 incompatible with go1.26
runtime; go vet used as linter until toolchain is updated.