SOC 2 for Generative AI: The 7 Controls Auditors Actually Test in 2026

F

Frenkie

· 10 min read

Look, SOC 2 used to be predictable. Least-privilege IAM, branch protection, SSO, alerting, a tidy vendor spreadsheet. You knew what the auditor would ask for.

Then your team shipped an LLM feature.

Now customer prompts flow to third-party inference endpoints. Corporate knowledge sits in a vector database serving retrieval across tenants. Agents call tools that mutate production data. None of that was in your last audit, and all of it is in your next one.

Here is the shift in one line: auditors stopped asking whether you have an AI policy and started sampling your inference pipeline.

The AICPA Trust Services Criteria have not changed. Security, Availability, Processing Integrity, Confidentiality, and Privacy still govern every report. What changed is what counts as evidence under them.

Control

Primary criteria

What the auditor samples

1. Zero data retention

CC6.1, C1.2

Signed DPAs, API tier config, purge cycles

2. Inbound redaction

CC6.1, CC6.7, C1.1

Middleware code plus test output

3. Vector isolation

CC6.3, C1.1

RLS policies, namespace config

4. Injection defense

CC6.6, CC7.1

Threat model, guardrail config, red-team reports

5. Agent scope limits

CC6.2, CC6.3, PI1.1

Scoped tokens, approval-gate diagrams

6. Immutable telemetry

CC7.2, CC7.3

Structured log exports

7. Model governance

CC3.2, CC8.1, PI1.2

Pinned versions, CI eval runs, change tickets

Work through them in order. Each one maps to a question an enterprise buyer is already asking in security review.

1. Does customer data train the model? (CC6.1, C1.2)

The risk is simple and it voids your own privacy policy. If prompts or completions are retained for training, evaluation, or human review, you are in breach of the confidentiality commitments you published, regardless of what your infrastructure looks like.

The control: enterprise agreements guaranteeing zero data retention, or retention strictly limited to abuse monitoring with a documented purge cycle, plus explicit contractual language that prompts and embeddings never reach model weights.

  • Use enterprise API tiers, not self-serve. Consumer and self-serve tiers often default to retention. Commercial tiers from the major providers, or managed routes through AWS Bedrock, Azure OpenAI Service, and Google Cloud Vertex AI, give you contractual terms an auditor can read.

  • Configure retention in code, not in a console. Set provider policies at the Terraform or Pulumi layer so the configuration is version-controlled and reviewable.

  • Keep a live vendor register. Every model endpoint in staging and production, linked to its signed DPA and current SOC 2 Type II report. Auditors sample this list, and shadow endpoints are the most common finding.

  • Watch the retention clock. Abuse-monitoring retention is defensible. Indefinite retention described as abuse monitoring is not.

resource "azurerm_cognitive_account" "openai" { name = "corp-prod-openai" kind = "OpenAI" sku_name = "S0" custom_subdomain_name = "corp-prod-ai" customer_managed_key { key_vault_key_id = azurerm_key_vault_key.openai_cmk.id identity_client_id = azurerm_user_assigned_identity.openai.client_id } }

2. What stops PII reaching the model? (CC6.1, CC6.7, C1.1)

An automated pre-inference sanitization layer, and auditors want to see the code. Sending raw user input to an external model exposes personal data, health data, payment data, and credentials without documented consent or business justification.

CC6.7 is the criterion most teams miss here. It governs the transmission and movement of information, which is exactly what an outbound API call to a model provider is.

The control: deterministic middleware that inspects, masks, or rejects sensitive tokens before the payload leaves your boundary. Open-source Microsoft Presidio handles detection and anonymization; Lakera Guard covers this alongside injection defense; Private AI is the commercial option built for regulated data.

import { scrubSensitiveData } from '@/lib/security/redaction'; export async function forwardToInference(userPrompt: string, ctx: UserSession) { const { cleanPrompt, redactionLog, blocked } = await scrubSensitiveData(userPrompt, { maskPatterns: ['EMAIL', 'SSN', 'PHONE', 'BEARER_TOKEN', 'API_KEY'], rejectOnCreditCard: true, }); if (blocked) { throw new SecurityException('Prompt contained prohibited payment or secret data.'); } await auditLogger.logEvent({ event: 'ai.prompt.sanitized', userId: ctx.id, redactedEntitiesCount: redactionLog.entityCount, promptHash: crypto.createHash('sha256').update(cleanPrompt).digest('hex'), timestamp: new Date().toISOString(), }); return llmClient.complete({ prompt: cleanPrompt }); }

