Skip to main content
Sevrel← Back to Home

Security

Technical security documentation for IT and compliance teams evaluating Sevrel. This page details the cryptographic primitives, network architecture, access controls, and hardening measures that protect your organization's data.

1. Architecture

Sevrel uses a hybrid zero-trust deployment model that separates the web application, API backend, and LLM inference across independent trust boundaries. No single component compromise grants access to the full pipeline.

Data Flow

Browser (HTTPS)
  → Cloudflare Pages (static export, no server-side rendering)
  → FastAPI backend (Railway, managed PaaS)
       ├── Railway PostgreSQL (TLS-enforced, provider-managed encryption at rest)
       ├── Egnyte MCP (document search & retrieval, org-level OAuth)
       └── Enterprise AI provider (HTTPS, API-key authenticated)
              Tiered model routing: fast / standard / deep
              Built-in web search (server-side, no third-party search API)

Trust Boundaries

  • Frontend → Backend: HTTPS with CORS restricted to explicit origins. Session cookies with HttpOnly Secure SameSite=Strict. No secrets in frontend — only NEXT_PUBLIC_* environment variables (non-sensitive).
  • Backend → LLM: HTTPS to our enterprise AI provider. Every request authenticated with a server-side API key. Key validated at startup — the application will not start without it.
  • Backend → Database: TLS-encrypted PostgreSQL connections (ssl=require enforced in asyncpg connect args). Credentials injected at runtime via DATABASE_URL environment variable — never committed to source control.
  • Backend → Egnyte: Per-user OAuth tokens with encrypted storage. All API calls scoped to configured base path with traversal protection. Per-user token isolation prevents cross-tenant document access.

Key Design Decisions

  • Enterprise AI with data protection: AI processing uses our enterprise AI provider, which does not use API data to train models. Only the minimum document context required for each query is sent. Tiered model routing ensures cost-efficient processing.
  • Static frontend export: No server-side rendering eliminates SSRF and server-side template injection attack surface on the frontend.
  • No data training: Your documents are never used to train or fine-tune any model. The LLM weights are frozen — your data cannot influence model behavior for other customers.

2. Authentication & Identity

Sevrel delegates identity management entirely to Microsoft Entra ID (Microsoft Entra ID). We do not store or manage passwords for SSO users.

Microsoft Entra ID Token Validation

When a user signs in, the frontend obtains an Microsoft access token via MSAL and sends it to POST /api/auth/azure with a Authorization: Bearer header. The backend performs full cryptographic validation:

Signature AlgorithmRS256 (RSA public key from Microsoft JWKS)
JWKS Resolution4-source fallback: tenant-specific → issuer discovery → common tenant → organizations tenant
JWKS CachePer-URL cache, 3,600s TTL, max 50 entries with LRU eviction, thread-safe (prevents TOCTOU races)
Audience CheckMust match AZURE_CLIENT_ID or api://{AZURE_CLIENT_ID}
Issuer WhitelistOnly https://sts.windows.net/ and https://login.microsoftonline.com/ accepted
Tenant EnforcementWhen configured with a specific tenant GUID, rejects tokens from other tenants
Clock Skew Leeway5 seconds (OIDC standard)
SSRF ProtectionTenant ID validated against UUID regex before URL interpolation; jwks_uri domain must match Microsoft hosts
Token Size Cap8,192 characters max (prevents DoS via oversized tokens)

User Provisioning

Users are matched by Microsoft Object ID (oid claim) — an immutable identifier that survives email and display name changes. On first login, a Sevrel account is automatically provisioned. Email domain allowlists can restrict which organizations are permitted. Domain checks are re-enforced on every Microsoft login to catch email changes from the identity provider.

Timing Attack Mitigation

All authentication paths execute in constant time regardless of whether the user exists. For password-based flows, a dummy bcrypt hash is verified on user-not-found to prevent account enumeration via timing side channels. Admin email comparisons use hmac.compare_digest() with length padding to eliminate timing oracles.

3. Session Management

Cookie Format & Signing

Sessions use a 4-part signed cookie: accountId.expiresAt.sessionId.signature

Signing AlgorithmHMAC-SHA256
Key DerivationHKDF-SHA256 from SECRET_KEY (≥256-bit). Salt: 'sevrel-session-hmac-v1', Info: 'session-cookie-signing'. Produces a 32-byte domain-separated key unique to session cookies.
Signature VerificationConstant-time comparison via hmac.compare_digest() — performed BEFORE any other validation to prevent timing leaks
Cookie Length Cap300 characters max (prevents DoS via oversized HMAC input)
Expiry Cap30-day maximum — even a correctly signed cookie is rejected if expiresAt exceeds this cap
Account ID ValidationMust be valid UUID format (prevents injection in downstream database queries)

Cookie Attributes

