Skip to content

Security Operations

This reference covers the complete security operations lifecycle for Arbitex administrators: monitoring authentication events, verifying audit log integrity, executing incident response playbooks, hardening your deployment, and collecting SOC 2 evidence. It consolidates operational procedures that span multiple subsystems into a single authoritative reference.

For user-facing event history and session self-service, see Account Security Monitor. For OCSF export format details and framework-specific evidence packs, see Compliance Audit Evidence Guide. For CredInt architecture and corpus details, see Credential Intelligence.


Every authentication action against an Arbitex account produces an audit event with the auth. prefix. The table below lists all 12 event types, the action string used in API responses and audit exports, the source service, and the trigger condition.

Action String Source Trigger
auth.login_success auth.py Successful interactive login via password or passkey
auth.login_failed auth.py Failed login — wrong password, locked account, or unrecognised user
auth.logout auth.py Explicit logout initiated by the user
auth.mfa_verify_success auth.py MFA challenge completed successfully during the login flow
auth.mfa_verify_failed auth.py MFA challenge failed — wrong code or expired TOTP window
auth.mfa_enabled auth.py TOTP or WebAuthn MFA factor added to the account
auth.mfa_disabled auth.py MFA factor removed by user or admin
auth.webauthn_registered auth.py New WebAuthn passkey registered on the account
auth.webauthn_login auth.py Successful login using a WebAuthn/FIDO2 passkey
auth.webauthn_revoked auth.py WebAuthn passkey removed from the account
auth.session_evicted sessions.py Session terminated automatically — concurrent limit exceeded, oldest session removed (FIFO)
auth.session_force_logout sessions.py Session terminated by an admin force-logout action

All events include user_id (UUID of the authenticating user) and source_ip (origin of the request). Events related to admin actions additionally include metadata.admin_id identifying the acting administrator.

Every user can view their own authentication history and active sessions without administrator involvement.

Path: /portal/my-security or Account → Security

The self-service view provides:

  • Event timeline — last 90 days of auth events, most recent first; relative timestamps with absolute on hover
  • Active Sessions panel — all currently valid sessions with IP, location, device, and expiry
  • Filter controls — event type (one of the 12 types or “All”), date range
  • Auto-refresh — timeline refreshes every 60 seconds

Each event row shows: event type icon (green = success, red = failure, amber = warning), IP address, GeoIP city and country, device parsed from User-Agent, and anonymising network badges (VPN / Proxy / Tor / Hosting) when detected.

API (self-service):

Terminal window
# Query own auth events
curl -H "Authorization: Bearer $TOKEN" \
"https://api.arbitex.ai/v1/audit/events?user_id=me&limit=50"
# Filter to failed logins only
curl -H "Authorization: Bearer $TOKEN" \
"https://api.arbitex.ai/v1/audit/events?user_id=me&action=auth.login_failed&limit=50"

The user_id=me alias resolves to the authenticated caller’s UUID at query time. Non-admin users can only use "me" — passing any other UUID returns 403.

Admin users can list and terminate sessions across the entire organisation.

GET /api/v1/admin/sessions # list all org sessions
GET /api/v1/admin/sessions?user_id={uuid} # filter to one user
DELETE /api/v1/admin/sessions/{session_id} # revoke a single session
DELETE /api/v1/admin/users/{user_id}/sessions # revoke all sessions for a user

List sessions response:

{
"items": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"user_id": "a1b2c3d4-0000-0000-0000-000000000001",
"token_jti": "d4f8e2c1-1234-5678-abcd-ef0123456789",
"created_at": "2026-03-14T08:00:00Z",
"last_activity": "2026-03-14T10:15:00Z",
"expires_at": "2026-03-21T08:00:00Z",
"ip_address": "203.0.113.42",
"user_agent": "Mozilla/5.0 ...",
"is_active": true
}
],
"total": 1
}

Concurrent session limit: Default is 5 sessions per user. When a new login would exceed the limit, sessions.py runs enforce_session_limit() immediately: the oldest sessions are evicted in FIFO order, their token_jti values added to the JWT blacklist, and an auth.session_evicted event is written for each. Session rows are soft-deleted (is_active = false) — they are never purged, preserving the audit trail.