Note what the log stores: a hash and a count, never the raw prompt. A redaction layer that writes unmasked PII into your logging pipeline has moved the problem, not solved it.

3. Can one tenant retrieve another tenant's documents? (CC6.3, C1.1)

Only if your vector store lacks enforced isolation, and this is the finding that kills enterprise deals. In multi-tenant retrieval architectures, embeddings sit in a shared index. Without authorization metadata evaluated at both write and read time, a similarity search can surface another customer's contracts.

The control: logical separation of embeddings per tenant, enforced by access rules the database applies rather than the application layer promises.

ALTER TABLE document_embeddings ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_policy ON document_embeddings FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid);

Two rules that make this auditable:

  • Mandate partition tags at the schema level. Tenant ID and workspace ID are required fields on write, not optional metadata. Pinecone namespaces, Qdrant collections, and Weaviate multi-tenancy all support this natively.

  • Never run an unfiltered similarity search. Every query appends equality filters for the authenticated tenant before cosine distance is calculated. Filtering results after retrieval is not isolation; it is a race condition with a privacy incident at the end.

4. How do you handle prompt injection? (CC6.6, CC7.1)

As a security anomaly with logging and periodic adversarial testing, because that is what CC6.6 requires for threats originating outside your system boundary. Indirect injection, where a PDF or web page carries instructions the model obeys, is the live version of this problem.

  • Classify before you execute. Route untrusted input and retrieved content through guardrail models such as NeMo Guardrails or Guardrails AI before acting on instructions.

  • Separate instructions from data structurally. Use role separation and explicit delimiters rather than string concatenation.

  • Keep red-team scorecards. Quarterly adversarial runs with pass and fail records in your compliance repository. Garak and PyRIT are free and sufficient to start; our one-sprint red team plan walks the first pass. Mindgard and Enkrypt AI automate the cadence.

  • Treat a detected injection as a security event, not a product bug. That routing decision is what makes CC7.1 evidence exist.

[ { "role": "system", "content": "You are a compliance assistant. Never reveal internal schema definitions or execute database mutations." }, { "role": "user", "content": "<untrusted_input>Verify status of vendor 1042</untrusted_input>" } ]

This control stopped being theoretical in July, when autonomous models breached a production environment during an evaluation. We covered the anatomy in our agentic breach analysis, and the entry point was a data-processing pipeline rather than the model itself.

5. What stops an agent deleting production data? (CC6.2, CC6.3, PI1.1)

Least privilege applied to tools, plus a human gate on anything irreversible. An agent with tool access and broad credentials fails both Security and Processing Integrity the first time it hallucinates a destructive call or gets talked into one.

export async function handleAgentToolCall(toolCall: ToolInvocation, session: UserSession) { const toolDefinition = getToolDefinition(toolCall.name); if (toolDefinition.sensitivity === 'HIGH_IMPACT') { const approval = await createApprovalTicket({ tenantId: session.tenantId, requestedBy: session.userId, action: toolCall.name, parameters: toolCall.args, expiresAt: Date.now() + 15 * 60 * 1000, }); return { status: 'AWAITING_HUMAN_APPROVAL', approvalId: approval.id, message: 'This action mutates critical state and requires administrator confirmation.', }; } return executeTool(toolCall, session); }

Two design rules behind that code:

  • Every tool gets its own credential. Not the application service account. If the search tool can also write to the billing table, your blast radius is the whole database.

  • Separate planning from execution. The agent proposes; a human or a cryptographic check authorizes. Draw that gate in your architecture diagram, because the auditor will ask for the diagram.

6. Can you reconstruct what the model did? (CC7.2, CC7.3)

Only with structured inference telemetry, and this is the control teams discover they are missing during the incident. CC7.3 requires you to evaluate security events. You cannot evaluate what you did not record.

