Skip to content

Security Overview

Complete inventory of all security features in the ODP API.

Security Posture at a Glance


Transport & Headers

Security Headers (Helmet)

@fastify/helmet is registered on every response.

HeaderValue
Content-Security-Policydefault-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policyno-referrer
Strict-Transport-SecurityHelmet default

File: src/server.ts (registerPlugins)

CORS

Configurable per environment. Defaults allow all origins for development convenience — production should restrict.

VariableDefaultNotes
CORS_ENABLEDtrueMaster toggle
CORS_ORIGIN'*'Comma-separated list or *
CORS_METHODS'GET,POST,PATCH,DELETE'
CORS_ALLOWED_HEADERS'Content-Type,Authorization'
CORS_EXPOSED_HEADERS'Content-Range'
CORS_CREDENTIALStrue
CORS_MAX_AGE180005 hours

File: src/server.ts, src/env.ts

Session cookies use strict security settings:

AttributeValue
httpOnlytrue (no JavaScript access)
securetrue (HTTPS only)
sameSite'lax' (implicit CSRF protection)
path'/'

Configurable via SESSION_COOKIE_ENABLED (default true) and SESSION_COOKIE_NAME (default odp_session_token).

File: src/routes/auth.ts

Database SSL

VariableDefaultDescription
DB_SSLfalseEnable SSL/TLS to database
DB_SSL_REJECT_UNAUTHORIZEDtrueValidate server certificate

Authentication

Detailed docs: Auth Middleware & Token Strategy

4-Strategy Pipeline

Every request passes through authenticate middleware which tries strategies in order:

StrategyMechanismCachedUse Case
JWT (with sid)HS256 + session bindingSession cache (Redis/memory)Primary auth flow
Static tokenodp_users.token hash matchNoLong-lived API keys
Session tokenDirect odp_sessions.token lookupNoInternal flows
Sub-token (odp:)SHA256 hash + timing-safe compareNoScoped API access

Falls back to public accountability if no token or all strategies fail.

Key env vars: SECRET, ACCESS_TOKEN_TTL (15m), REFRESH_TOKEN_TTL (7d)

File: src/middleware/authenticate.ts

Session Management

  • Sessions stored in odp_sessions with token, user, IP, user-agent, expiry
  • Session cache: 3-tier (L1 in-memory → L2 Redis → L3 DB) for sub-millisecond lookups
  • Token rotation on refresh (old session gets next_token pointer)
  • Explicit cache invalidation on logout/revocation
  • Bulk logout: deleteAllUserSessionData() clears all sessions for a user

Files: src/auth/session.ts, src/auth/session-cache.ts

Password Security

  • Hashing: Argon2 (memory-hard KDF, OWASP recommended)
  • Policy enforcement from odp_settings:
    • Minimum length (default 8)
    • Require uppercase / lowercase / digit / special character
    • Custom regex pattern
    • Max age in days (password expiration)
  • password_changed_at tracked per user

Files: src/auth/drivers/local.ts, src/utils/password-policy.ts

Two-Factor Authentication (TOTP)

  • 6-digit codes, 30-second period, SHA1
  • Window: ±1 period for time drift tolerance
  • Triggered when odp_users.tfa_secret is set
  • SSO users cannot enable TFA (IdP handles MFA)

File: src/services/auth.ts

Login Throttling & Account Lockout

Progressive server-side throttling on failed login:

Attempt 1 → 0s delay
Attempt 2 → 1s delay (LOGIN_THROTTLE_DELAY base)
Attempt 3 → 2s delay
...
Attempt N → min(N × base, LOGIN_THROTTLE_MAX_DELAY)
After auth_login_attempts exceeded → account suspended
  • Counter stored in cache with 15-minute TTL
  • Reset on successful login
  • LOGIN_THROTTLE_DELAY default 1s, LOGIN_THROTTLE_MAX_DELAY default 30s
  • auth_login_attempts from odp_settings

File: src/services/auth.ts

WebSocket Authentication

WebSocket clients must authenticate via message after connection:

VariableDefaultOptions
WEBSOCKETS_REST_AUTH'handshake''public', 'handshake', 'strict'
WEBSOCKETS_GRAPHQL_AUTH'handshake'Same

Heartbeat: configurable period (default 30s) via WEBSOCKETS_HEARTBEAT_PERIOD.

File: src/services/websocket.ts


Access Control

Role-Based Access Control (RBAC)

User → Role → Policies → Permissions (per collection, per action, per field)
  • Roles have access flags: admin_access, tech_access, app_access
  • Policies attach to roles, define permission sets
  • Permissions are per-collection, per-action (read, create, update, delete), with optional field lists and row-level filters
  • tech_access implies admin_access

Files: src/permissions/validate-access.ts, src/permissions/process-ast.ts

Authorization Middleware

MiddlewareChecksProtects
requireAdminaccountability.admin === trueDDL routes, schema, roles, policies, settings
requireAppAccessaccountability.app || accountability.adminApp panel endpoints
collectionExistsCollection exists in schemaItem CRUD routes

Files: src/middleware/require-admin.ts, src/middleware/require-app-access.ts

