We state the split plainly. The architecture Actionary runs on today satisfies the SOC 2 Trust Service Criteria that matter for a customer-facing platform. The Type II attestation — the auditor sitting with the evidence for twelve months — is on the compliance roadmap. Everything below distinguishes the two.

A SOC 2 auditor reads controls. Every control on this page maps to a row in a table, a query against audit_log, a policy enforced by Postgres, or a file in services/core_api/. Nothing here is aspirational.

The five load-bearing claims

Application code never touches secrets. Credentials resolve through SDK default chains (boto3, OAuth providers), sit in environment variables, or attach to compute via IAM roles. The application source has zero credential strings. PLATFORM_MASTER_KEY is the single root secret; every subsystem key — MFA, SSO, integration tokens, LLM provider keys, MCP OAuth, JWT signing — is HKDF-derived at boot.

Per-tenant isolation at every layer. tenant_id sits on every multi-tenant row, every S3 path, every search index, every audit log entry. Postgres row-level security enforces the boundary at the database — a query without the request-scoped GUC set returns zero rows. See tenant isolation for the RLS + four-role runtime.

Audit-everything, mask-nothing. Every administrative action and every entity mutation writes an audit_log row with actor, before, after, and timestamp. Errors surface with their root cause; no except Exception: return 500 pattern exists in any router.

Fail-loudly capture as a compliance control. Every uncaught exception lands in server_errors with a full traceback; every browser-side failure lands in frontend_errors. Both surfaces triage from /platform, and every triage transition writes an audit_log row.

Server-side sessions carry the Common Criteria. Revocation is one UPDATE on auth_sessions. Idle clocks tick per session kind (staff 30 min, tenant 4 h). staff_users sits physically separate from users — operator identity never carries through a tenant click. This maps directly onto CC6.1, CC6.2, CC6.3, and CC6.7.

Public-surface SOC 2 mapping

Twenty-three surfaces a security reviewer probes, mapped to the criterion each satisfies.

# Surface Mechanism Criterion
1 Tenant browser session __Host-acr_tenant_session, opaque 32-byte id, HttpOnly, SameSite=Lax CC6.1
2 Staff console session __Host-acr_staff_session on separate origin CC6.1
3 Support-access grant staff_grant kind, one-shot Redis ticket, bounded by expires_at CC6.2
4 CSRF defence Session-bound token, in-memory store, constant-time compare CC6.1
5 MFA (TOTP) users.totp_secret + recovery codes, encrypted with derived subsystem key CC6.1
6 Passkeys (WebAuthn) webauthn_credentials, strictly-increasing sign_count, self-only management CC6.1
7 Step-up on destructive actions require_step_up dep, 5-minute window, policy toggle audit-gated CC6.1
8 Enterprise SSO OIDC PKCE + SAML 2.0, signature verified, JIT provisioning CC6.2
9 Password storage scrypt:N=32768:r=8:p=1$salt$digest, constant-time verify CC6.1, CC7.1
10 Session revocation Single UPDATE auth_sessions SET revoked_at = now() CC6.3
11 Session-event audit auth_session_events row on every create / rotate / revoke / idle-expire CC6.7
12 Tenant isolation (DB) Postgres RLS keyed on app.current_tenant_id GUC, four-role runtime CC6.1, CC6.6
13 Tenant isolation (HTTP) Four-layer resolver in get_tenant, cross-tenant probes return 404 generic CC6.6
14 Repository-layer tenant filter Every read WHERE tenant_id = X, every write sets tenant_id from context CC6.6
15 S3 object keys <tenant_id>/<asset_id> — namespaces do not overlap CC6.6
16 Encryption at rest SSE-S3, pgp_sym_encrypt on MFA / SSO / OAuth / LLM-key columns CC6.7
17 Encryption in transit TLS everywhere via Caddy, HSTS max-age=63072000; includeSubDomains CC6.7
18 Audit log audit_log table, CRUD + admin + workflow + MCP token events CC7.2, CC7.3
19 Server error capture server_errors (migration 195), global FastAPI handler, no router silent-catch CC7.2
20 Frontend error capture frontend_errors (migration 170), window.onerror + React componentDidCatch CC7.2
21 Token logging hygiene Bearer credentials never appear in logs; token_hash = sha256(token)[:12] CC6.1
22 Operator runtime probes /platform/operations live probes, banner on degraded / down CC7.1, CC7.2
23 Rate limiting fastapi-limiter on every state-changing endpoint CC6.6, CC7.2

