DEV Community

JF Meyers
JF Meyers

Posted on

Your Enterprise Customer Just Asked for a SOC 2 Type 2 Report. Now What?

You are three weeks from closing a six-figure deal. The customer's security team sends a vendor assessment form. Question 4: "Do you have a SOC 2 Type 2 report?"

You don't.

The deal goes on hold. Six months later, it dies.

This is happening more and more. SOC 2 Type 2 is no longer just a nice-to-have for companies selling to US enterprise — it is a procurement gate. And for .NET teams, the path from "we should probably do this" to "we have the controls in place" is less obvious than it should be.

This article maps the five SOC 2 Trust Service Criteria (TSC) to concrete .NET controls, and shows how Granit — an open-source modular .NET 10 framework — covers most of the technical side out of the box.


SOC 2 in one paragraph

SOC 2 is an AICPA standard. It comes in two flavors:

  • Type 1: your controls exist at a point in time. Cheap to get. Low customer value.
  • Type 2: your controls operated effectively over a 6–12 month observation window. Expensive. What enterprise customers actually want.

The audit covers up to five Trust Service Criteria. Only Security (CC) is mandatory. Most enterprise customers also require Availability and Confidentiality.

The key thing to internalize: the auditor does not certify your code. They certify that your controls operated and that your team followed procedures. Granit gives you the technical controls. The processes, runbooks, and evidence collection are still on you.


CC6 — Logical access controls

This is the biggest control family. It covers authentication, authorization, and how you prevent unauthorized access.

CC6.1 — Strong authentication

Granit's BFF module keeps tokens off the browser entirely. The access_token and refresh_token live server-side in an encrypted HttpOnly, SameSite=Strict session cookie. No XSS attack — including a compromised npm dependency — can steal a token the browser never had.

// Program.cs
builder.AddGranit(granit => granit
    .AddModule<GranitBffModule>()
    .AddModule<GranitBffYarpModule>());
Enter fullscreen mode Exit fullscreen mode

For machine-to-machine flows, Granit.Authentication.DPoP implements RFC 9449 proof-of-possession tokens. Even if an attacker intercepts an access token, they cannot replay it without the client's private key.

The full FAPI 2.0 security profile — PKCE (RFC 7636) + PAR (RFC 9126) + DPoP (RFC 9449) + private_key_jwt (RFC 7523) — is available via Granit.OpenIddict.

CC6.3 — Authorization and least-privilege

Granit.Authorization stores permissions in the database and evaluates them at runtime. No recompile cycle when your access model changes. Permissions follow a strict [Group].[Resource].[Action] format enforced by architecture tests.

// Endpoint declaration
group.MapDelete("/{id:guid}", DeleteAsync)
    .RequirePermission(InvoicePermissions.Invoices.Manage);
Enter fullscreen mode Exit fullscreen mode

Per-tenant permission policies are built-in via Granit.MultiTenancy — the same endpoint can enforce different access rules for different customer organizations.

CC6.7 — Encryption at rest

Granit.Encryption provides IStringEncryptionService for field-level encryption. In development, it falls back to AES-256-CBC automatically. In production, Granit.Vault.HashiCorp delegates to HashiCorp Vault Transit — the key never leaves Vault:

public class PatientService(IStringEncryptionService encryption)
{
    public async Task<Patient> CreateAsync(
        string name,
        string nationalId,
        CancellationToken ct)
    {
        // nationalId is encrypted before it touches the database
        string encrypted = await encryption.EncryptAsync(nationalId, ct)
            .ConfigureAwait(false);

        return Patient.Create(name, encrypted);
    }
}
Enter fullscreen mode Exit fullscreen mode

The Vault module disables itself in Development — no local Vault instance needed for day-to-day development.

CC6.8 — Malware and supply chain

See the BFF section above. Tokens never in localStorage. Supply chain XSS attacks are neutralized by design, not by CSP headers.


CC7 — System operations and monitoring

CC7.1 — Anomaly detection

Granit.Observability wires Serilog + OpenTelemetry in one module registration:

builder.AddGranit(granit => granit
    .AddModule<GranitObservabilityModule>());
Enter fullscreen mode Exit fullscreen mode

Every module emits:

  • Structured logs via [LoggerMessage] source generation — no string interpolation, no accidental PII in log output
  • Metrics via IMeterFactory — standardized naming (granit.authorization.permission.check, granit.persistence.query.duration)
  • Distributed traces via ActivitySource — one per module, correlated with logs via trace_id

The Grafana LGTM stack (Loki + Grafana + Tempo + Mimir) ships as a Docker Compose overlay. A trace_id on every request means you can reconstruct the exact sequence of events for any incident — which is what the auditor wants to see.

CC7.2 — Audit trail

This is the one control teams most often try to implement manually and get wrong. With Granit, it is automatic:

// Inheriting AuditedEntity gives you these four fields on every write:
// CreatedAt  → IClock.Now (UTC, always)
// CreatedBy  → ICurrentUserService.UserId (or "system" for jobs)
// ModifiedAt → IClock.Now
// ModifiedBy → ICurrentUserService.UserId

