Skip to main content
This page describes the security architecture of KaireonAI as it is implemented today. Where a capability is planned but not yet shipped, it is marked explicitly.

Tenant Isolation

KaireonAI is multi-tenant by default. Every database record includes a tenantId column, and every API query is scoped to the authenticated tenant.

Application-Layer Filtering

All database access flows through a tenant-scoped data layer that automatically constrains every query to the authenticated tenant:
  • The query layer rejects any read or write that lacks an explicit tenant filter, so a missing tenant clause becomes a server-side error rather than a cross-tenant data leak.
  • Read, create, update, and delete operations against tenant-scoped tables are wrapped to inject the tenant ID before the query runs, so application code cannot accidentally widen the scope.
If the tenant cannot be resolved from the session or API key, the request is denied with 403 Forbidden.

Row-Level Security (RLS)

For database-level isolation as a second boundary, KaireonAI applies PostgreSQL RLS policies to tenant-scoped tables. By default this runs automatically at server startup (enableRLSOnAllTables) for the tables registered in src/lib/db/rls.ts; set RLS_AUTO_ENABLE=false to skip it — for example when your database role lacks the privileges to alter policies, or you manage RLS out of band. An admin API (POST /api/v1/admin/rls) is also available to (re)apply those policies on demand. RLS requires sufficient privileges on the database and adds a small query overhead. Production status (as of 2026-07-10): RLS is enabled and forced on all 111 target tables — every table with a tenantId/tenant_id column gets a tenant_isolation policy (scoped to the session variable app.current_tenant_id); tables without one get default-deny RLS instead. The remaining 51 tables beyond the startup-managed set (e.g. consent_records, webauthn_credentials, shopify_shops) were enabled directly via prisma/manual-sql/35_rls_track_a.sql. Supabase’s Data API (the anon/PostgREST HTTP surface) is also disabled at the source, closing the one externally-reachable path that could hit these tables directly without going through the application at all.
RLS is a defense-in-depth layer for external/direct-connection access, not the enforcement mechanism for the app’s own queries. The application’s own Postgres connection uses a BYPASSRLS-class role by design, so none of these policies constrain what the app itself can read or write — the app-layer where: { tenantId } filter (above) is what actually protects the app’s own traffic, and it is always active regardless of RLS state. A Prisma-extension-based path to make RLS govern the app’s own connection too (withTenantRLS(), using a non-bypass role + AsyncLocalStorage) is designed but has zero production call sites yet — it’s tracked as a separate, higher-risk activation, not part of what’s described above.

Single-Tenant Mode

Self-hosted deployments can set SINGLE_TENANT_MODE=true to bypass multi-tenant resolution. In this mode, all data belongs to a single default tenant and the tenant resolution middleware is skipped.

Authentication

KaireonAI supports three authentication methods, each suited to a different integration pattern.

Browser Sessions (NextAuth)

Interactive users authenticate via NextAuth.js sessions:
  • Google OAuth — one-click sign-in, automatic email verification.
  • Email and password — bcrypt-hashed passwords, email verification flow, password reset via time-limited tokens.