Impersonation Safeguards

Admins can impersonate other users with strict guardrails:

  • Requires impersonate_access or tech_access role flag
  • Cannot impersonate self
  • Cannot nest impersonation (admin already impersonating → denied)
  • Cannot impersonate users with tech_access or impersonate_access
  • Max concurrent sessions: MAX_IMPERSONATION_SESSIONS (default 3)
  • Session TTL: IMPERSONATION_TTL (default 15m)
  • All start/stop events logged to activity + security events

File: src/services/impersonation.ts

Sub-Token Scoping

Sub-tokens (odp: prefix) enforce least-privilege:

  • SCOPE-01: allowed_roles intersected with user's current roles at auth time — prevents privilege creep if user roles change after token creation
  • SCOPE-03: scopes validated against fixed set: ['read', 'create', 'update', 'delete']
  • SCOPE-04: Sub-tokens cannot create other sub-tokens
  • Token format: odp: + 64-char hex, stored as SHA256 hash
  • Timing-safe comparison to prevent timing attacks

Files: src/services/sub-token.ts, src/middleware/authenticate.ts


Runtime Protection

Rate Limiting

Detailed docs: Rate Limiting

Three layers registered at different Fastify hook points:

LayerHookKeyDefaultPurpose
GlobalonRequest'global'1000/1sServer throughput cap
Per-IPonRequestrequest.ip50/1sSingle-IP abuse
Per-UserpreHandleruserId || ip100/60sPer-identity abuse
Emailper-routerequest.ip5/60sPassword reset spam

Backend: RateLimiterMemory (default) or RateLimiterRedis with insuranceLimiter fallback.

SSRF Protection

Detailed docs: SSRF Protection

safeFetch() wraps all outbound HTTP calls:

  • Protocol whitelist: http: and https: only
  • DNS resolution before connect — blocks all RFC1918, loopback, link-local, cloud metadata ranges
  • Manual redirect following with per-hop validation (max 5 hops)
  • DNS cache (5min TTL) to prevent rebinding attacks
  • Configurable allowlist via SSRF_ALLOWED_HOSTS

Applied to: OAuth2 token exchange, OpenID discovery, Mattermost webhooks.

Input Validation

  • Zod schemas for all query parameters and request payloads
  • Relational depth limit: max 10 levels (configurable via QUERYSTRING_MAX_PARSE_DEPTH)
    • Enforced across: fields, filter, sort, deep queries
    • Prevents exponential query expansion
  • Query string parsing: qs library with arrayLimit: 100, parameterLimit: 500
  • Body limit: MAX_PAYLOAD_SIZE (default 1mb)
  • Filter operators: strict Zod union — no arbitrary operators accepted
  • Sanitized query frozen after parsing to prevent downstream mutation

Files: src/middleware/sanitize-query.ts, src/utils/validate.ts, src/utils/validate-relational-depth.ts

Path Traversal Protection

Local storage driver validates all file paths:

resolved = path.resolve(root, filepath)
if (!resolved.startsWith(root + path.sep)) → throw 'Path traversal detected'

Prevents ../../etc/passwd style attacks on file read/write.

File: src/storage/local.ts

SQL Injection Prevention

All database access uses Knex.js parameterized queries. No raw string concatenation in SQL anywhere in the codebase. Advisory locks use integer constants (pg_advisory_xact_lock(9999)), not user input.

Error Sanitization

  • Database errors pattern-matched and mapped to safe HTTP status codes
  • Stack traces hidden from responses (only visible at debug/trace log level)
  • Constraint violations → 409/400 with generic message
  • sanitizeErrorResponse() onSend hook catches errors that bypass the custom error handler
  • Extension hook request.error allows customization

File: src/middleware/error-handler.ts


Monitoring & Audit

Security Event Logging

Detailed docs: Security Events

Dedicated odp_security_events table + pino structured logging for 22 event types:

CategoryEvents
AuthenticationLOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, PASSWORD_CHANGED, PASSWORD_RESET_REQUESTED, SESSION_REVOKED
AuthorizationROLE_ASSIGNED, ROLE_REMOVED, PERMISSION_CHANGED
TokensTOKEN_CREATED, TOKEN_REVOKED
MFAMFA_ENABLED, MFA_DISABLED
ImpersonationIMPERSONATION_STARTED, IMPERSONATION_STOPPED
InfrastructureCONFIG_CHANGED, SSRF_BLOCKED, RATE_LIMIT_TRIGGERED, AUDIT_CHAIN_BROKEN, DRIFT_DETECTED

Every event carries: correlation_id (from x-request-id), user_id, ip, user_agent, severity, details (JSONB).

Audit Hash Chain

Detailed docs: Audit Hash Chain

HMAC-SHA256 chain on odp_activity records:

  • Each new record hashes its content + the previous record's hash
  • pg_advisory_xact_lock(9999) serializes writes to prevent race conditions
  • Scheduled background verification detects tampering
  • Emits AUDIT_CHAIN_BROKEN (severity: critical) on chain break

Configuration Drift Detection

Detailed docs: Drift Detection