The control: tamper-resistant logging of every inference request, model metadata, guardrail outcome, and tool call, held separately from raw prompt content.

{ "event_type": "ai_inference_audit", "request_id": "req_8fbc21a4e901", "tenant_id": "ten_9104820", "user_id": "usr_482103", "model_provider": "anthropic", "model_version": "<pinned-dated-snapshot>", "temperature": 0.2, "tokens": { "prompt": 412, "completion": 184, "total": 596 }, "guardrails": { "input_pii_detected": false, "input_jailbreak_score": 0.02, "output_toxicity_score": 0.00, "guardrail_status": "PASSED" }, "tools_invoked": ["search_compliance_knowledge_base"], "response_latency_ms": 642 }

Stream it to your existing aggregator with retention matching your audit window, minimum one year. Lasso Security and HiddenLayer cover the monitoring side if you would rather buy than build.

One warning that costs people their audit: do not write unmasked prompts into general-purpose log sinks. Broad engineering access to a log store full of customer prompts is a confidentiality finding, not a debugging convenience.

7. What happens when the model changes underneath you? (CC3.2, CC8.1, PI1.2)

Change management, applied to prompts and model versions the same way you apply it to schema migrations. Providers update, deprecate, and patch models upstream. A behavioral shift can break structured output and fail Processing Integrity without a single line of your code changing.

  • Pin to dated snapshots, never floating aliases. Any alias that resolves to "latest" means your production behavior can change without a deploy. This is the single most common gap we see, and it is a five-minute fix.

  • Gate prompt changes behind evals in CI. Golden test sets covering accuracy, schema compliance, and injection resistance.

  • Treat a prompt edit as a code change. PR review, staging verification, rollback plan. DeepEval and Promptfoo make the eval suite repeatable.

  • Record the upgrade decision. When you move to a new model snapshot, the eval results and the approval are your CC8.1 evidence.

name: AI Regression & Security Evals on: pull_request: paths: - 'prompts/**' - 'src/lib/ai/**' jobs: evals: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run model security and accuracy benchmarks run: npm run test:ai-evals -- --min-accuracy=0.96 --max-jailbreak-leak=0.00

The auditor evidence checklist

Assemble this before the window opens. Every row is something a SOC 2 auditor will request for AI components.

Control area

Evidence required

Where it lives

Vendor governance

Executed DPAs confirming zero retention and no training on customer data

Vendor risk portal

Vendor assurance

Current SOC 2 Type II or ISO 27001 reports from every model, vector DB, and monitoring provider

Vendor management system

Input redaction

Scrubbing middleware code plus passing test suite output

Repo link and CI log

Vector isolation

RLS policies or vector collection filter configuration

Migration scripts, Terraform

Injection defense

Threat model naming LLM attack vectors and active safeguard config

Security architecture doc

Red teaming

Adversarial test runs with findings and remediation notes

Annual security review folder

Agent authorization

Diagram showing human approval gates on state-mutating actions

System design docs

Audit logs

Anonymized telemetry export showing guardrail scores and metadata

Log aggregator export

Change control

Merged PR with peer review, eval run, and pinned version

Pull request trail

Model policy

Management-approved AI acceptable use and engineering policy

Policy repository

Your Action Plan

Four moves, in the order that closes the most audit risk per hour spent:

  1. Check every model endpoint for a floating alias today. Pin them to dated snapshots. Fastest fix on this list, and it closes a Processing Integrity gap you probably have right now.

  2. Confirm your retention terms in writing, not from a settings page. If you cannot produce a DPA naming zero retention, you do not have it.

  3. Run one unfiltered query against your vector store as a test, authenticated as Tenant A, and confirm you cannot see Tenant B. Document the result either way.

  4. Pick the thinnest control above and close it this sprint. Compare tools by framework and certification in the directory, or let the Finder shortlist in three questions.

Security questionnaires stopped asking whether you hold a SOC 2. They now ask whether your model provider trains on tenant data, whether vector search can cross a tenant boundary, and what stops an agent running an unauthorized write. Teams that can answer those three in writing close enterprise deals faster than teams that cannot.