public sealed class Invoice : FullAuditedAggregateRoot<Guid>
{
    public decimal Amount { get; private set; }

    public static Invoice Create(decimal amount) =>
        new() { Amount = amount };
    // Audit fields set by interceptor inside SaveChangesAsync — not by application code
}
Enter fullscreen mode Exit fullscreen mode

AuditedEntityInterceptor runs inside the same database transaction as the business write. You cannot have a committed write without an audit record. You cannot accidentally skip it.

For business-level events ("Invoice approved by Marie"), the Timeline module adds a human-readable activity log with actor, timestamp, and Markdown body — independent of the field-level audit trail.


Confidentiality TSC — crypto-shredding

Here is a problem that comes up in every SOC 2 + GDPR scenario:

  • GDPR Art. 17 says you must be able to erase a user's data.
  • SOC 2 CC7.2 says your audit trail must be immutable.

These appear to contradict. If you delete rows to satisfy GDPR, your audit trail references records that no longer exist. If you keep the rows, you're not actually erasing the data.

Granit.Privacy resolves this with crypto-shredding:

  1. Each tenant's sensitive fields are encrypted with a tenant-specific key stored in Vault.
  2. On erasure request, the key is destroyed in Vault.
  3. All encrypted fields become permanently unreadable — mathematically erased.
  4. The audit trail rows stay intact. The data they reference is gone.

No rows deleted. Full erasure. Intact audit trail. Compliant with both.


Availability TSC

Granit.RateLimiting protects against abusive traffic patterns. Standard 429 Too Many Requests with Retry-After header — easily validated in load tests for the auditor's evidence package.

Granit.MultiTenancy supports three isolation strategies. The DatabasePerTenant strategy means one tenant's database issue cannot cascade to others — a common availability concern for SaaS audits.

Strategy Risk containment
SharedDatabase Logical (query filter)
SchemaPerTenant Schema-level separation
DatabasePerTenant Full physical isolation

Privacy TSC

  • [LoggerMessage] source generation enforces that no PII can slip into log strings at compile time. The Roslyn analyzer flags violations before they reach CI.
  • IProcessingRestrictable adds a global EF Core query filter for processing restriction (GDPR Art. 18) — applied by ApplyGranitConventions, never forgettable.
  • IPersonalDataProvider aggregates personal data exports across all modules for data portability requests (GDPR Art. 15 / SOC 2 P4.1).

What you still need to do

Granit handles the technical controls. The audit also requires:

  • Incident response runbook — a documented procedure, not just Grafana dashboards
  • Access review cadence — quarterly reviews of who has production access, with evidence
  • Change management — pull request policies, deployment approval gates
  • Employee security training — awareness + phishing simulation records
  • Annual penetration test — with findings and remediation tracking
  • Vendor due diligence — security assessments for your own third-party integrations

The observation window starts when you engage your auditor. Starting with the technical controls already in place means your preparation budget goes to the operational side — which is where the actual audit risk lives.


SOC 2 TSC compliance matrix

Criterion Control Granit module
CC6.1 — Authentication JWT Bearer + DPoP + PKCE Granit.Authentication
CC6.1 — Transport encryption RequireHttpsMetadata = true Granit.Authentication
CC6.3 — Authorization Dynamic RBAC/ABAC, runtime permissions Granit.Authorization
CC6.3 — Tenant isolation Query filters, 3 strategies Granit.MultiTenancy
CC6.6 — Third-party auth FAPI 2.0 (PAR + DPoP + private_key_jwt) Granit.OpenIddict
CC6.7 — Encryption at rest Field-level AES / Vault Transit Granit.Encryption, Granit.Vault
CC6.7 — Key management HashiCorp Vault, auto-rotation Granit.Vault.HashiCorp
CC6.8 — Token protection BFF pattern, HttpOnly cookies Granit.Bff
CC7.1 — Anomaly detection Structured logs + metrics + traces Granit.Observability
CC7.2 — Audit trail Interceptor-based, immutable Granit.Auditing
A1.1 — Availability Rate limiting, distributed cache Granit.RateLimiting, Granit.Caching
A1.2 — Tenant isolation DB/schema/filter strategies Granit.MultiTenancy
C1.1 — Confidentiality Field encryption + crypto-shredding Granit.Encryption, Granit.Privacy
P4.1 — Data portability IPersonalDataProvider aggregation Granit.Privacy
P8.1 — Right to erasure Crypto-shredding via Vault key destruction Granit.Privacy

TL;DR

SOC 2 Type 2 is a process audit, not a code audit. But the technical controls need to be in place and operating before the observation window starts.

If you're building on .NET, Granit gives you CC6, CC7, and the Confidentiality/Privacy TSC controls out of the box: dynamic RBAC, automatic audit trails, field-level encryption with Vault key management, BFF token protection, and crypto-shredding for GDPR erasure without breaking your audit trail.

The hard part — runbooks, access reviews, pen tests, employee training — is still on you. But starting with a framework that makes the compliant path the default path means you're spending your prep time on the right things.

Full documentation: granit-fx.dev
Source code: github.com/granit-fx/granit-dotnet (Apache 2.0)

Top comments (0)