Skip to content

Security Architecture

This guide documents the Arbitex security architecture from authentication through audit trail. It covers the complete request lifecycle, token formats, session management, middleware ordering, RBAC enforcement, and tamper-evident audit logging.

For the security trust center overview, see Security Trust Center. For MFA and passkey admin operations, see Platform Administration.


Arbitex supports four authentication methods: username/password with optional MFA, SAML 2.0 SSO, WebAuthn/passkeys, and OAuth 2.0 M2M.

Client Platform
│ │
├── POST /api/auth/login ────────────►│
│ {email, password} │
│ ├── Verify password (bcrypt)
│ ├── Check user.is_active
│ │
│ ┌─── MFA enabled? ───────────────┤
│ │ │
│ YES NO
│ │ │
│ ├── {mfa_required: true, ├── Create access token
│ │ mfa_token: <5-min JWT>} ├── Create refresh token
│ │ ├── Create session
│ │ ├── Enforce session limit
│ ├── POST /api/auth/mfa/verify ──►│├── Audit: auth.login_success
│ │ {mfa_token, code} │
│ │ ├── Verify TOTP or backup code
│ │ ├── Issue tokens (mfa_verified=true)
│ │ ├── Create session
│ │ ├── Enforce session limit
│◄──┤ │
│ {access_token, refresh_token} │

Anti-enumeration: Invalid email and wrong password return identical "Invalid credentials" responses. Inactive accounts return the same error. Password verification uses bcrypt with constant-time comparison.

Password requirements: Minimum 8 characters, at least one uppercase letter, at least one digit.

Client Platform IdP
│ │ │
├── GET /saml/login ────────►│ │
│ ?idp_id=<uuid> ├── Generate AuthnRequest │
│ ├── Store CSRF token (Redis, 10-min TTL)
│◄── 302 Redirect ──────────┤ │
│ │ │
├── (IdP login flow) ───────────────────────────────────►│
│ │ │
│◄── POST /saml/acs ────────────────────────────────────┤
│ SAMLResponse + RelayState│ │
│ ├── Validate CSRF (single-use)
│ ├── Validate signature (x509)
│ ├── Extract attributes (email, username, groups)
│ ├── JIT user provisioning
│ ├── Sync group memberships
│ ├── Issue JWT tokens
│◄── {access_token, ...} ───┤ │

JIT provisioning: Users are created on first SAML login with a random password (SSO-only accounts). The user’s saml_name_id and saml_session_index are persisted for Single Logout.

Group sync: SAML groups attribute values are matched against Group.name or Group.external_group_id. New memberships are added; stale memberships are removed only for groups with an external_group_id set. Platform-only groups (no external ID) are never removed by SAML sync.

CSRF protection: A random state token is stored in Redis with a 10-minute TTL and passed as RelayState. The ACS endpoint validates and consumes the token (single-use). If Redis is unavailable, the login flow fails closed.

SP metadata: Available at GET /api/auth/saml/metadata — provides entity ID, ACS URL, SLO URL, and NameID format for IdP configuration.

WebAuthn provides passwordless authentication using FIDO2 hardware keys or platform authenticators.

Registration:

  1. POST /api/auth/webauthn/register/begin — Returns PublicKeyCredentialCreationOptions with a challenge stored server-side.
  2. User authenticates with their device authenticator.
  3. POST /api/auth/webauthn/register/complete — Verifies attestation, stores credential. Audit: auth.webauthn_registered.

Authentication:

  1. POST /api/auth/webauthn/authenticate/begin — Returns PublicKeyCredentialRequestOptions with user’s registered credentials.
  2. User authenticates with their device.
  3. POST /api/auth/webauthn/authenticate/complete — Verifies assertion, issues tokens with mfa_verified=true (hardware key counts as MFA). Audit: auth.webauthn_login.

Configuration: WEBAUTHN_RP_ID (relying party identifier) and WEBAUTHN_RP_ORIGIN (expected origin URL).

Machine-to-machine authentication for programmatic API access. Two grant types:

Client credentials (RFC 6749 §4.4):

Terminal window
curl -X POST https://platform.arbitex.ai/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d "grant_type=client_credentials&scope=audit:read dlp:read"

Client secrets are stored as SHA-256 hashes. A grace period after rotation allows the previous secret to remain valid.

Authorization code with PKCE (RFC 6749 §4.1 + RFC 7636):

