Security Overview
Defense-in-depth security architecture covering authentication, license enforcement, HTTP hardening, audit logging, and encryption.
This document outlines the security architecture of the Remote Eaze DFA solution. The system employs defense-in-depth principles with cryptographic protection, multi-layered access control, and comprehensive audit capabilities.
Request Lifecycle Security Chain
Every authenticated request passes through multiple security gates in order:
Request
→ populateUser() Resolve session from cookie (Better Auth)
→ requireAuth() Validate user exists + license guard
→ passwordGuard() Enforce password change / expiry
→ requirePermission() RBAC check (4-layer engine)
→ Route handler Business logic with tenant scopingEach gate is fail-closed — if any layer throws, the request is denied and an audit event is logged.
Authentication (Better Auth)
Authentication is handled by Better Auth with Fastify integration. A catch-all route at /api/auth/* proxies requests to Better Auth's handler.
Invite-Only Registration
Public signup is blocked in production. Users are created exclusively through admin invitation:
- Admin calls
POST /api/v1/users/with user details and role assignment - System validates: email uniqueness, MX records on email domain, role and branch exist within tenant
- A cryptographically secure temporary password is generated using
crypto.randomBytes+ Fisher-Yates shuffle, meeting the tenant's password policy with a 4-character entropy buffer - Better Auth creates the account internally (
auth.api.signUpEmail()) mustChangePasswordis set totrue; welcome email sent with temp credentials- Temporary password is never returned in production API responses (dev only)
Session Management
- Sessions stored in
remoteEaze_userSession(Prisma) withid,token,expiresAt,ipAddress,userAgent - Cookie-based session management via Better Auth
- Custom session plugin enriches every session response with
roleSlugandroleName(avoids extra API calls for frontend RBAC) - Users can list, revoke specific, revoke all, or revoke other sessions
- Sessions cascade-delete when a user is deleted
Self-Service Restrictions
Users calling /api/auth/update-user cannot modify security-critical fields:
roleId, dataAccessScope, branchId, tenantId, userType, costCentre, userDeptUnit, reportingToId, staffNumber, branchRestrict
These require admin action via PUT /api/v1/users/:id.
Password Security
Tenant-Specific Policies
Each tenant configures their own password policy via their license record:
| Setting | Default | Range |
|---|---|---|
passwordMinLength | 8 | 8–128 |
passwordMinNumber | 1 | 0–10 |
passwordMinUppercase | 1 | 0–10 |
passwordMinLowercase | 1 | 0–10 |
passwordMinSpecialChar | 1 | 0–10 |
passwordChangeInterval | 90 days | 0–365 |
inactivityTimeout | 600 seconds | 60–86,400 |
The SYSTEM tenant uses a stricter default: passwordMinLength: 12.
Password Validation Points
| When | Policy Source |
|---|---|
| Sign-up (dev only) | Tenant policy from request body tenantId |
| Change password | Tenant policy resolved from session cookie |
| Reset password | SYSTEM policy (user is unauthenticated) |
| User invitation | Tenant policy for password generation |
Password Guard (Global Hook)
An onRequest hook on every request enforces password change requirements:
- If
mustChangePassword === true→ 403 with reasonPASSWORD_CHANGE_REQUIRED - If
passwordLastChangedAt + passwordChangeInterval < now→ setsmustChangePassword = true, returns 403 with reasonPASSWORD_EXPIRED
Exempt routes: /api/auth/sign-in/*, /api/auth/get-session, /api/auth/change-password, /api/auth/sign-out, /health, /documentation
SYSTEM tenant users are fully exempt from the password guard.
License Guard
On every authenticated request (except exempt routes), the guard calls fastify.licenses.getLicense(tenantId) which performs:
- Block list check — Redis key
license:blocked:{id}. If set (tampering detected), immediate 403LICENSE_TAMPERED - Cache check — Redis key
license:data:{id}. If cached, validate expiry in tenant's timezone - DB fetch — Load license, check
isActiveand soft-delete status - Integrity verification — Recompute HMAC-SHA256 signature and compare with
crypto.timingSafeEqual. On mismatch: set 5-minute Redis block, throw 403 - Expiry check — Timezone-aware end-of-day evaluation. Throw 402
LICENSE_EXPIREDif past - Cache result — Dynamic TTL:
min(3600, max(60, secondsUntilExpiry))
Exempt from license checks: SYSTEM tenant users, /api/auth/* routes, health/docs routes, and a tenant admin viewing their own license.
For the full license data model and API, see License Security.
Permission Engine
The system implements a 4-layer hybrid permission model combining RBAC with conditional rules, amount thresholds, and multi-level approval workflows.
Decision Hierarchy
| Layer | Name | Behavior |
|---|---|---|
| L0 | System Admin Bypass | roleSlug === "SYSTEM_ADMIN" → allow immediately |
| L1 | Base Permission Matrix | Role × Entity × Action lookup. If not allowed → hard deny (fast fail) |
| L2b | VALIDATION Rules | Tenant-wide hard blocks (e.g., no transactions on holidays). If match → deny |
| L2b | PERMISSION Rules | Role-specific overrides with conditions. First match by priority wins. Can change requiredLevels |
| L2a | Amount Thresholds | Currency + amount range lookup. Strict mode: no matching range → deny |
| Fallback | Layer 1 result | If no L2 rules matched, return L1 result with base requiredLevel |
Fail-Closed Policy
If Redis or the database is unreachable during permission evaluation, the system denies access rather than allowing it through. All denials are logged as HIGH sensitivity audit events.
Redis-Cached Permissions
Permissions are cached per role at permissions:{tenantId}:{roleId} with 1-hour TTL. All users sharing a role share one cache entry. Cache is invalidated on any mutation to the role's matrix, thresholds, or rules.
For the full permission system documentation, see Permissions & Maker-Checker.
HTTP Security Headers
@fastify/helmet (default configuration) sets the following headers:
| Header | Value | Purpose |
|---|---|---|
| Content-Security-Policy | default-src 'self'; ... | Restricts resource sources to same origin |
| Strict-Transport-Security | max-age=31536000; includeSubDomains | Forces HTTPS for 1 year |
| Cross-Origin-Opener-Policy | same-origin | Isolates browsing context (Spectre mitigations) |
| Cross-Origin-Resource-Policy | same-origin | Prevents cross-origin resource loading |
| X-Content-Type-Options | nosniff | Prevents MIME-sniffing |
| X-Frame-Options | SAMEORIGIN | Prevents clickjacking |
| Referrer-Policy | no-referrer | Strips referrer data |
| X-DNS-Prefetch-Control | off | Disables DNS prefetching |
| X-Download-Options | noopen | Prevents IE download execution |
| X-Permitted-Cross-Domain-Policies | none | Restricts Flash/PDF cross-domain |
| X-XSS-Protection | 0 | Disables legacy XSS filter |
Rate Limiting
@fastify/rate-limit is configured at the application layer:
- Window: 1 minute
- Max Requests: 100 per IP
- Exempt: Localhost (
127.0.0.1) for development
CORS
@fastify/cors configuration:
{
origin: env.NODE_ENV === "production"
? env.FRONTEND_URL // Strict production domain
: ["http://localhost:5173", "http://127.0.0.1:5173"], // Vite dev server
credentials: true, // Required for session cookies
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
}Audit Logging
All security-relevant events are logged through the activity log system with structured metadata, sensitivity classification, and tags.
Authentication Events
Every tracked auth event (sign-in, sign-out, change-password, reset-password, change-email, session operations) is logged with:
- Actor identification (userId, name, role, branch — resolved from DB even for failed attempts on known emails)
- Sensitivity:
HIGHfor PII operations and failures,MEDIUMfor standard auth ops changeBefore/changeAfterdiffs for data-modifying operations- Email included in metadata only when HIGH sensitivity or failure
Permission Denials
Blocked permission checks are logged with HIGH sensitivity, full context:
- Denied action and entity type
- Actor role, tenant, and branch
- Denial reason and which layer denied
- Tags:
security,authorization,access_denied
License Violations
License guard failures are logged with HIGH sensitivity:
LICENSE_TAMPERED→ tags:security,tampering,integrity_violationLICENSE_EXPIRED→ tags:security,expiry,access_deniedLICENSE_INACTIVE→ tags:security,disabled,access_denied
Tiered Guarantees
| Tier | Failure Response | Use Case |
|---|---|---|
| SYNC | Request aborted (500) | Money movement, auth changes, license violations |
| QUEUE | Background retry (BullMQ) | Updates, settings changes |
| ASYNC | Fire-and-forget | Views, searches, reports |
SYNC-tier failures prevent un-audited security events from completing.
For the full activity logging architecture, see Activity Logs.
Data Protection and Encryption
PII Sanitization Pipeline
All audit logs pass through a multi-layer pipeline before storage:
- Redaction (always) — Secrets permanently replaced with
[REDACTED](passwords, tokens, API keys, PINs, CVVs) - Encryption (HIGH sensitivity) — PII encrypted with AES-256-GCM (SSN, national IDs, email, phone, address, DOB, IBAN, account numbers)
- Truncation — Large binary data truncated to 20 chars
AES-256-GCM Encryption
- Key derivation:
scryptusingENCRYPTION_KEY+ENCRYPTION_SALTfrom environment - Format:
ENC:v1:IV:AuthTag:Ciphertext - Decryption audit: Every decrypt operation is logged with admin ID and reason
Key Management
ENCRYPTION_KEY=<32-byte-string> # AES-256-GCM key for PII protection
ENCRYPTION_SALT=<16-byte-string> # scrypt key derivation salt
LICENSE_SECRET_KEY=<32+-char-string> # HMAC-SHA256 secret for license integrity
BETTER_AUTH_SECRET=<32+-char-string> # Better Auth session secretKey Rotation Warning
Changing encryption keys without migration renders existing encrypted data unrecoverable. Key rotation requires a coordinated migration script with dual-key support.
Request Tracing
Every request carries identifiers that flow through all security layers:
- Request ID — Application-level tracking (
X-Request-ID) - Trace ID — Distributed tracing for security workflows
- Session ID — Authentication context correlation
- Tenant ID — Isolation boundary for incident scoping