Revoke all sessions for a user:

Terminal window
curl -X DELETE \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/users/a1b2c3d4-0000-0000-0000-000000000001/sessions"

Returns the list of revoked session objects. Returns 404 if the user has no active sessions. Each revocation writes an auth.session_force_logout event with metadata.admin_id, metadata.session_id, and metadata.reason: "admin_force_logout".

Every authentication event is enriched at write time using a layered GeoIP pipeline. The enrichment data is stored with the event record and cannot be retroactively altered.

Data Source Purpose Fields
MaxMind GeoIP2 City MMDB Primary city/country/ISP resolution country_code, country_name, region, city, isp
IP2Location DB25 Fallback city resolution proxy_type, threat
iptoasn TSV ASN resolution asn, asn_org
MaxMind Anonymous IP MMDB Anonymising network detection is_vpn, is_proxy, is_tor, is_hosting

The enrichment layer maintains a 128,000-entry TTL cache with a 4-hour TTL per entry. The cache is hot-reloaded on SIGHUP — send this signal to the platform process to reload updated MMDB files without a full restart.

Fields on each auth event:

Field Description
src_country_code ISO 3166-1 alpha-2 country code
src_country_name Full country name
src_region Region/state/province
src_city City name (best-effort; absent for some IP ranges)
src_isp Internet service provider
src_proxy_type Proxy classification if is_proxy is true
src_threat Threat label from IP2Location (e.g., spam, attacker)
src_asn Autonomous System Number
src_asn_org ASN organisation name
is_vpn True if source IP is a known VPN exit node
is_proxy True if source IP is a known open proxy
is_tor True if source IP is a Tor exit node
is_hosting True if source IP is a cloud hosting/datacenter range

Private (RFC 1918), CGNAT (100.64.0.0/10), and loopback addresses resolve to empty location fields. GeoIP enrichment is best-effort and should not be the sole basis for access control decisions.

Users with the ADMIN role have access to expanded controls that are not available to standard USER accounts. Arbitex has two roles: USER and ADMIN. There is no dedicated security_auditor role — security auditing is performed by ADMIN users.

Portal path: Admin → Users → [User] → Security Events

Admin capabilities beyond self-service:

  • View the full event timeline for any user in the organisation
  • Export — download events as CSV for audit evidence
  • Force Logout — terminate all active sessions for a selected user in a single action
  • Search across users by IP address, event type, or date range

Hot retention: All auth events are queryable via the API for 90 days. Events older than 90 days are available via the compliance export API. See Compliance Audit Evidence Guide for export procedures.


Every audit log record is cryptographically bound to its predecessor using HMAC-SHA256. This makes it detectable if any record is modified, deleted, or reordered after the fact — even by a database administrator with direct write access.

Each record includes three integrity fields:

Field Description
hmac HMAC-SHA256 of this record’s canonical content + previous_hmac
previous_hmac HMAC of the immediately preceding record (acts as a digest pointer)
hmac_key_id Identifier of the signing key used for this record

These three fields are excluded from the HMAC digest computation. This makes re-verification idempotent — you can verify the chain without needing to strip integrity fields before hashing.

The first audit record in the chain uses a fixed genesis sentinel as its previous_hmac:

previous_hmac = "0000000000000000000000000000000000000000000000000000000000000000"

That is 64 zero characters — a 256-bit zero string. verify_chain() checks that the first record’s previous_hmac equals exactly this value. Any deviation indicates the chain has been prefixed with fabricated records.

The canonical message that is HMAC-signed for each record is:

key_id + ":" + json.dumps(content, sort_keys=True) + previous_hmac

Where:

  • key_id is the value of hmac_key_id for the record (defaults to "default" for legacy events created before key versioning was introduced)
  • content is the record’s data as a Python dict with keys sorted (sort_keys=True) — this ensures deterministic serialisation across platform versions
  • previous_hmac is the raw 64-character hex string from the preceding record

The sort_keys=True requirement is critical: if keys are serialised in a different order, the HMAC will not match even though the data is identical.