Public clients must provide code_verifier; the server verifies base64url(sha256(verifier)) == stored_challenge. Authorization codes are single-use with a 60-second TTL, stored in Redis.


Token type claim Algorithm Expiry Purpose
Access access HS256 Configurable (default 60 min) API access
Refresh refresh HS256 7 days Token rotation
MFA challenge mfa_challenge HS256 5 minutes Login MFA step
M2M OAuth m2m RS256 Per-client (max 86400s) Machine-to-machine
{
"sub": "user-uuid",
"role": "admin",
"jti": "unique-token-id",
"type": "access",
"iat": 1710500000,
"exp": 1710503600,
"mfa_verified": true,
"passkey_enrollment_required": false
}
Claim Type Description
sub string User UUID
role string admin or user
jti string Unique token identifier (UUID v4) for revocation
type string Token type discriminator
mfa_verified boolean Present and true if MFA was completed
passkey_enrollment_required boolean Present and true if org requires passkey but user has none
{
"sub": "client-uuid",
"client_id": "client-uuid",
"tenant_id": "org-uuid",
"scope": "audit:read dlp:read",
"token_type": "m2m",
"rate_limit_tier": "standard",
"iat": 1710500000,
"exp": 1710503600
}

M2M tokens include a kid header referencing the RSA key used for signing. The JWKS endpoint at GET /.well-known/jwks.json publishes current and previous public keys for external verification.

Token type Algorithm Key source
User / refresh / MFA HS256 JWT_SECRET_KEY env var
M2M OAuth RS256 (default) OAUTH_JWT_PRIVATE_KEY env var (PEM or file path)

Production guard: The platform refuses to start in non-debug mode if JWT_SECRET_KEY equals the default placeholder "CHANGE-ME-IN-PRODUCTION".

RSA key rotation: The rsa_key_manager supports a current keypair and a previous keypair. Both appear in the JWKS response. Token verification checks the kid header and falls back to the previous key during rotation.

POST /api/auth/refresh
Authorization: Bearer <refresh_token>
  1. Decode refresh token, verify type == "refresh".
  2. Check bloom filter + database blacklist for jti.
  3. Verify user exists and is active.
  4. Blacklist the old refresh token (single-use rotation).
  5. Issue new access + refresh token pair.

Refresh preserves mfa_verified from the old token if the user still has MFA enabled.

Revocation uses a two-tier system for performance:

┌──────────────┐
Is token revoked? ──►│ Bloom filter │ O(1) memory check
└──────┬───────┘
Not in filter → Token is valid (fast path)
In filter ↓
┌──────────────┐
│ Database │ Confirm (eliminates false positives)
│ blacklist │
└──────────────┘

Bloom filter parameters:

Setting Default Description
bloom_filter_expected_items 10,000 Expected token count
bloom_filter_fp_rate 0.001 Target false positive rate
bloom_filter_sync_interval 300s Database rebuild interval

The bloom filter provides O(1) fast-path rejection for the common case (token is not blacklisted). False positives fall through to a database confirmation query.


Each authentication creates a session record linked to the JWT via token_jti:

Field Type Description
id UUID Session identifier
user_id UUID Owner
token_jti string Links session to the JWT’s jti claim
ip_address INET Client IP at session creation
user_agent string Client User-Agent header (max 512 chars)
is_active boolean Whether session is currently valid
created_at timestamp Session creation time
last_activity timestamp Last observed activity
expires_at timestamp Expiration time (matches token expiry)

Configured via SESSION_STORE_URL:

Backend Setting Description
In-memory (empty/unset) Per-process dict with TTL. Not cluster-safe — use only for single-instance deployments
Redis redis://host:port/db Redis-backed with session:{id} keys and session_count:{user_id} counters. Required for horizontal scaling

Default maximum: 5 concurrent sessions per user. Configurable via the max_concurrent_sessions system config key.

When a new login would exceed the limit:

  1. All active sessions for the user are fetched, ordered by created_at ascending.
  2. The oldest excess sessions are evicted: is_active set to false, JWT blacklisted, audit event auth.session_evicted recorded.
  3. The new session is created normally.

The session store caches counts to avoid per-login database queries. The cache is invalidated when sessions are created or destroyed.