HttpOnlytrue — cookie is inaccessible to client-side JavaScript
Securetrue in production — transmitted only over HTTPS
SameSiteStrict in production (Lax in development)
Max-Age604,800 seconds (7 days)
DomainNot set — browser defaults to exact request domain, preventing subdomain leakage
Path/

Server-Side Session Revocation

Every login creates a row in the user_sessions table recording the session ID, user ID, IP address, user agent, creation time, and expiration. On every authenticated request, the backend verifies the session has not been revoked. Administrators and users can invalidate all active sessions across all devices via POST /api/auth/logout-all. Logout sets revoked_at on the session record.

Sliding Session Refresh

When a session passes 50% of its lifetime, the backend automatically re-issues a fresh signed cookie with a new session ID and records it in the database. This prevents active users from being logged out while maintaining a tight absolute expiry window. Refresh only occurs on successful responses (HTTP < 400) and is skipped if the handler already deleted the cookie (logout flow).

4. Encryption & Key Management

In Transit

TLS VersionsTLS 1.2 and TLS 1.3 only — all older protocols disabled
Cipher SuitesECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-GCM-SHA384, ECDHE-ECDSA-CHACHA20-POLY1305, ECDHE-RSA-CHACHA20-POLY1305 (server-preferred)
HSTSmax-age=63072000; includeSubDomains; preload (2-year STS, preload-list eligible)
Session TicketsDisabled (ssl_session_tickets off) — prevents ticket-based resumption attacks
OCSP StaplingEnabled with verification
Database TLSEnforced via ssl='require' in asyncpg connect_args, applied even when the provider strips sslmode from the connection URL

At Rest

OAuth Token EncryptionFernet (AES-128-CBC + HMAC-SHA256, authenticated encryption with associated data)
Key DerivationHKDF-SHA256 from SECRET_KEY. Output: 32 bytes. Salt: configurable via FERNET_SALT or fixed domain-separation salt. Info: 'fernet-encryption-key-v2'
Token TTL EnforcementOptional TTL parameter on decryption — access tokens rejected after 2 hours regardless of encryption validity
Key Rotationreset_fernet() clears cached key instance, enabling SECRET_KEY rotation without restart
Database EncryptionProvider-managed encryption at rest (Railway PostgreSQL)
Startup GuardRuntimeError raised if SECRET_KEY is empty at key derivation time — prevents silent use of a zero-entropy key

Password Hashing

Algorithmbcrypt via passlib (CryptContext)
Work Factor12 rounds (4,096 iterations)
VerificationConstant-time via passlib internals
Auto-RehashDeprecation mode 'auto' — older bcrypt versions are re-hashed on successful login

Internal JWT (Access Tokens)

AlgorithmHS256 (HMAC-SHA256, symmetric) — hardcoded in code with a field validator that rejects any other value, preventing algorithm confusion attacks
KeySECRET_KEY (required ≥32 characters)
Expiry60 minutes (configurable)
Claimssub (subject), exp (expiration) — minimal claim surface

5. Access Control

Role-Based Access Control (RBAC)

Three tiers with least-privilege defaults. New users are assigned viewer by default. The permission matrix is defined as Python frozenset constants — immutable at runtime, preventing accidental mutation or configuration drift.

PermissionViewerMemberAdmin
chat:use
documents:read
conversations:read / write
calendar:read
email:read
documents:ingest
calendar:write
email:write
admin:accounts / roles / audit

Enforcement

  • Permissions are enforced via FastAPI dependency injection on every endpoint — Depends(require_permission("admin:accounts")). No endpoint can be accessed without passing the permission guard.
  • Permission denials are logged as security audit events with the user ID, requested permission, and correlation ID.
  • Admin email bypass (for initial bootstrap) uses hmac.compare_digest() with equal-length padding to prevent timing side-channel attacks.

Multi-Tenancy Isolation

Organization-scoped data isolation via org_idforeign keys on core tables (users, conversations, documents, audit logs). All queries are scoped to the authenticated user's organization. Egnyte document access uses per-user OAuth tokens — one user's token cannot access another user's files.

6. Data Handling & Privacy

Document Processing

When Sevrel answers a question, it fetches document content from your storage vault in real time and processes it in memory to generate the response. Full document text is not persisted as source-of-truth. Conversation metadata (message text, document names, page numbers, and short excerpts used as citations) is stored in PostgreSQL. Embeddings used for retrieval are stored in pgvector and scoped per organization.

PDF Extraction Cap2,000,000 characters max
XLSX Extraction Cap50,000 characters max
DOCX Decompressed Size100 MB max (ZIP bomb protection)
CSV/TXT Extraction Cap200,000 characters max
Vector Storepgvector (PostgreSQL extension), stored within the application database. Embeddings are scoped per organization and encrypted at rest via the database provider. All retrieval honors row-level security.