Sessions are stored as signed HTTP-only cookies. Session tokens are rotated on each request. The session includes userId, tenantId, and role.
MFA (TOTP) — precise status. TOTP endpoints at /api/v1/auth/mfa (setup, enable, verify, disable) are implemented and functional per RFC 6238. Secrets are encrypted at rest, backup codes are one-way-hashed, token comparison is timing-safe, and the verify path is rate-limited. Middleware enforcement is active. For admin accounts with MFA enabled, any state-changing API request (POST/PUT/PATCH/DELETE under /api/) requires a fresh step-up proof. The proof is a server-issued HMAC cookie (kaireon_stepup) minted only by a successful TOTP verify (or a WebAuthn verify) — no client-side session.update, so a client cannot forge its own freshness. The cookie self-expires after 15 minutes; requests without it get 403 MFA_REQUIRED. Set MFA_ENFORCEMENT_DISABLED=true to bypass (incident break-glass only). Kill-switch usage is audited (SOC 2 Phase 0): at process startup, an engaged MFA_ENFORCEMENT_DISABLED (or CSP_DISABLED) writes a killswitch_active audit-log entry and a structured warn log; per request, the first admin write it actually bypasses also logs a throttled (max once per 5 minutes) structured warning from the middleware itself — the switch can no longer flip behavior with zero trace.
WebAuthn (FIDO2 hardware-backed MFA) — shipped 2026-05-03. Four HTTP routes implement the standard WebAuthn ceremonies. Registration uses POST /api/v1/auth/webauthn/register/begin followed by POST /api/v1/auth/webauthn/register/finish; sign-in uses POST /api/v1/auth/webauthn/verify/begin followed by POST /api/v1/auth/webauthn/verify/finish. Challenges are persisted in Redis with a 60-second TTL and consumed atomically (GETDEL) keyed by user and ceremony purpose, so replays of /finish fail closed. Registration verifies the client-data round-trip (type, challenge, origin); verification re-checks the assertion signature with a full COSE-key parse supporting ES256 and RS256, and rejects assertions whose authenticator counter has not advanced (monotonic-counter replay defence). Enrolled credentials are stored per user. The four-eyes admin enrolment flow is the compensating control for V1’s attestation: "none" acceptance — attestation conveyance is V3.

API Keys (Server-to-Server)

For programmatic access, users generate API keys from the Settings page. Keys are prefixed krn_ for easy identification in logs.
  • Keys are stored as one-way HMAC-SHA256 hashes — the raw key is shown once at creation and cannot be recovered.
  • Each key is bound to a specific tenant. The X-Tenant-Id header is ignored when authenticating via API key to prevent tenant spoofing.
  • Keys support optional scopes (read-only, write, admin) and expiration dates.

Internal Service Tokens

For internal service-to-service calls (e.g., pipeline workers, cron jobs), the platform accepts a shared INTERNAL_SERVICE_SECRET validated via constant-time comparison. This token bypasses session lookup and is intended for trusted infrastructure only.
Keep INTERNAL_SERVICE_SECRET out of client-side code and environment variable logs. Rotate it when team members leave.

CSRF Protection

KaireonAI uses a dual-strategy CSRF model: The X-Requested-With check is enforced in the API middleware layer. Requests without this header or a valid API key are rejected with 403.
This is a standard defense — the same approach is used by Django, Rails, and Angular. It relies on the browser refusing to attach custom request headers on cross-origin requests, which is enforced by the same-origin policy in all modern browsers.

SSRF Protection

Server-Side Request Forgery (SSRF) is a significant risk because KaireonAI makes outbound HTTP calls in several places: webhook delivery, REST API connectors, CMS content sync, external ML scoring endpoints, alert notifications, and trigger engine webhooks.

Two-Layer Validation

  1. Hostname check (synchronous) — Outbound URLs are first screened against a deny list of known-bad hostnames (localhost, metadata.google.internal, 169.254.169.254) and private IPv4/IPv6 ranges (10.x, 172.16-31.x, 192.168.x, 127.x, ::1, fc00::/7, fe80::/10).
  2. DNS resolution check (async) — The platform then resolves the hostname via DNS (both A and AAAA records) and verifies that every resolved IP is outside private ranges before issuing the request. This prevents DNS rebinding attacks where a hostname initially resolves to a public IP during validation but later resolves to a private IP.

Protected Code Paths

The full DNS-resolution check runs before any outbound HTTP call from the following surfaces:
  • Outbound webhook delivery
  • REST API connectors used by data pipelines
  • External ML scoring endpoints
  • CMS content sync (every adapter URL is validated)
  • Alert notification webhooks
  • Trigger-engine webhooks
Admin-configured SSO endpoints (JWKS and token URLs) use the synchronous hostname check at configuration time; because these URLs are vetted by an administrator and stored, the lighter-weight check is sufficient.

Self-Hosted Recommendation