Admins can view and terminate any user’s sessions:

  • GET /api/v1/admin/sessions — List active sessions (filter by user_id).
  • DELETE /api/v1/admin/sessions/{session_id} — Force-logout a single session. Blacklists the JWT. Audit: auth.session_force_logout.
  • DELETE /api/v1/admin/users/{user_id}/sessions — Force-logout all sessions for a user.

The Cloud Portal maintains its own session tracking in Redis, separate from the Platform:

Setting Default Description
SSO_SESSION_TIMEOUT 8 hours Inactivity timeout
SSO_MAX_CONCURRENT_SESSIONS 5 Maximum per-user sessions

Cloud sessions use Redis sorted sets (sso:sessions:{user_id}) and TTL keys (sso:activity:{jti}) for activity tracking. Inactivity causes expiry via Redis TTL mechanics.


Middleware processes every request in a defined order. Listed outermost (first to execute) to innermost (last before route handler):

Order Middleware Purpose
1 MetricsMiddleware Total latency capture, W3C trace context propagation
2 MTLSMiddleware Client certificate validation on /v1/internal/* routes
3 MtlsCertHeaderMiddleware Strip/inject X-SSL-Client-Cert header; trust only from proxy CIDRs
4 EmergencyMiddleware Redis-backed emergency controls — org isolation, feature kill switches, credential revocation, outpost disconnection
5 SecurityHeadersMiddleware Inject security response headers (see below)
6 UsageThrottleMiddleware Org plan consumption check; 429 when usage limit reached
7 RateLimitMiddleware Sliding window rate limiter (per-user JWT, per-IP, per-email, or per-OAuth-client)
8 PasskeyEnforcementMiddleware Restrict access for tokens with passkey_enrollment_required
9 MfaEnforcementMiddleware Enforce org-level MFA on sensitive endpoints
10 IPAllowlistMiddleware Block requests from IPs not on the org allowlist
11 RequestIDMiddleware Assign X-Request-ID header
12 ContentTypeMiddleware Reject invalid content types (415)
13 PayloadSizeMiddleware Reject oversized request bodies (413); max 1 MB default
14 CORSMiddleware CORS handling; configured origins, exposes X-Request-ID

The EmergencyMiddleware checks Redis-backed emergency keys on every inbound HTTP request, returning 503 Service Unavailable with a Retry-After: 30 header when any key is active. It executes early in the stack (position 4) — after mTLS validation but before rate limiting — so emergency controls take effect before any business logic.

Key families checked:

Redis Key Pattern Scope
emergency:org_isolated:{org_id} Per-org isolation
emergency:credentials_revoked:{org_id} Per-org credential revocation
emergency:killswitch:{feature} Per-feature shutdown (chat, admin, dlp, mcp, oauth, scim, saml)
emergency:outpost_disconnected:{outpost_id} Per-outpost disconnection

Redis reads are cached in-process with a 5-second TTL. If Redis is unavailable, the middleware fails open (allows requests). The emergency_enforcement.enabled system config key can disable all checks at runtime.

Health probes (/internal/health*, /internal/metrics), API docs (/docs, /openapi.json, /redoc) are exempt from emergency checks.

For the full emergency controls API reference, see Emergency Controls API.

The SecurityHeadersMiddleware injects these headers on every response:

Header Value
Strict-Transport-Security max-age=31536000; includeSubDomains; preload
X-Frame-Options DENY
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy camera=(), microphone=(), geolocation()
Content-Security-Policy Restrictive policy (configurable via SECURITY_CSP_POLICY)

HSTS can be disabled via security_hsts_enabled system config key for environments behind a reverse proxy that manages its own HSTS.

The rate limiter uses a sliding window algorithm backed by Redis sorted sets for multi-process consistency. When Redis is unavailable in production, rate limiting fails closed (rejects). Non-production environments fall back to in-memory counters.

Per-path tier buckets:

Path RPM Limit Key Type
/api/v1/auth/login 10 per-email + 20 per-IP backstop Dual-bucket (see below)
/api/v1/auth/register 10 Per-IP
/api/v1/auth/refresh 30 Per-IP
/api/v1/conversations/shared/ 30 Per-user (JWT)
POST /api/v1/conversations/*/messages 60 Per-user (JWT)
POST /api/v1/admin/dlp-rules/test 10 Per-user (JWT)
General default 60 Per-user or per-IP