Monitors security-relevant config (env vars + DB settings + admin roles):

  • Baseline captured at startup, compared on schedule (default: every 5min)
  • Event-driven check on settings update
  • Cross-node hash comparison via Redis (odp:drift:{nodeId})
  • Emits DRIFT_DETECTED (severity: warning) with diff details

Sensitive Field Redaction

Sensitive fields are automatically stripped from outputs:

FieldTableBehavior
passwordodp_usersStripped from all API responses
tfa_secretodp_usersStripped, replaced by computed tfa_enabled: boolean
tokenodp_usersStripped from API responses

Also redacted in: activity log payloads, emitter events, data exports (JSON/CSV/XML/YAML), revision deltas.

File: src/utils/sensitive-fields.ts

Activity Logging

All CRUD mutations logged to odp_activity with: action, user, timestamp, IP, user-agent, collection, item key, origin. Used for audit trail and content version history.


Request Pipeline (Full)

Complete hook registration order in server.ts:

onRequest:
  1. Correlation ID (AsyncLocalStorage)       ← x-request-id / UUID
  2. extractToken                             ← Header / Cookie / Query
  3. rateLimiterGlobal                        ← 1000 req/s app-wide
  4. rateLimiterIP                            ← 50 req/s per IP

preHandler:
  1. authenticate                             ← JWT → Static → Session → Sub-token → Public
  2. rateLimiterPerUser                       ← 100 req/60s per identity
  3. loadSchema                               ← Attach DB schema
  4. sanitizeQuery                            ← Zod validation + depth check

Route-level preHandlers (as needed):
  - requireAdmin
  - requireAppAccess
  - collectionExists
  - validateBatch

onSend:
  - cacheResponse
  - sanitizeErrorResponse                     ← Catch escaped errors

Startup schedulers:
  - initAuditVerification()                   ← Cron chain verification
  - initDriftDetection()                      ← Cron drift check

Redis Dependency Matrix

FeatureWithout RedisWith Redis
Session cacheIn-memory (per-instance)Shared across pods
Public role cacheL1 memory + L3 DBL1 memory + L2 Redis + L3 DB
Rate limiterPer-instance countingDistributed counting
Security eventsFullFull
SSRF protectionFullFull
Audit hash chainFullFull
Drift detectionLocal check onlyLocal + cross-node comparison
Login throttlePer-instance counterShared counter

Environment Variables (Security)

Transport

VariableDefaultDescription
SECRET(required)JWT signing key (min 32 chars recommended)
DB_SSLfalseDatabase SSL/TLS
DB_SSL_REJECT_UNAUTHORIZEDtrueValidate DB server certificate
CORS_ENABLEDtrueCORS toggle
CORS_ORIGIN'*'Allowed origins
SESSION_COOKIE_ENABLEDtrueSession cookie mode
SESSION_COOKIE_NAME'odp_session_token'Cookie name

Auth & Session

VariableDefaultDescription
ACCESS_TOKEN_TTL'15m'JWT access token lifetime
REFRESH_TOKEN_TTL'7d'Refresh token / session lifetime
LOGIN_THROTTLE_DELAY'1s'Base delay per failed attempt
LOGIN_THROTTLE_MAX_DELAY'30s'Max throttle delay
IMPERSONATION_TTL'15m'Impersonation session lifetime
MAX_IMPERSONATION_SESSIONS3Max concurrent impersonation sessions

Runtime Protection

VariableDefaultDescription
SECURITY_EVENTS_ENABLEDtrueSecurity event logging
SSRF_PROTECTION_ENABLEDtrueSSRF validation on outbound HTTP
SSRF_ALLOWED_HOSTS''SSRF allowlist (comma-separated)
RATE_LIMITER_ENABLEDfalseRate limiter master toggle
RATE_LIMITER_STORE'memory''memory' or 'redis'
RATE_LIMITER_POINTS50Per-IP limit
RATE_LIMITER_DURATION1Per-IP window (seconds)
RATE_LIMITER_USER_POINTS100Per-user limit
RATE_LIMITER_USER_DURATION60Per-user window (seconds)
RATE_LIMITER_LOGIN_POINTS10Login attempt limit
RATE_LIMITER_LOGIN_DURATION60Login window (seconds)
RATE_LIMITER_GLOBAL_ENABLEDtrueGlobal limiter toggle
RATE_LIMITER_GLOBAL_POINTS1000Global limit
RATE_LIMITER_GLOBAL_DURATION1Global window (seconds)

Audit & Monitoring

VariableDefaultDescription
AUDIT_HASH_ENABLEDfalseAudit hash chain
AUDIT_HMAC_KEY''HMAC key (must differ from SECRET)
AUDIT_VERIFY_SCHEDULE'0 * * * *'Verification cron (hourly)
DRIFT_DETECTION_ENABLEDfalseConfig drift detection
DRIFT_CHECK_SCHEDULE'*/5 * * * *'Drift check cron (5min)

Input Limits

VariableDefaultDescription
MAX_PAYLOAD_SIZE'1mb'Request body size limit
QUERYSTRING_MAX_PARSE_DEPTH10Max query nesting depth
MAX_RELATIONAL_DEPTH10Max relational field depth

ODP Internal API Documentation