The hmac_key_id field enables key rotation without breaking backward-compatible verification. When a new signing key is introduced:

  1. New records use the new key_id in the message prefix and store the new hmac_key_id.
  2. Verification reads hmac_key_id from each record and selects the corresponding key for that record’s HMAC computation.
  3. Records signed with the previous key (stored with the old hmac_key_id) continue to verify correctly using the old key.

Legacy records that predate key versioning have hmac_key_id = "default".

The verify_chain() function performs three checks on a sequence of records ordered by creation time:

  1. Genesis check — the first record’s previous_hmac must equal the 64-character zero sentinel.
  2. HMAC recompute — for each record, recompute the HMAC using the stored hmac_key_id and compare against the stored hmac. Mismatch means the record was altered.
  3. Link consistency — for each record after the first, the stored previous_hmac must equal the hmac of the immediately preceding record. Mismatch means a record was inserted, deleted, or reordered.

The function returns (is_valid: bool, errors: list[str]). A valid chain returns (True, []). Any failure returns (False, [<description of each broken link>]).

API verification:

Terminal window
curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"start": "2026-01-01T00:00:00Z", "end": "2026-12-31T23:59:59Z"}' \
"https://api.arbitex.ai/api/v1/admin/audit/verify" \
| jq '.'

Clean chain response:

{
"valid": true,
"events_checked": 48215,
"errors": []
}

Broken chain response:

{
"valid": false,
"events_checked": 48215,
"errors": [
"Event 1423: HMAC mismatch (expected 'a3f...', got 'c7d...')",
"Event 1424: previous_hmac mismatch"
]
}

The HMAC chain is silently disabled when the AUDIT_HMAC_KEY environment variable is empty or unset. In this state, hmac, previous_hmac, and hmac_key_id fields are omitted from records, and verify_chain() returns (True, []) trivially (no records to verify). Production deployments must always have AUDIT_HMAC_KEY set to a strong secret.

Arbitex exports audit events in OCSF v1.1.0 (Open Cybersecurity Schema Framework) for SIEM integration. The product metadata embedded in every export is:

{
"metadata": {
"product": {
"vendor_name": "Arbitex",
"uid": "arbitex-platform",
"version": "..."
}
}
}

OCSF class mappings:

OCSF Class Class UID Arbitex Events
API Activity 6003 API requests, model routing, admin API calls
Security Finding 2001 DLP blocks, policy enforcement, CredInt hits
Authentication Activity 3002 Auth events (auth.*)
Account Change 3004 Admin config changes, user provisioning, SCIM events

OCSF formatter action mappings (shorter form used internally by the formatter, which maps to the full auth.* action strings above):

Formatter Form Maps From OCSF Activity
login_success auth.login_success Authentication success
mfa_success auth.mfa_verify_success MFA challenge success
token_issued JWT issuance events Token lifecycle
token_revoked Session eviction/force-logout Token revocation
password_changed Password reset events Account change
saml_login SAML SSO login Federation login
webauthn_login auth.webauthn_login Passkey authentication

For full SIEM connector configuration and OCSF field mappings per connector, see Compliance Audit Evidence Guide.


Playbook 1: Suspicious Login Investigation

Section titled “Playbook 1: Suspicious Login Investigation”

Use this playbook when you observe unexplained auth.login_failed spikes, logins from unusual geographic locations, or reports of account compromise.

Step 1 — Identify the pattern

Query the audit log for failed login events in the investigation window:

Terminal window
# Failed logins in last 24 hours
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/v1/audit/events?action=auth.login_failed&created_after=$(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ)&limit=500" \
| jq '.events | group_by(.metadata.src_ip) | map({ip: .[0].metadata.src_ip, count: length}) | sort_by(-.count)'

Look for: a single IP with high failed-login count (brute force), or a single user_id appearing across many failures (targeted account attack).

Step 2 — Check GeoIP and anonymising network flags

For each suspicious IP, examine the enrichment fields on the events:

Terminal window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/v1/audit/events?action=auth.login_failed&created_after=2026-03-14T00:00:00Z&limit=50" \
| jq '.events[] | {ip: .metadata.src_ip, country: .src_country_code, city: .src_city, is_vpn: .is_vpn, is_tor: .is_tor, is_hosting: .is_hosting}'