The twenty-control audit summary

The controls a Type II auditor reads as positive evidence, condensed.

  1. Tenant isolation at every layer of the stack, enforced by Postgres RLS at the database and by repository discipline in the code.
  2. RBAC gated at the FastAPI dep layer with per-permission rows in role_permissions.
  3. Row-level _permissions block on every list response; CI gate at tests/test_row_permission_contract.py.
  4. Encryption at rest with subsystem keys derived from one master via HKDF-SHA256.
  5. Encryption in transit via TLS + HSTS on every origin.
  6. audit_log capturing entity mutations with before / after payloads.
  7. Session-event audit lineage in auth_session_events.
  8. Fail-loudly error capture on both tiers, triaged with per-transition audit rows.
  9. Passkey enrolment with monotonic sign_count replay defence.
  10. Staff / tenant identity split — two tables, two origins, two cookies.
  11. Support-access grants as the only staff-to-tenant path, per-grant audit.
  12. Step-up MFA on destructive actions with policy-toggle audit chain.
  13. Password storage at the OWASP-recommended scrypt tier, stdlib-native.
  14. Rate limiting on every sensitive endpoint via Redis-backed limiter.
  15. Cross-tenant probes return 404 generic — no existence enumeration.
  16. Secrets outside the DB they protect: PLATFORM_MASTER_KEY in env, IAM roles for S3.
  17. Continuous PITR on managed Postgres, 5-minute granularity, 7-day retention.
  18. Object storage versioning enabled for per-object recovery.
  19. Operator runtime probes at /platform/operations with degraded-state banner.
  20. OpenTelemetry pipeline across core-api, web-app, mcp-server with W3C trace context.

What we deliberately ruled out

Storing storage credentials in the application database. Widens the secret surface to backups, replicas, SQL injection, and any operator with read access. Documented as an anti-pattern in docs/13_storage/storage_admin_panel_spec.md §2.

An is_root flag on any user record. SOC 2 CC6.1 invariant. session_kind === "staff_platform" is the only platform-admin signal. Bearer JWTs are stripped of session_kind at the request boundary so a captured external token cannot claim staff elevation. See design principles.

Silent exception catches in router code. The except Exception: return api_error("server_error", str(exc), 500) pattern is structurally absent across every router file. Exceptions bubble unconditionally to the global handler.

Third-party log shippers, ingestion delays, sampling. Every error lives in the same Postgres a compliance reviewer can SELECT against. No vendor login. No ingestion lag.

The honest gaps

Formal Type II attestation. The 12-month evidence window is the remaining engagement — the automation vendors that drive continuous evidence collection sit against controls that already run.

Append-only audit_log partition-by-month. The table is append-only at the row level. The partition + cold-storage lifecycle triggers on volume; the create_audit_log_partition(year, month) helper is ready.

Persistent OTEL sink. The trace shape is captured across every hop. The persistent trace backend is the wiring item.

Automated GDPR Article 17 deletion. Manual against a documented workflow. Crypto-shredding for backup data is the target shape.

Cross-region S3 replication and SSE-KMS with customer-managed keys. Provisioned in-region with SSE-S3 and all four Block Public Access settings on. The enterprise-tier upgrade path is scoped.

For CISOs evaluating risk against a control matrix, the security substrate reads the way the CISO brief frames it: the controls are the code, and the code is the evidence.

The takeaway

SOC 2 is the auditor’s confidence that controls exist, run, and leave evidence. Actionary’s controls live as code in this repository. The security posture and tenant isolation pages read the same way an auditor’s fieldwork report does, because the substrate is the same substrate. The attestation follows the evidence. The evidence is already accumulating.