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.
Authentication Flow
Section titled “Authentication Flow”Arbitex supports four authentication methods: username/password with optional MFA, SAML 2.0 SSO, WebAuthn/passkeys, and OAuth 2.0 M2M.
Password authentication
Section titled “Password authentication”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.
SAML 2.0 SSO
Section titled “SAML 2.0 SSO”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 / Passkeys
Section titled “WebAuthn / Passkeys”WebAuthn provides passwordless authentication using FIDO2 hardware keys or platform authenticators.
Registration:
POST /api/auth/webauthn/register/begin— ReturnsPublicKeyCredentialCreationOptionswith a challenge stored server-side.- User authenticates with their device authenticator.
POST /api/auth/webauthn/register/complete— Verifies attestation, stores credential. Audit:auth.webauthn_registered.
Authentication:
POST /api/auth/webauthn/authenticate/begin— ReturnsPublicKeyCredentialRequestOptionswith user’s registered credentials.- User authenticates with their device.
POST /api/auth/webauthn/authenticate/complete— Verifies assertion, issues tokens withmfa_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).
OAuth 2.0 M2M
Section titled “OAuth 2.0 M2M”Machine-to-machine authentication for programmatic API access. Two grant types:
Client credentials (RFC 6749 §4.4):
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.
JWT Lifecycle
Section titled “JWT Lifecycle”Token types
Section titled “Token types”| 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 |
Access token claims
Section titled “Access token claims”{ "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 |
M2M token claims
Section titled “M2M token claims”{ "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.
Signing keys
Section titled “Signing keys”| 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.
Token refresh
Section titled “Token refresh”POST /api/auth/refreshAuthorization: Bearer <refresh_token>- Decode refresh token, verify
type == "refresh". - Check bloom filter + database blacklist for
jti. - Verify user exists and is active.
- Blacklist the old refresh token (single-use rotation).
- Issue new access + refresh token pair.
Refresh preserves mfa_verified from the old token if the user still has MFA enabled.
Token revocation
Section titled “Token revocation”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.
Session Management
Section titled “Session Management”Session model
Section titled “Session model”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) |
Session store backends
Section titled “Session store backends”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 |
Concurrent session limit
Section titled “Concurrent session limit”Default maximum: 5 concurrent sessions per user. Configurable via the max_concurrent_sessions system config key.
When a new login would exceed the limit:
- All active sessions for the user are fetched, ordered by
created_atascending. - The oldest excess sessions are evicted:
is_activeset tofalse, JWT blacklisted, audit eventauth.session_evictedrecorded. - 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.
Admin session management
Section titled “Admin session management”Admins can view and terminate any user’s sessions:
GET /api/v1/admin/sessions— List active sessions (filter byuser_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.
Cloud Portal sessions
Section titled “Cloud Portal sessions”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 Stack
Section titled “Middleware Stack”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 |
Emergency controls
Section titled “Emergency controls”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.
Security headers
Section titled “Security headers”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.
Rate limiting
Section titled “Rate limiting”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:
- Primary — per-email bucket (key:
login:{email}, 10 RPM): The middleware buffers and parses the request body to extract theemailfield, then checks this bucket first. This prevents brute-force attacks against a single account. - 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).
MFA enforcement
Section titled “MFA enforcement”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).
mTLS (internal routes)
Section titled “mTLS (internal routes)”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.
RBAC Model
Section titled “RBAC Model”Platform roles
Section titled “Platform roles”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.
Group-based access control
Section titled “Group-based access control”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.
Cloud Portal roles
Section titled “Cloud Portal roles”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 |
Cloud API key scopes
Section titled “Cloud API key scopes”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.
Audit Trail
Section titled “Audit Trail”Audit log schema
Section titled “Audit log schema”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 reference
Section titled “Action reference”Authentication events
Section titled “Authentication events”| 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 | — |
SSO events
Section titled “SSO events”| 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 |
Admin events
Section titled “Admin events”| 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 |
HMAC chain integrity
Section titled “HMAC chain integrity”When AUDIT_HMAC_KEY is set, every audit event is HMAC-signed and chained:
HMACChain.chain_event(event)computes HMAC-SHA256 over event content fields (includingsrc_ipanddst_ipas observed network facts).- The computed HMAC is stored as
event.hmac. - The previous event’s HMAC is stored as
event.previous_hmac, forming a tamper-evident chain. hmac_key_idtracks key version for rotation support.- 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.
Audit sinks
Section titled “Audit sinks”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".
Defense-in-depth model
Section titled “Defense-in-depth model”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 |
BYOK envelope encryption
Section titled “BYOK envelope encryption”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.
KMS abstraction layer
Section titled “KMS abstraction layer”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 |
Envelope encryption flow
Section titled “Envelope encryption flow”- Generate: The platform calls
generate_dek()on the KMS. The KMS returns a plaintext DEK and a wrapped (encrypted) copy. - Encrypt locally: Audit fields are encrypted with the plaintext DEK using AES-256-GCM (12-byte nonce, nonce-prefixed ciphertext).
- Store: The ciphertext and wrapped DEK are stored in the
audit_logstable. The plaintext DEK is cached in memory — never persisted. - Decrypt: To read encrypted data, the platform unwraps the DEK via the KMS, then decrypts locally.
DEK caching
Section titled “DEK caching”| Parameter | Value |
|---|---|
| Cache TTL | 1 hour (_DEK_CACHE_TTL_SECONDS = 3600) |
| Cache capacity | 256 orgs (_DEK_CACHE_MAX_SIZE = 256) |
| Eviction policy | LRU |
Fail-closed writes, fail-open reads
Section titled “Fail-closed writes, fail-open reads”| 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 |
Container security posture
Section titled “Container security posture”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.
Rust compiled extensions
Section titled “Rust compiled extensions”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.
Supply chain security
Section titled “Supply chain security”Software Bill of Materials (SBOM)
Section titled “Software Bill of Materials (SBOM)”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:
cosign verify-attestation --type cyclonedx \ arbitex/platform:latest | jq -r '.payload' | base64 -dLicense compliance
Section titled “License compliance”| License | Type |
|---|---|
| MIT, Apache 2.0, BSD, ISC, PSF, Unlicense, BlueOak, 0BSD | Permissive |
| MPL 2.0 | Weak copyleft (dynamic linking) |
| GPL, AGPL, SSPL | Prohibited |
Container image signing
Section titled “Container image signing”All production container images are signed using Sigstore cosign with keyless (OIDC-based) identity. Verify signatures before deployment:
cosign verify --certificate-identity-regexp='.*' --certificate-oidc-issuer-regexp='.*' \ arbitex/platform:latestKeyless 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.
Vulnerability audit policy
Section titled “Vulnerability audit policy”| Severity | Policy |
|---|---|
| HIGH / CRITICAL | Must be resolved before release |
| MODERATE / LOW | Tracked and resolved in subsequent releases |
Platform-to-Cloud SSO Exchange
Section titled “Platform-to-Cloud SSO Exchange”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.
Security Configuration Reference
Section titled “Security Configuration Reference”| 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 | — |
See also
Section titled “See also”- Security Trust Center — Architecture overview, compliance framework mapping
- Platform Administration — MFA policy, session management, kill switch operations
- SAML SSO — SAML IdP configuration walkthrough
- Audit Log — HMAC verification, SIEM integration, retention
- Encryption — Data encryption at rest and in transit