Impossible travel: If the same user_id has a auth.login_success in city A and then a login attempt (success or failure) from city B within a timeframe too short to travel, this is a strong indicator of credential compromise.

VPN/Tor/hosting flags: is_vpn: true, is_tor: true, or is_hosting: true on a login event does not by itself indicate compromise, but combined with auth.login_failed spikes or unusual access times, it warrants investigation.

Step 3 — Force-revoke sessions

If you determine the account is compromised or being actively attacked, revoke all active sessions immediately:

Terminal window
curl -X DELETE \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/users/{user_id}/sessions"

This immediately blacklists all token_jti values for the user’s sessions. Any in-flight requests using those tokens will receive 401 Unauthorized. The action is logged as auth.session_force_logout events.

Step 4 — Export audit trail for evidence

Export the investigation period’s events with the HMAC chain intact:

Terminal window
# Export events for the affected user, full investigation window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/v1/audit/events?user_id={user_id}&created_after=2026-03-01T00:00:00Z&limit=1000" \
> investigation-$(date +%Y%m%d)-user-{user_id}.json
# Verify chain integrity for the evidence period
curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"start": "2026-03-01T00:00:00Z", "end": "2026-03-15T00:00:00Z"}' \
"https://api.arbitex.ai/api/v1/admin/audit/verify" \
> chain-verification-$(date +%Y%m%d).json

Step 5 — Notify and harden

  1. Notify the affected user and their org admin of the investigation.
  2. Initiate a forced password reset: POST /api/v1/admin/users/{user_id}/reset-password
  3. If the attack originated from a specific IP range, consider adding an IP allowlist entry via Admin → Security → IP Allowlist.
  4. If attacks are ongoing from a class of IPs (e.g., a specific ASN), route the events to your SIEM and set a detection rule on auth.login_failed volume from that src_asn.

Playbook 2: DLP Policy Bypass Investigation

Section titled “Playbook 2: DLP Policy Bypass Investigation”

Use this playbook when you observe ALLOW_WITH_OVERRIDE events that appear unexpected — overrides by users who should not have override permissions, override volume spikes, or overrides on high-sensitivity policies.

Step 1 — Query override events

Terminal window
# All overrides in the last 7 days
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/audit-logs?action=allow_with_override&start=2026-03-08T00:00:00Z&end=2026-03-15T00:00:00Z&limit=1000" \
| jq '.events[] | {event_id: .id, user_id: .user_id, org_id: .org_id, rule_id: .metadata.rule_id, entity_type: .metadata.entity_type, override_reason: .metadata.override_reason, created_at: .created_at}'

Look for: repeated overrides from the same user_id on the same rule_id (systematic bypass), overrides on BLOCK-level policies (which should be impossible — BLOCK is non-overridable), or overrides outside business hours.

Step 2 — Examine the override token details

ALLOW_WITH_OVERRIDE generates a short-lived override token issued at the time the user provides their justification. This token is a signed HS256 JWT with:

  • Subject: policy_override
  • TTL: 5 minutes from issuance
  • Claims: rule_id, user_id, org_id, entity_type

The token TTL means an override is scoped to a single 5-minute window. If you see many override events from the same user on the same rule within a short window, this indicates the user is repeatedly triggering overrides rather than a persistent bypass.

Step 3 — Check user group membership

Examine whether the overriding user belongs to a group that legitimately has override permissions for that rule:

Terminal window
# Get the user's group memberships
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/users/{user_id}/groups"
# Check which groups have override permission on the affected rule
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/policy/rules/{rule_id}/group-permissions"

If the user’s group is not expected to have override capability for that rule, this is a policy misconfiguration — not a technical bypass. Update the rule’s override permission matrix to remove the group.

Step 4 — Run policy simulation

Use the policy simulator to validate current rule behaviour before making changes:

Terminal window
# Simulate against a test payload to confirm current enforcement
curl -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"user_id": "{user_id}",
"payload": "Sample content containing sensitive data",
"policy_chain_id": "{chain_id}"
}' \
"https://api.arbitex.ai/api/v1/admin/policy/simulate"