Conversation Storage

Storage FormatJSONB column in PostgreSQL (messages_json)
Max Message Length128,000 characters per message
Max Messages per Conversation500 (oldest messages pruned on overflow)
Max Conversations per User2,000
Row-Level LockingSELECT ... FOR UPDATE prevents lost-update races on concurrent message appends
Title SanitizationControl characters (0x00-0x1f, 0x7f-0x9f), Unicode direction overrides, and prompt delimiters stripped

What We Do NOT Do

  • We do not train or fine-tune models on your data
  • We do not share your data with any third party
  • We do not log document content to application logs — only metadata (file names, page numbers)
  • We do not send your queries or documents to any AI provider outside the single enterprise AI provider we operate

Data Deletion

Users can delete individual conversations or all their data. Deletion removes conversation records, associated messages, and evidence metadata from PostgreSQL. Because document content is never persisted, there is nothing to delete on the document side — your files remain in your Egnyte vault, untouched.

7. Network & Infrastructure

Infrastructure Security

The backend application runs on Railway, a managed PaaS with automated security patching and network isolation. The database (PostgreSQL) is hosted on Railway with TLS-enforced connections and provider-managed encryption at rest. All external API communication (AI provider, Egnyte, Microsoft Graph) uses HTTPS with API key or OAuth authentication.

Cloudflare (CDN & Hosting)

The frontend is served via Cloudflare Pages with automatic HTTPS, DDoS protection, and global CDN distribution. All API requests from the frontend to the backend use HTTPS with session cookie authentication.

API Security

All external API communication is authenticated and encrypted:

Enterprise AI providerAPI key authentication over HTTPS, tiered model routing
Egnyte MCPOrganization-level OAuth tokens, proactively refreshed
Microsoft GraphOBO token exchange, encrypted token storage at rest
StripeServer-side only, webhook signature verification

API keys and OAuth tokens are never exposed to the frontend. All secrets are injected as environment variables at runtime and validated at application startup.

Database Connection Security

TLSRequired (ssl='require' in connect_args)
Connection Pool5 connections, 10 max overflow, 30s timeout, 1,800s recycle
Pre-PingEnabled — validates connections before use to detect stale handles
Statement Timeout5 seconds — prevents runaway queries from blocking the pool
Slow Query LoggingWarning at ≥500ms, error at ≥2,000ms
Credential ScrubbingRegex scan removes postgresql://[password]@ from all log output

8. Rate Limiting & DoS Protection

Multi-tier rate limiting with per-category isolation. Each category has its own lock to prevent cross-category blocking (e.g., a chat flood cannot block authentication rate checks).

CategoryLimitWindowScopeStorage
Authentication15 req60sPer IPIn-memory + PostgreSQL
Chat (LLM inference)60 req60sPer userIn-memory
Read (list/browse)300 req60sPer userIn-memory
Email send10 req60sPer userIn-memory
Admin writes10 req60sPer userIn-memory

IP Normalization

IPv4-mapped IPv6 addresses (e.g., ::ffff:1.2.3.4) are normalized to their IPv4 form to prevent rate-limit bypass via address format tricks. Behind a reverse proxy, the rightmost IP from X-Forwarded-For is used (trusted proxy appends the real client IP last; leftmost values are attacker-controllable).

Memory Exhaustion Protection

Rate limit tracking dictionaries are capped at 100,000 keys. When exceeded, a forced cleanup evicts stale entries. This prevents memory exhaustion from a botnet hammering unique IPs. Authentication rate limits are additionally backed by PostgreSQL (atomic UPSERT), surviving worker restarts and shared across process instances.

Additional DoS Controls

Request Body Limit10 MB — enforced at ASGI level by counting actual bytes during streaming (not just Content-Length header), defeating chunked-encoding bypass
Concurrent Streams5 per user max — prevents a single user from monopolizing LLM inference capacity
Stream Duration600 seconds absolute cap per SSE stream
Audit Offset Cap100,000 max — prevents full-table-scan DoS via unbounded OFFSET

9. Input Validation

Chat Request Validation

Messages1–100 messages per request
Message Content1–32,000 characters per message
Role EnforcementRegex-validated to 'user' or 'assistant' only — prevents system role injection
Conversation IDUUID format validated via regex
AttachmentsMax 20 per request, 255-char filename, 200,000-char extracted text
File Upload20 MB max, extension allowlist enforced, streamed with running byte counter
Model TierEnum: 'fast', 'standard', 'deep'

