# Security Architecture — CapaKraken > Version: 1.0 | Date: 2026-03-27 --- ## 1. Authentication - **Auth.js v5** (NextAuth) with Credentials provider - **Password hashing**: Argon2id via `@node-rs/argon2` (memory cost 65536, time cost 3) - **Multi-Factor Authentication**: TOTP (RFC 6238) via `otpauth` library - Configurable per user (enable/disable via admin or self-service) - 30-second window, SHA-1, 6-digit codes with 1-step tolerance - **Rate limiting**: 5 login attempts per 15 minutes per email address (in-memory sliding window) - **Session strategy**: JWT with server-side validation - Absolute timeout: 8 hours (configurable via `sessionMaxAge`) - Idle timeout: 30 minutes (configurable via `sessionIdleTimeout`) - **Concurrent session limit**: configurable `maxConcurrentSessions` (default 3), kick-oldest strategy - **Login/logout audit**: all authentication events (success, failure, rate-limit, invalid TOTP, logout) are recorded in the audit log ## 2. Authorization ### Role-Based Access Control (RBAC) Five-level role hierarchy: | Role | Level | Capabilities | | ---------- | ----- | ---------------------------------------------------------- | | ADMIN | 5 | Full system access, user management, system settings | | MANAGER | 4 | Project management, resource allocation, vacation approval | | CONTROLLER | 3 | Financial views, budget management, reporting | | USER | 2 | Self-service (own vacations, own resource profile) | | VIEWER | 1 | Read-only access to permitted areas | ### Per-User Permission Overrides - `permissionOverrides` JSONB field on User model - `resolvePermissions(role, overrides)` computes effective permissions - `requirePermission(ctx, key)` enforced on every tRPC procedure - Granular `PermissionKey` enum covering all domain actions ### tRPC Middleware Stack ``` publicProcedure -> protectedProcedure (requires authenticated session) -> controllerProcedure (ADMIN + MANAGER + CONTROLLER) -> managerProcedure (ADMIN + MANAGER) -> adminProcedure (ADMIN only) ``` ## 3. Data Protection ### Database Security - **PostgreSQL** with TLS in production - **Prisma ORM**: parameterized queries by default — no SQL injection risk - Database not exposed to the internet (Docker internal network only) - All monetary values stored as integer cents (no floating-point precision issues) ### Data at Rest - Passwords: Argon2id hash (never stored in plaintext) - TOTP secrets: stored in DB (encrypted at-rest via PostgreSQL TDE when available) - Runtime secrets now resolve env-first for AI, Gemini, SMTP, and anonymization seed values. Database-backed `SystemSettings` values remain transitional compatibility storage, not the preferred production source of truth. - Recommended runtime overrides: `OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY`, `AZURE_DALLE_API_KEY`, `GEMINI_API_KEY`, `SMTP_PASSWORD`, `ANONYMIZATION_SEED` - Admin settings reads expose only presence flags (`hasApiKey`, `hasSmtpPassword`, `hasGeminiApiKey`) instead of returning secret values to the browser, and those flags also reflect environment-backed runtime overrides - The admin settings mutation no longer persists new secret values into `SystemSettings`; secret inputs must be provisioned through environment or a deployment-time secret manager, and legacy database copies can be cleared explicitly - The admin UI now exposes runtime secret source/status plus an explicit "clear legacy DB secrets" cleanup path so operators can complete the migration without direct database writes - Production startup now validates Auth.js runtime configuration and refuses to boot if `AUTH_SECRET`/`NEXTAUTH_SECRET` is missing, left on a known development placeholder, or paired with a non-HTTPS public auth URL ### Anonymization - Configurable global anonymization for VIEWER role - Resource names, emails replaced with deterministic pseudonyms (seeded hash) - Anonymization domain and mode configurable in SystemSettings ## 4. Session Management - **Server-side JWT** with `SameSite=Strict` cookies - `httpOnly` cookies prevent XSS-based session theft - `secure` flag enforced in production (HTTPS only) - CSRF protection via Auth.js built-in CSRF token - Configurable session timeouts (absolute + idle) via SystemSettings - Active session registry with concurrent session limit enforcement ## 5. Input Validation - **Zod schemas** on every tRPC procedure input - Strict TypeScript (`strict: true`, `exactOptionalPropertyTypes: true`) - Blueprint dynamic fields validated at runtime against stored Zod schema definitions - File uploads validated by: - MIME type whitelist (`image/png`, `image/jpeg`, `image/webp`, `image/tiff`, `image/bmp`) - Size limit (10 MB client-side, 4 MB server-side after compression) - Magic byte verification (actual file content matched against declared MIME) ### Prompt-Injection Guard (defense-in-depth only) `packages/api/src/lib/prompt-guard.ts` runs a short regex list against every free-text user prompt sent to an AI tool (assistant chat + project-cover DALL-E prompt). Input is normalised before the regex runs: 1. Unicode NFKD decomposition (collapses fullwidth / compatibility forms and splits diacritics from their base letter). 2. Strip zero-width / directional / combining code points that attackers use to break contiguous substring matches. 3. Fold a small set of Cyrillic / Greek homoglyphs to their Latin equivalents. This guard is **defense-in-depth, not an authorisation boundary**. The actual security boundary for AI-initiated actions is the per-tool `requirePermission(ctx, PermissionKey.*)` check inside every assistant tool — an LLM that has been successfully jailbroken still cannot perform an action its caller's role does not allow. Motivated adversaries **will** find prompts that defeat the regex layer; its purpose is to raise the cost of casual injection attempts and to surface them as audit-log entries. ## 6. Audit Logging ### Activity History System - Centralized `createAuditEntry()` function (fire-and-forget, never blocks) - Covers 29+ of 36 tRPC routers - Logged fields: `entityType`, `entityId`, `action`, `userId`, `changes` (JSONB with before/after/diff), `source`, `summary` - Authentication events: login success/failure, logout, rate limiting, MFA failures ### External API Call Logging - All OpenAI/Azure/Gemini API calls logged via `loggedAiCall()` wrapper - Structured Pino logs: `{ provider, model, promptLength, responseTimeMs }` - Failed calls logged at `warn` level with sanitized diagnostics only, with URL and secret-like tokens redacted before they reach structured logs ### tRPC Request Logging - Every tRPC call logged with request ID, user ID, path, duration - Slow calls (>500ms) logged at `warn` level ## 7. HTTP Security Headers Static headers are configured in `next.config.ts`. The Content-Security-Policy is emitted per-request by `apps/web/src/middleware.ts` so it can carry a per-request nonce. | Header | Value | | ------------------------- | ---------------------------------------------- | | Strict-Transport-Security | `max-age=63072000; includeSubDomains; preload` | | Content-Security-Policy | Restrictive CSP with nonce-based script-src | | X-Frame-Options | `DENY` | | X-Content-Type-Options | `nosniff` | | X-XSS-Protection | `1; mode=block` | | Referrer-Policy | `strict-origin-when-cross-origin` | | Permissions-Policy | Camera, microphone, geolocation disabled | ### Content-Security-Policy directives (production) | Directive | Value | Rationale | | ----------------- | ------------------------- | -------------------------------------------------- | | `default-src` | `'self'` | Baseline deny-all-cross-origin. | | `script-src` | `'self' 'nonce-'` | No `unsafe-inline` / `unsafe-eval` in prod. | | `style-src` | `'self' 'unsafe-inline'` | Accepted residual risk — see note below. | | `img-src` | `'self' data: blob:` | Allow base64 previews and generated blobs only. | | `font-src` | `'self' data:` | Data URLs for inline-embedded fonts. | | `connect-src` | `'self'` | All AI / third-party calls are server-side. | | `frame-ancestors` | `'none'` | Clickjacking defence. | | `frame-src` | `'none'` | No third-party iframes. | | `object-src` | `'none'` | Blocks legacy `` / Flash / applet vectors. | | `media-src` | `'self'` | No cross-origin video / audio. | | `worker-src` | `'self' blob:` | Next.js runtime uses blob-URL workers. | | `base-uri` | `'self'` | Blocks `` hijacks. | | `form-action` | `'self'` | Blocks form-exfiltration to third parties. | **Residual risk — `style-src 'unsafe-inline'`:** React inlines component-scoped style attributes and `@react-pdf/renderer` emits inline `