See Outpost Policy Simulator for full simulation options.

Step 5 — Remediation

Finding Action
User overriding a policy they should not have override rights on Remove group override permission for that rule
Override volume is expected but undocumented Add override event routing to SIEM; create audit evidence package
Policy is set to ALLOW_WITH_OVERRIDE when it should be BLOCK Change rule action to BLOCK — this removes the override path entirely
Override reason text is absent or trivial Review override reason requirements in policy configuration

Use this playbook when CredInt detects a known-compromised credential in an AI request, indicated by a credint_hit: true audit event with frequency_bucket: "critical" or "high".

Step 1 — Identify the exposure event

Terminal window
# Find credint hits in the last 7 days
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/audit-logs?credint_hit=true&start=2026-03-08T00:00:00Z&limit=100" \
| jq '.events[] | {id: .id, user_id: .user_id, frequency_bucket: .frequency_bucket, sha1_prefix: .sha1_prefix, created_at: .created_at, action: .action}'

The sha1_prefix field contains the first 8 hex characters of the SHA-1 hash of the detected credential. This allows cross-referencing against breach databases without exposing the plaintext credential. The cleartext credential is never written to the audit log.

Step 2 — Assess CredInt service state

CredInt uses a circuit breaker to prevent a degraded CredInt microservice from blocking AI requests. Check the circuit breaker state:

  • Circuit breaker threshold: 3 consecutive failures triggers the circuit open
  • Reset window: 60 seconds after opening, the circuit transitions to half-open
  • Fail-open design: When the circuit is open, requests are permitted through without CredInt checking — they are not blocked

Check current CredInt availability:

Terminal window
# CredInt service health (from outpost or platform host)
curl -s http://credint:8202/health | jq '.'
# Check circuit breaker state in platform logs
grep "credint circuit" /var/log/arbitex/platform.log | tail -20

Step 3 — Block the user’s sessions

Immediately terminate all active sessions for the user whose request contained the compromised credential:

Terminal window
curl -X DELETE \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/users/{user_id}/sessions"

This revokes all active JWTs immediately via the blacklist. The user cannot authenticate with the compromised credential even if they attempt another login before the password is reset.

Step 4 — Coordinate credential rotation

Arbitex cannot rotate the exposed credential — it is external to the platform (a database password, API key, bearer token, etc.). The sha1_prefix field in the audit event identifies which credential was flagged without revealing the plaintext.

Actions required outside Arbitex:

  1. Determine which system or service the credential belongs to (coordinate with the user).
  2. Immediately rotate the credential in the owning system.
  3. Revoke or invalidate the exposed credential in all systems where it was used.
  4. Audit the owning system’s access logs for any use of the credential between the estimated exposure time and revocation.

Step 5 — Assemble compliance record

Export the CredInt event and related audit trail:

Terminal window
# Export the specific credint event
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/v1/audit/events/{event_id}" \
> credint-event-$(date +%Y%m%d).json
# Export full user audit trail for investigation window
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/v1/audit/events?user_id={user_id}&created_after=2026-03-01T00:00:00Z&limit=1000" \
> user-trail-$(date +%Y%m%d).json
# Verify chain integrity
curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"start": "2026-03-01T00:00:00Z", "end": "2026-03-15T23:59:59Z"}' \
"https://api.arbitex.ai/api/v1/admin/audit/verify" \
> chain-verify-$(date +%Y%m%d).json

This package constitutes the incident record for compliance reporting. The credint_hit: true flag, frequency_bucket, and sha1_prefix fields in the audit export are sufficient to document the detection event without retaining the plaintext credential.


Arbitex supports Azure Key Vault as a secrets backend. When configured, all sensitive environment variables are resolved from Key Vault at startup rather than from process environment variables or .env files.

Configuration:

Terminal window
SECRETS_BACKEND=vault
AZURE_VAULT_URL=https://your-vault.vault.azure.net/

Authentication uses DefaultAzureCredential — this resolves credentials in order: environment variables, workload identity, managed identity, Azure CLI. In production, configure a managed identity on the compute hosting the platform.