If you run KaireonAI on a private network, consider adding an egress proxy (e.g., Squid, Envoy) that restricts outbound traffic to known-good destinations. The application-level SSRF check is a strong default, but an egress proxy provides defense in depth at the network layer.

Rate Limiting

KaireonAI includes a sliding-window rate limiter with two storage backends:
  • In-memory — suitable for single-process deployments. Tracks request timestamps per key and rejects when the count exceeds the configured limit within the window.
  • Redis-backed — uses sorted-set sliding window (zadd + zremrangebyscore + zcard) for consistent rate limiting across multiple Node.js processes.
The limiter tries the Redis backend first and falls back to in-memory tracking if Redis is unavailable, so rate limiting is always active even during a Redis outage. Rate limits are applied to:
  • Journey callback endpoints
  • The Recommend API (configurable per deployment)
  • Authentication endpoints (SOC 2 Phase 0) — per-IP, Redis-backed (pool-wide across all container instances), on top of the per-account lockout in authorize() (5 failed attempts locks that account for 15 minutes): POST /api/auth/callback/credentials (the NextAuth credentials login endpoint — /api/auth/* is excluded from the Edge middleware’s matcher entirely, so this route enforces the limit itself) and the verify action of POST /api/v1/auth/mfa (layered on top of its pre-existing per-user 3/60s limit). Defaults to 10 attempts / 5 minutes / IP, configurable via AUTH_RATE_LIMIT_MAX and AUTH_RATE_LIMIT_WINDOW_MS. Breaches return 429 with Retry-After. The client IP is taken from the rightmost X-Forwarded-For entry (the value the trusted edge — App Runner / ALB — appends), not the leftmost client-supplied one, so an attacker cannot rotate a spoofed X-Forwarded-For to land in a fresh bucket each request. Redis-outage posture differs by surface: login fails open (degrades gracefully — a Redis outage must not deny every login; the DB-backed per-account lockout still blunts targeted brute-force during the window, so Redis is not required for login to function), while MFA verify fails closed (stricter, lower-volume gate — matches its sibling per-user limit).
For DDoS protection, we recommend using edge-level rate limiting (AWS WAF, Cloudflare rate rules, or nginx limit_req_zone) in addition to the application-level limiter.

Encryption at Rest

Connector Secrets

All sensitive connector credentials (database passwords, API tokens, OAuth client secrets) are encrypted with AES-256-GCM before storage. Each record uses a unique initialization vector (IV) and authentication tag. The encryption key is derived from the CONNECTOR_ENCRYPTION_KEY environment variable. Self-hosted deployments must set this to a cryptographically random 32-byte hex string. Key rotation is supported without re-encrypting existing rows in one step: set the new key in CONNECTOR_ENCRYPTION_KEY and keep the old key in CONNECTOR_ENCRYPTION_KEY_PREVIOUS so existing ciphertext still decrypts while new writes use the current key. CONNECTOR_ENCRYPTION_KEY_VERSION (default 1) and CONNECTOR_ENCRYPTION_KEY_PREVIOUS_VERSION (default 0) tag each ciphertext with the key version that produced it.

API Key Hashing

Platform API keys (krn_ prefix) are stored as one-way HMAC-SHA256 hashes. The raw key is displayed once at creation and cannot be recovered from the database. This includes the auto-created onboarding key from POST /api/v1/auth/register — its PlatformSetting bookkeeping row (category: "onboarding", key: "initial_api_key") stores the same peppered hash as the real ApiKey record, never the raw key (SOC 2 Phase 0; previously plaintext).

Password Hashing

User passwords are hashed with bcrypt (cost factor 12) before storage.

DSAR Export Payloads

DSAR export payloads (dsar_exports.payload — full per-subject PII dumps) are encrypted (AES-256-GCM) at rest by default (SOC 2 Phase 0). GET /api/v1/dsar/{id}/download decrypts server-side, after the admin-role + tenant-scope check, and always serves plain JSON. See DSAR.

Audit Logging

Every mutating operation is recorded in an immutable audit log with:
  • Tenant ID, user ID, action, entity type, and entity ID
  • Before-and-after JSON snapshots (for update operations)
  • The originating IP address and user agent
  • A server-assigned timestamp
Audit log entries cannot be edited or deleted through the API. Every successful mutation in an API route emits one entry as part of the same request, so the audit trail and the underlying state stay in lockstep. Audit logs can be queried via the UI (Settings > Audit Log) or fetched programmatically via GET /api/v1/audit-logs (paginated JSON, suitable for SIEM forwarders that poll an HTTP source).

LLM Explanation Safety

KaireonAI can generate natural-language explanations of individual decisions via POST /api/v1/decisions/:id/narrative. Because these explanations are sent to an external LLM provider, the feature has several defense layers:
  • Per-tenant opt-in — Disabled by default. The flag tenantSettings.aiAnalyzerSettings.llmExplanationsEnabled must be set to true before any narrative is produced. A 403 response is returned otherwise.
  • PII redaction — Customer attributes are stripped of personally identifiable fields before the request body is assembled. The prompt sent to the LLM only contains offer IDs, feature contributions, and policy reasons, not raw customer fields.
  • Audit log for regulator mode — When mode = "regulator", the handler writes a dedicated audit-log entry (action generate_narrative, entity type decision_trace) capturing the mode, model, cache-hit flag, and the first 200 characters of the narrative. This supports DSAR exports and compliance review.
  • Out-of-band from /recommend — Narrative generation runs only on demand against the persisted decision-trace record for a previously made decision. It never executes during a /recommend call, so LLM latency or availability cannot affect decisioning.
  • Rate limited — 20 narrative requests per minute per tenant.
  • Cached — Results are cached in Redis for 7 days keyed by (tenantId × decisionTraceId × mode × model × inputsHash), so repeat requests for the same trace do not hit the LLM again.
See the LLM Explanations page for how to enable the feature and a worked example.

Input Validation

All API request bodies are validated with Zod schemas before processing. Invalid requests are rejected with 400 Bad Request and a structured error response listing the validation failures. The formula engine uses a custom recursive-descent parser — dynamic code execution via eval or constructor-based evaluation is never used. External content IDs (for example, CMS entry IDs) are sanitized to the character set [a-zA-Z0-9._-] before being used in any downstream call or storage path.

Webhook Signature Verification

Inbound webhooks (from CMS providers, payment systems, etc.) are verified using HMAC-SHA256 signature validation with constant-time comparison to prevent timing attacks. The signing secret is configured per integration. Outbound webhooks sent by KaireonAI include an X-Kaireon-Signature header containing a sha256= prefixed HMAC, allowing recipients to verify authenticity.

What Users Should Configure

For a production deployment, ensure these are set:
In production (NODE_ENV=production), the app validates its environment at startup and refuses to boot if any of NEXTAUTH_SECRET, JWT_SIGNING_SECRET, CONNECTOR_ENCRYPTION_KEY, WEBHOOK_SIGNING_SECRET, or API_KEY_PEPPER is missing, or if CORS_ALLOWED_ORIGINS is unset, empty, or *. DATABASE_URL is required in every environment.

MCP & AI Tool Surface

The platform exposes an MCP server and an in-app AI assistant that call the same /api/v1/* routes. In production, MCP write operations are blocked by defaultPOST/PUT/PATCH/DELETE tools return a read-only error unless you set MCP_ALLOW_WRITES=true. Read tools always work. This keeps an exposed MCP endpoint from mutating tenant data unless writes are deliberately enabled.

Planned Enhancements

The following capabilities are on the roadmap but not yet fully implemented:
  • OAuth 2.0 provider — Allow external applications to authenticate via OAuth flows
  • SCIM provisioning — Automated user provisioning from identity providers
  • Field-level encryption — Encrypt sensitive customer data fields at the application layer
  • IP allowlisting — Restrict API key usage to specific IP ranges