Content Security

  • Document poisoning detection: All uploaded documents are scanned for prompt injection patterns, encoded payloads (base64, hex, percent-encoded), and suspicious metadata before processing.
  • Text sanitization: Document text is sanitized before inclusion in LLM prompts — stripping control characters, Unicode direction overrides, and known injection patterns.
  • Encoding exfiltration defense: LLM responses are scanned for encoded payloads; detected segments are redacted before reaching the user, with a security audit event logged.
  • ZIP bomb protection: DOCX and XLSX files have decompressed size validated (100 MB max) before parsing begins.
  • Path traversal prevention: All Egnyte document paths are validated against the configured base path. Invalid paths are rejected before any API call is made.

SQL Injection Prevention

All database queries use SQLAlchemy's parameterized query builder — no raw string interpolation. The ORM layer ensures values are always bound as parameters, never concatenated into SQL.

10. Security Headers

Applied on every response via nginx, including error responses:

Strict-Transport-Securitymax-age=63072000; includeSubDomains; preload
Content-Security-Policydefault-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'; object-src 'none'
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(self), geolocation=(), payment=(), usb=(), interest-cohort=()
X-Permitted-Cross-Domain-Policiesnone

CORS

Allowed OriginsExplicit HTTPS-only allowlist (no wildcards in production)
Credentialstrue (session cookies sent cross-origin)
MethodsGET, POST, PATCH, DELETE, OPTIONS
HeadersAuthorization, Content-Type
Wildcard ProtectionIf '*' is present in config AND credentials=true, wildcard is stripped at startup to prevent any-origin cookie access
Error ResponsesCORS headers are applied even on 413/500/502 error responses (EnsureCorsOnErrorMiddleware) so browsers can read error messages

Host Header Validation

TrustedHostMiddleware validates the Host header against a whitelist derived from CORS origins. This prevents Host header injection in OAuth redirect URIs and cache-poisoning attacks.

11. Audit Trail

Append-only audit_logs table. The application layer never issues UPDATE or DELETE against this table.

Recorded Events

  • auth.login / auth.login_failed / auth.logout / auth.logout_all
  • auth.login_via_azure / auth.profile_updated_from_azure
  • role_change / admin_account_access / admin_account_disabled
  • permission_denied (user ID, requested permission, correlation ID)
  • chat_query (user, query text, documents accessed, duration)
  • document_ingest / document_access
  • email_token_decryption_failed (forensic trail for token compromise investigation)
  • session_revoked / encoding_redaction

Correlation IDs

Every request is assigned a UUID correlation ID (from the X-Correlation-ID header if present and valid, or auto-generated). The ID is returned in the response header and attached to all audit events, enabling end-to-end request tracing. Correlation IDs are UUID-format-validated to prevent log injection via control characters.

Integrity & Resilience

Column TruncationAction (64 chars), query (2,000 chars), response summary (500 chars) — truncated at source to prevent database IntegrityErrors that would silently lose audit events
DB Unavailability FallbackIf the database insert fails, the event is written to structured stderr at ERROR level with sanitized content (newlines stripped to prevent log injection)
Query LimitsMax 500 results per query, offset capped at 100,000 to prevent full-table-scan DoS
Newline SanitizationAll audit fallback log messages have newlines replaced to prevent log injection attacks

12. Vulnerability Management

All identified vulnerabilities are tracked in-code with unique IDs (e.g., VULN-971) and mitigated at the source. The codebase currently tracks 30+ identified and mitigated vulnerabilities covering:

  • Timing side-channel attacks (HMAC verification, admin email comparison, account enumeration)
  • Session management (revocation, sliding refresh, cookie signing)
  • SSRF prevention (tenant ID validation, JWKS URL domain checks, issuer whitelisting)
  • Memory exhaustion (rate limit dictionary caps, request body streaming, connection pool limits)
  • Log injection (correlation ID format validation, newline sanitization, credential scrubbing)
  • Content injection (document poisoning detection, prompt delimiter stripping, encoding redaction)
  • Race conditions (thread-safe JWKS cache, session key derivation locks, row-level DB locking)
  • Information disclosure (stack trace hiding, validation error sanitization, credential scrubbing in logs)

Startup Fail-Fast Validation

The application performs comprehensive security validation at startup and refuses to run if critical configuration is invalid:

  • SECRET_KEY must be non-empty, non-default, and ≥32 characters
  • SECURE_COOKIES=true is required when using PostgreSQL
  • Cloudflare Access credentials are required when the LLM URL is production
  • Wildcard CORS origins are stripped in production (prevents cookie leakage)
  • Non-HTTPS CORS origins are filtered out when secure cookies are enabled
  • Trusted hosts are auto-derived from CORS origins if not explicitly set
  • JWT algorithm is locked to HS256 — any environment override is rejected

13. Responsible Disclosure

If you discover a security vulnerability, please report it responsibly by emailing security@sevrel.com. We take all reports seriously and will respond within 48 hours. Please include reproduction steps, affected endpoints, and potential impact assessment where possible.