Name translation: Environment variable names use underscores (AUDIT_HMAC_KEY) but Key Vault secret names use hyphens. The secrets backend automatically translates _ to - when resolving names: AUDIT_HMAC_KEY resolves from Key Vault as AUDIT-HMAC-KEY.

Fail-fast startup: If SECRETS_BACKEND=vault and any required secret cannot be resolved from Key Vault, the platform refuses to start. This prevents silent fallback to insecure defaults or unset secrets in a production environment.

Internal API routes (/v1/internal/) are protected by mutual TLS. The platform validates the client certificate against a CA bundle before processing any internal request.

Configuration:

Variable Description
MTLS_CA_BUNDLE Path to a PEM bundle containing CA certificates. Takes precedence over individual cert paths.
CLOUD_CA_CERT_PATH Path to the primary CA certificate (used if MTLS_CA_BUNDLE is not set).
CLOUD_CA_INTERMEDIATE_PATH Path to the intermediate CA certificate (used if MTLS_CA_BUNDLE is not set).

When MTLS_CA_BUNDLE is set, it overrides CLOUD_CA_CERT_PATH and CLOUD_CA_INTERMEDIATE_PATH. Use the bundle path for simplicity; use individual cert paths only for legacy compatibility.

What mTLS protects: The /v1/internal/ route prefix. All requests to these endpoints must present a valid client certificate signed by the configured CA. External requests without a certificate are rejected before any handler logic runs.

Support mTLS Bypass — Emergency Use Only

Section titled “Support mTLS Bypass — Emergency Use Only”

The _SUPPORT_MTLS_BYPASS environment variable enables an emergency bypass that allows support access without a valid client certificate. This is intended exclusively for recovery scenarios where certificate infrastructure has failed and the system is inaccessible.

Confirm bypass is not active:

Terminal window
# Confirm the variable is not set in the running environment
printenv _SUPPORT_MTLS_BYPASS
# Search application logs for bypass usage
grep "support-bypass" /var/log/arbitex/platform.log

SCIM provisioning tokens are generated using Python’s secrets.token_urlsafe(32), providing 256 bits of cryptographic entropy. When a SCIM token is created:

  1. The full token value is displayed once — at creation time only.
  2. The platform stores only the SHA-256 hash of the token (not bcrypt, not the plaintext).
  3. Subsequent requests authenticate by hashing the presented token and comparing to the stored hash.

Multi-organisation isolation is enforced at the token level: each SCIM token is scoped to a single org_id. A token issued for organisation A cannot authenticate SCIM requests targeting organisation B.

Token rotation procedure:

  1. Generate a new token: POST /api/v1/admin/scim/tokens
  2. Copy the returned token value — this is the only opportunity to do so.
  3. Update the token value in your IdP SCIM connector.
  4. Verify provisioning is working with the new token.
  5. Delete the old token: DELETE /api/v1/admin/scim/tokens/{token_id}

MFA events produce auditable records that demonstrate the MFA control is operating:

Event Trigger
auth.mfa_enabled User adds TOTP or WebAuthn factor
auth.mfa_disabled User or admin removes an MFA factor
auth.mfa_verify_success MFA challenge passed during login
auth.mfa_verify_failed MFA challenge failed

MFA backup codes are stored using bcrypt (not SCIM token SHA-256). Backup codes are one-time use; each use invalidates the code.

Enforce MFA org-wide:

Terminal window
curl -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"enforcement_level": "required"}' \
"https://api.arbitex.ai/api/v1/admin/org/mfa-policy"

With enforcement_level: "required", all requests to sensitive endpoints must carry a JWT with mfa_verified: true. Requests without this claim receive 403. The policy takes effect immediately — the in-memory policy cache is cleared synchronously on the API response.

See MFA Enforcement for full policy configuration options.

When the DLP inference service is unavailable, the platform’s behaviour is controlled by DLP_INFERENCE_FAIL_MODE:

Value Behaviour
closed (default) Requests are blocked when inference is unavailable — no content passes without a policy evaluation
open Legacy mode — requests pass through without DLP inspection when inference is unavailable

Production deployments should always use the default closed mode. Monitor inference service availability via the OTel metrics pipeline and alert on sustained unavailability so that fail-closed blocks can be distinguished from an actual infrastructure issue.