Login dual-bucket architecture: The /api/v1/auth/login endpoint is protected by two sequential rate limit checks:

  1. Primary — per-email bucket (key: login:{email}, 10 RPM): The middleware buffers and parses the request body to extract the email field, then checks this bucket first. This prevents brute-force attacks against a single account.
  2. Secondary — per-IP backstop (20 RPM): If the per-email check passes, the per-IP check catches credential-stuffing bots rotating through many email addresses from one IP.

All /api/v1/auth/* paths force IP-based keying regardless of whether a JWT is present, preventing authenticated users from bypassing auth-endpoint rate limits.

Identity resolution:

Scope Key pattern Default limit
Authenticated user user:{user_id} 60 RPM (standard), 600 RPM (admin)
Unauthenticated ip:{client_ip} 60 RPM
M2M OAuth oauth:{client_id} Per-tier (see below)
Enterprise override user:{user_id} Custom RPM from enterprise_entitlements (300 s cache)

OAuth M2M client tiers:

Tier Claim RPM
standard 1,000
premium 5,000
unlimited Exempt

M2M requests are keyed by oauth:{client_id} — completely isolated from user buckets. Unknown tier values default to 1,000 RPM. Requests on /api/v1/oauth/* paths receive RFC 6749-compliant error responses on 429.

Usage throttle integration: When the UsageThrottleMiddleware signals that an org is in the 95–100% quota band, the effective rate limit is multiplied by 0.5 (configurable via USAGE_THROTTLE_RPM_FACTOR), reducing throughput by 50% before the hard usage cap triggers a full 429.

Admin users receive elevated rate limits (600 RPM default). Full admin bypass is configurable via rate_limit_admin_exempt system config. Tier configuration is overridable at runtime via RATE_LIMIT_TIERS (JSON environment variable).

The MFA enforcement middleware checks the org’s OrgMfaPolicy.enforcement_level and blocks access to sensitive endpoints when MFA is required but mfa_verified is not in the JWT.

Sensitive endpoint prefixes:

  • /api/admin — All admin operations
  • /api/keys — API key management
  • /api/saml-admin — SAML configuration
  • /api/policy — Policy management
  • /api/auth/mfa/setup — MFA setup
  • /api/auth/mfa/disable — MFA disable

The middleware caches org MFA policy with a 60-second TTL to avoid per-request database lookups. On cache miss or error, it fails open (allows the request).

The /v1/internal/* route prefix is protected by mutual TLS. The MTLSMiddleware validates client certificates against a CA certificate bundle (CLOUD_CA_CERT_PATH or MTLS_CA_BUNDLE). This secures platform-to-cloud communication including the SSO exchange flow.


The platform implements a two-role model:

Role Access
admin Full access to all admin endpoints and user data within the tenant
user Standard access to own data only

Role is stored in users.role and encoded in the JWT role claim. All admin endpoints use a require_admin dependency that returns HTTP 403 if role != "admin".

Users cannot change their own role. Self-deactivation is also prevented.

Groups extend the role model with fine-grained permissions:

Control Scope Description
DLP overrides Per-group Override detector action/enabled per group (GroupDLPConfig)
Model access Per-group Allow/deny specific models per group (GroupModelAccess)
Compliance bundles Per-group Assign compliance policy bundles
Quotas Per-group Token/request limits for aggregate group usage
Policy conditions Per-rule Policy rules can target user_groups as a condition

Groups are tenant-scoped and optionally linked to IdP groups via external_group_id for SAML/SCIM sync.

The Cloud Portal uses a five-role model for org-scoped access:

Role Access
org_admin Full access to all portal endpoints
dlp_viewer Read-only DLP data
audit_viewer Read-only audit logs
billing_viewer Read-only billing data
outpost_viewer Read-only outpost data

Portal API keys use scope-based authorization:

Scope Access
dlp:read Read DLP data
dlp:write Modify DLP configuration
audit:read Read audit logs
webhook:write Manage webhooks
admin Full access

API keys are prefixed with arb_ and checked via X-API-Key header. When both API key and Bearer JWT are present, API key takes precedence.


Every significant platform event is recorded in the audit_logs table:

Field Type Description
id UUID Event identifier
user_id UUID Acting user (null for system events)
tenant_id UUID Organization scope
conversation_id UUID Associated conversation (if applicable)
action string Event type (see action reference below)
model_id string AI model involved (if applicable)
provider string AI provider (if applicable)
prompt_text text Request text (if applicable)
response_text text Response text (if applicable)
token_count_input integer Input tokens consumed
token_count_output integer Output tokens consumed
cost_estimate numeric Estimated cost
latency_ms integer Request latency
extra_metadata JSONB Event-specific metadata
hmac string HMAC-SHA256 of event content
previous_hmac string Previous event’s HMAC (chain link)
hmac_key_id string Key version identifier
src_ip / dst_ip INET Network addresses (included in HMAC)
source string "outpost" when forwarded from Hybrid Outpost
outpost_id UUID Originating outpost (if applicable)
created_at timestamp Event timestamp

GeoIP enrichment fields (NOT included in HMAC chain — dataset-dependent):

src_country_code, src_country_name, src_region, src_city, src_isp, src_asn, src_asn_org, dst_country_code, dst_asn, dst_asn_org.

Credential intelligence fields: credint_enabled, credint_hit, frequency_bucket, context_type, sha1_prefix, credint_confidence.

Action Description Metadata
auth.login_success Successful password login
auth.login_failed Failed login attempt
auth.logout User logout
auth.mfa_enabled User enabled MFA
auth.mfa_disabled MFA disabled (user or admin) admin_id if admin-initiated
auth.mfa_verify_success MFA code verified
auth.mfa_verify_failed MFA code rejected
auth.webauthn_registered Passkey registered
auth.webauthn_login Passkey authentication
auth.webauthn_revoked Passkey revoked by admin
auth.session_evicted Session evicted (concurrent limit)
auth.session_force_logout Admin force-logout
Action Description
sso_code_issued SSO authorization code generated
sso_exchange_success SSO code exchanged for identity
sso_exchange_failed SSO exchange failed (invalid code)
sso_exchange_expired SSO code expired
sso_exchange_replay SSO code reuse detected
Action Description Metadata
config_changed System configuration updated key, old_value, new_value
user_invite_created User invitation sent email, invite_id
kill_switch_disable Model/provider disabled model_config_id, reason, scope
kill_switch_enable Model/provider re-enabled model_config_id, scope

When AUDIT_HMAC_KEY is set, every audit event is HMAC-signed and chained:

  1. HMACChain.chain_event(event) computes HMAC-SHA256 over event content fields (including src_ip and dst_ip as observed network facts).
  2. The computed HMAC is stored as event.hmac.
  3. The previous event’s HMAC is stored as event.previous_hmac, forming a tamper-evident chain.
  4. hmac_key_id tracks key version for rotation support.
  5. GeoIP enrichment fields are excluded from the HMAC calculation (dataset-version-dependent, not observed facts).

A dead-letter queue (1,000 events max) buffers events when audit writes fail.

For HMAC chain verification procedures, see Audit Log.

Configured via AUDIT_SINKS env var (comma-separated):

Sink Description Configuration
db PostgreSQL audit_logs table (required) Always enabled
jsonl Append to local JSONL file AUDIT_JSONL_PATH (default: /var/log/arbitex/audit.jsonl)
webhook HTTP POST to SIEM endpoint AUDIT_WEBHOOK_URL, AUDIT_WEBHOOK_TOKEN
splunk_hec OCSF-formatted events via Splunk HEC SPLUNK_HEC_URL, SPLUNK_HEC_TOKEN

The JSONL sink writes in an executor thread (fire-and-forget) for Splunk Universal Forwarder or Filebeat collection. The webhook sink sends Splunk HEC-compatible payloads with sourcetype: "arbitex:audit".


Arbitex enforces seven discrete security layers. Each layer is independently enforceable; a failure or bypass in one layer does not collapse the next.

graph TD
    CLIENT([Client])
    L1["Layer 1 — Authentication\nJWT RS256 · WebAuthn · SAML · PKCE"]
    L2["Layer 2 — Authorization\nRBAC · API key · OAuth scopes"]
    L3["Layer 3 — Data Protection\nTLS · Fernet AES · HMAC audit chain"]
    L4["Layer 4 — DLP Security\nScan pipeline · Redaction · Signed bundles"]
    L5["Layer 5 — Network Security\nmTLS · Rate limiting · IP allowlist · GeoIP · SSRF"]
    L6["Layer 6 — Secrets Management\nPluggable backends · Azure Key Vault · Redis stores"]
    L7["Layer 7 — Supply Chain\nSBOM · License compliance · Signed policy bundles"]

    CLIENT --> L1 --> L2 --> L3 --> L4 --> L5
    L6 -.->|injects secrets| L1
    L6 -.->|injects secrets| L3
    L7 -.->|validates dependencies| L4
Layer Primary threat Failure mode
Authentication Identity spoofing, token forgery Fail-closed: 401 on any verification failure
Authorization Privilege escalation, cross-tenant access Fail-closed: 403 on missing role or scope
Data protection Data exposure at rest or in transit Fail-closed: empty credential on decrypt failure; BYOK writes fail-closed, reads fail-open
DLP security Sensitive data exfiltration via prompts Fail-closed: block on bundle validation failure
Network security Lateral movement, SSRF, abuse Fail-closed: 403 on allowlist miss; SSRF blocked before DNS resolution
Secrets management Credential exposure, secret sprawl Mixed: sessions fail-open; PKCE fail-closed
Supply chain Dependency compromise, license risk Sprint-gate on HIGH/CRITICAL CVEs
Container security Container escape, privilege escalation Read-only FS, dropped capabilities, non-root user

Organizations that require customer-managed encryption keys (BYOK/CMEK) can configure a per-org KMS provider. When enabled, audit log fields are encrypted using envelope encryption before being stored.

The KMSProvider abstract base class defines the envelope encryption contract:

Method Purpose
encrypt(plaintext) Encrypt data and return (ciphertext, wrapped_dek)
decrypt(ciphertext, wrapped_dek) Decrypt data using a previously wrapped DEK
generate_dek() Generate a new Data Encryption Key, return (plaintext_dek, wrapped_dek)
validate_connection() Test KMS reachability and permissions

Supported backends:

Backend SECRETS_BACKEND value Notes
mock Development and testing only
vault vault Azure Key Vault — managed identity / service principal
aws_kms AWS KMS — IAM / access key
  1. Generate: The platform calls generate_dek() on the KMS. The KMS returns a plaintext DEK and a wrapped (encrypted) copy.
  2. Encrypt locally: Audit fields are encrypted with the plaintext DEK using AES-256-GCM (12-byte nonce, nonce-prefixed ciphertext).
  3. Store: The ciphertext and wrapped DEK are stored in the audit_logs table. The plaintext DEK is cached in memory — never persisted.
  4. Decrypt: To read encrypted data, the platform unwraps the DEK via the KMS, then decrypts locally.
Parameter Value
Cache TTL 1 hour (_DEK_CACHE_TTL_SECONDS = 3600)
Cache capacity 256 orgs (_DEK_CACHE_MAX_SIZE = 256)
Eviction policy LRU
Operation Failure behavior Rationale
Write (encrypt) Fail-closed: AuditEncryptionError raised, audit event not stored Losing an audit entry silently would create a gap in the tamper-evident chain
Read (decrypt) Fail-open: returns "[encrypted - KMS unavailable]" placeholder Operators can still view audit metadata; sensitive fields show placeholder
SIEM export Fail-open: placeholder text forwarded KMS unavailability does not block SIEM delivery

All Arbitex containers enforce a consistent security baseline:

Control Implementation
Read-only root filesystem read_only: true in Docker Compose; readOnlyRootFilesystem: true in Kubernetes
Dropped capabilities cap_drop: [ALL] — no Linux capabilities granted to application containers
No privilege escalation no-new-privileges security option; allowPrivilegeEscalation: false in Kubernetes
Non-root user Backend runs as UID 1001 (appuser), Outpost as UID 1001 (arbitex), Kubernetes as UID 65532 (nonroot)
Multi-stage builds Build tools and compilers confined to builder stages and never reach the runtime image
Explicit COPY Dockerfiles use explicit file copies instead of COPY . . to prevent test files or secrets from leaking into images
Seccomp profile RuntimeDefault in Kubernetes
Log rotation json-file driver with max-size: 10m and max-file: 3-5 on all containers
Network isolation Two-tier Docker networks: backend-net (internal) and frontend-net (API proxy only)
Port binding Admin ports (8301) bound to 127.0.0.1 — never exposed to the network

Writable paths are limited to explicit tmpfs mounts (/tmp, /app/.cache) and named volumes for persistent data. Application code is owned by root:root and not writable by the runtime user.

Performance-critical modules are compiled to native shared libraries (.so) via Rust/PyO3. Customers receive compiled binaries, not source code.

Module Rust benefit
DLP regex scanner Aho-Corasick multi-pattern matching — 10-50x throughput over Python re
Policy engine Eliminates Python interpreter overhead on the hot evaluation path
HMAC chain Constant-time HMAC via ring library

The DLP_SCANNER_BACKEND environment variable controls whether the rust or python backend is used. Production Dockerfiles default to rust.


SBOMs are generated in CycloneDX format for both Python and Node.js dependency trees at sprint close. The CycloneDX format includes package identifiers, versions, hashes, and license information for automated vulnerability correlation.

Retrieve the SBOM for a container image:

Terminal window
cosign verify-attestation --type cyclonedx \
arbitex/platform:latest | jq -r '.payload' | base64 -d
License Type
MIT, Apache 2.0, BSD, ISC, PSF, Unlicense, BlueOak, 0BSD Permissive
MPL 2.0 Weak copyleft (dynamic linking)
GPL, AGPL, SSPL Prohibited

All production container images are signed using Sigstore cosign with keyless (OIDC-based) identity. Verify signatures before deployment:

Terminal window
cosign verify --certificate-identity-regexp='.*' --certificate-oidc-issuer-regexp='.*' \
arbitex/platform:latest

Keyless signing means no long-lived signing keys to manage — the signing identity is derived from the CI environment’s OIDC token, and the Rekor transparency log provides a public audit trail.

Severity Policy
HIGH / CRITICAL Must be resolved before release
MODERATE / LOW Tracked and resolved in subsequent releases

The Platform and Cloud Portal share authentication via an internal SSO exchange flow protected by mTLS:

Cloud Portal Platform (mTLS)
│ │
├── POST /v1/internal/sso/authorize ──►│
│ {client_id, redirect_uri, │ ├── Validate mTLS cert
│ user claims} │ ├── Generate auth code (60s TTL)
│ │ ├── Store SHA-256(code) in Redis
│◄── {code} ──────────────────┤ │
│ │ │
├── POST /v1/internal/sso/exchange ──►│
│ {code, client_id, │ ├── Validate: single-use, not expired
│ redirect_uri} │ ├── Verify client_id + redirect_uri binding
│ │ ├── Return user identity claims
│◄── {user_id, email, │ │
│ username, role, │ │
│ mfa_verified} ──────────┤ │

The exchange is restricted to registered clients (currently: cloud-portal). Auth codes are single-use — the Redis key is atomically deleted on exchange.


Variable Purpose Default
JWT_SECRET_KEY HS256 signing key for user tokens (blocked in production if unchanged)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES Access token lifetime 60
REFRESH_TOKEN_EXPIRE_DAYS Refresh token lifetime 7
OAUTH_JWT_PRIVATE_KEY RSA private key for M2M tokens (ephemeral if unset — not cluster-safe)
OAUTH_JWT_ALGORITHM M2M token algorithm RS256
AUDIT_HMAC_KEY HMAC chain key (chaining disabled if empty)
AUDIT_SINKS Active audit sinks db
CLOUD_CA_CERT_PATH CA cert for mTLS (mTLS disabled if empty)
RATE_LIMIT_REQUESTS_PER_MINUTE Standard user rate limit 60
SESSION_STORE_URL Redis URL for sessions (in-memory if empty)
BLOOM_FILTER_EXPECTED_ITEMS Token blacklist bloom filter size 10,000
BLOOM_FILTER_FP_RATE False positive rate 0.001
BLOOM_FILTER_SYNC_INTERVAL Database rebuild interval 300s
WEBAUTHN_RP_ID WebAuthn relying party ID localhost
WEBAUTHN_RP_ORIGIN WebAuthn expected origin http://localhost:5173
SECURITY_HSTS_ENABLED HSTS header true
OAUTH_JWT_PRIVATE_KEY_ID Key ID for JWKS SHA-256 thumbprint
OAUTH_JWT_PREVIOUS_PRIVATE_KEY Previous key for rotation window
AUDIT_HMAC_KEY_ID Key version identifier default
MAX_REQUEST_BODY_BYTES Payload size limit 1,048,576 (1 MB)
RATE_LIMIT_ADMIN_EXEMPT Exempt admin users from rate limits false
RATE_LIMIT_ADMIN_RPM Admin user rate limit 600
SECURITY_CSP_POLICY Content-Security-Policy header value default-src 'self'; script-src 'self'
MTLS_CA_BUNDLE PEM bundle for mTLS CA validation
IP_ALLOWLIST_BYPASS_CIDRS Platform-level CIDR bypass for IP allowlist