Control Configuration Verification
Azure Key Vault secrets SECRETS_BACKEND=vault, AZURE_VAULT_URL set Platform starts without env var secrets
mTLS on internal routes MTLS_CA_BUNDLE or cert paths set curl /v1/internal/ without cert returns 403
Support bypass disabled _SUPPORT_MTLS_BYPASS unset printenv _SUPPORT_MTLS_BYPASS returns empty
AUDIT_HMAC_KEY set Non-empty value in secrets backend verify_chain() returns field-present responses
MFA enforced enforcement_level: "required" Test request without mfa_verified claim returns 403
DLP fail mode closed DLP_INFERENCE_FAIL_MODE=closed or not set Default confirmed; do not set open
SCIM tokens rotated Tokens < 90 days old Audit: GET /api/v1/admin/scim/tokens — check created_at
Session limit configured max_concurrent_sessions reviewed Default is 5; adjust if org policy requires lower

Arbitex provides a pre-built SOC 2 summary report for administrators:

GET /api/v1/admin/compliance/soc2-report?days=30

This endpoint requires the ADMIN role. It returns a structured summary covering the five TSC domains mapped to actual platform state — not a static template.

Example request:

Terminal window
# 30-day report (default)
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/compliance/soc2-report?days=30" \
| jq '.' > soc2-report-$(date +%Y%m%d).json
# 90-day report for quarterly review
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/compliance/soc2-report?days=90" \
| jq '.' > soc2-report-90day-$(date +%Y%m%d).json
# Annual report for SOC 2 Type II examination period
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/compliance/soc2-report?days=365" \
| jq '.' > soc2-report-annual-$(date +%Y%m%d).json

The report response contains five top-level sections. Each maps to a SOC 2 Trust Services Criteria domain.

Section: access_controls (TSC: CC6)

Logical and physical access controls. Covers user account state, privilege distribution, MFA adoption, session management, and IP access controls.

{
"access_controls": {
"tsc": "CC6",
"total_users": 142,
"active_users": 138,
"role_distribution": {
"ADMIN": 8,
"USER": 134
},
"mfa_adoption_pct": 94.4,
"active_sessions": 23,
"ip_allowlist_entries": 5
}
}
Field SOC 2 Use
role_distribution Demonstrates least-privilege — only necessary users hold ADMIN role
mfa_adoption_pct Quantifies MFA adoption against org policy requirement
ip_allowlist_entries Documents network-level access restrictions in place

Section: audit_trail (TSC: CC7)

Detection and monitoring controls. Covers audit log volume, HMAC chain integrity status, any detected gaps, and the date range of coverage.

{
"audit_trail": {
"tsc": "CC7",
"total_records": 482150,
"hmac_chain_status": "verified",
"chain_errors": 0,
"gap_count": 0,
"date_range": {
"start": "2026-02-13T00:00:00Z",
"end": "2026-03-15T00:00:00Z"
}
}
}

hmac_chain_status will be one of:

  • "verified" — chain ran and all records checked cleanly
  • "failed" — chain verification detected one or more integrity errors; chain_errors > 0
  • "disabled"AUDIT_HMAC_KEY is not set; chain integrity is not being maintained

Section: data_protection (TSC: CC6/CC7)

Data classification and enforcement controls. Covers DLP rule coverage, event volume by severity, and compliance bundle activation.

{
"data_protection": {
"tsc": "CC6/CC7",
"dlp_rules_active": 47,
"dlp_rules_total": 52,
"events_by_severity": {
"critical": 12,
"high": 89,
"medium": 1204,
"low": 8871
},
"compliance_bundles_enabled": 3,
"compliance_bundles_total": 6
}
}

Section: change_management (TSC: CC8)

Change and configuration management controls. Covers the count and types of admin configuration changes during the report period.

{
"change_management": {
"tsc": "CC8",
"admin_config_changes": 34,
"tracked_actions": [
"policy_rule_created",
"policy_rule_updated",
"policy_rule_deleted",
"mfa_policy_updated",
"scim_token_created",
"scim_token_revoked",
"compliance_bundle_enabled",
"compliance_bundle_disabled",
"ip_allowlist_updated",
"user_role_changed"
]
}
}

Section: availability (TSC: A1)

Availability and reliability controls. Covers circuit breaker states for external service dependencies and failover configuration.

{
"availability": {
"tsc": "A1",
"circuit_breakers": {
"credint": "closed",
"dlp_inference": "closed",
"geoip": "closed"
},
"failover_configured": true
}
}

Circuit breaker states:

  • "closed" — service operating normally, all requests passing through
  • "open" — circuit tripped; service is bypassed (fail-open behaviour active)
  • "half_open" — recovering; test requests are being sent

Full SOC 2 evidence pack for a 12-month examination period:

Terminal window
EXAM_START="2025-01-01T00:00:00Z"
EXAM_END="2025-12-31T23:59:59Z"
EVIDENCE_DIR="soc2-evidence-$(date +%Y%m%d)"
mkdir -p "$EVIDENCE_DIR"
# SOC 2 report summary
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/compliance/soc2-report?days=365" \
> "$EVIDENCE_DIR/soc2-report.json"
# CC6 — Access events
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/audit-logs?category=authentication&start=${EXAM_START}&end=${EXAM_END}&limit=10000" \
> "$EVIDENCE_DIR/cc6-access-events.json"
# CC6 — DLP enforcement
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/audit-logs?action=block&start=${EXAM_START}&end=${EXAM_END}&limit=10000" \
> "$EVIDENCE_DIR/cc6-dlp-enforcement.json"
# CC7 — HMAC chain verification
curl -s -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"start\": \"${EXAM_START}\", \"end\": \"${EXAM_END}\"}" \
"https://api.arbitex.ai/api/v1/admin/audit/verify" \
> "$EVIDENCE_DIR/cc7-chain-verification.json"
# CC8 — Admin config changes
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/audit-logs?category=admin&start=${EXAM_START}&end=${EXAM_END}&limit=5000" \
> "$EVIDENCE_DIR/cc8-config-changes.json"
# CC6/CC7 — Compliance bundle export
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/compliance-bundles-export/" \
> "$EVIDENCE_DIR/compliance-bundles.json"
# Override audit (policy exceptions)
curl -H "Authorization: Bearer $ADMIN_TOKEN" \
"https://api.arbitex.ai/api/v1/admin/audit-logs?action=allow_with_override&start=${EXAM_START}&end=${EXAM_END}&limit=5000" \
> "$EVIDENCE_DIR/policy-overrides.json"
echo "Evidence pack assembled in ${EVIDENCE_DIR}/"
ls -la "$EVIDENCE_DIR/"
TSC Domain Label Arbitex Evidence
CC6 Logical and Physical Access Controls access_controls section; auth event exports; DLP enforcement exports; compliance bundle export
CC7 System Operations and Detection audit_trail section; HMAC chain verification result; SIEM OCSF exports
CC8 Change Management change_management section; admin audit log for config change events
A1 Availability availability section; circuit breaker states; outpost heartbeat logs

For sub-domain mapping and OCSF field references used in SIEM-based evidence, see Compliance Audit Evidence Guide. That guide covers framework-specific evidence filters for PCI-DSS, HIPAA, SOX, and the OCSF event schema in full detail.

Before presenting evidence to SOC 2 examiners:

  • Run GET /api/v1/admin/compliance/soc2-report?days=365 and confirm hmac_chain_status: "verified" and gap_count: 0
  • Run POST /api/v1/admin/audit/verify for the full examination period and confirm valid: true
  • Export compliance bundle definitions and confirm required bundles show "enabled": true
  • Confirm audit log retention covers the full examination period (90-day hot; archive via SIEM for longer periods)
  • Review role_distribution in the report — document rationale for each ADMIN-role user
  • Review mfa_adoption_pct — if below 100%, document exemptions and compensating controls
  • Check circuit_breakers — any "open" state at the time of the report indicates a live service degradation
  • Confirm _SUPPORT_MTLS_BYPASS is not set in the production environment
  • Confirm AUDIT_HMAC_KEY is set (chain status will show "disabled" if it is not)