Skip to content

Building SOC 2 Type 2-Ready SaaS with Granit

You close a deal with a large enterprise customer. Their security team sends a vendor assessment form. Question 3: “Do you have a SOC 2 Type 2 report?” If the answer is no, you go on a watch list. If it stays no for six months, the deal dies.

SOC 2 is not a regulation — but it is effectively a gate for selling to enterprise, healthcare, and financial services customers in North America and increasingly in Europe. This article maps Granit’s modules to the five Trust Service Criteria (TSC) so you know exactly which technical controls you already have in place when you start your audit preparation. The focus is on what the framework provides out of the box — and what your team must still build around it.

The AICPA framework defines two report types:

Type 1Type 2
What it certifiesControls exist at a point in timeControls operated effectively over a period (6–12 months)
Audit duration2–4 weeks6–12 months observation window
Customer valueLow (“you had the controls on day one”)High (“the controls actually work over time”)

The audit covers five Trust Service Criteria:

graph TD
    S["Security (CC)\nMandatory"]
    AV["Availability (A)"]
    C["Confidentiality (C)"]
    PI["Processing Integrity (PI)"]
    P["Privacy (P)"]
    S --> AV
    S --> C
    S --> PI
    S --> P

    style S fill:#fce4ec,stroke:#880e4f,color:#560027
    style AV fill:#e3f2fd,stroke:#1565c0,color:#0d47a1
    style C fill:#f3e5f5,stroke:#6a1b9a,color:#4a148c
    style PI fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20
    style P fill:#fff3e0,stroke:#e65100,color:#bf360c

Security (CC) is mandatory. The other four are optional — but most enterprise customers require Availability and Confidentiality as a minimum.

CC6 — Logical access controls (Security TSC)

Section titled “CC6 — Logical access controls (Security TSC)”

CC6 is the largest control family in SOC 2. It covers who can access what, how access is granted and revoked, and how you detect unauthorized access.

Granit.Authentication supports JWT Bearer tokens with provider packages for Keycloak, Entra ID, Cognito, and Google Cloud. For high-security scenarios, Granit.Authentication.DPoP adds proof-of-possession tokens — access tokens are cryptographically bound to the client’s private key and cannot be replayed by a third party even if intercepted:

Program.cs
builder.AddGranit(granit => granit
.AddModule<GranitAuthenticationJwtBearerKeycloakModule>()
.AddModule<GranitAuthenticationDPoPModule>());

RequireHttpsMetadata = true is enforced unconditionally. There is no way to disable it in production — the option validator rejects the configuration at startup. (CC6.1 requires encrypted communications for authentication.)

CC6.3 — Authorization and least-privilege

Section titled “CC6.3 — Authorization and least-privilege”

Granit.Authorization stores permissions in the database and evaluates them at runtime. There is no recompile cycle when your access model changes:

InvoiceEndpoints.cs
group.MapDelete("/{id:guid}", DeleteAsync)
.RequirePermission(InvoicePermissions.Invoices.Manage);

Permissions follow the [Group].[Resource].[Action] format, enforced by architecture tests. Granit.MultiTenancy isolates data at the query layer — permission policies can differ per tenant without duplicating application logic.

For machine-to-machine and third-party integrations, Granit.OpenIddict implements the full FAPI 2.0 security profile:

MechanismThreat it preventsRFC
PKCEAuthorization code interceptionRFC 7636
PARParameter tampering + PII leakage in URLRFC 9126
DPoPToken replay + theftRFC 9449
private_key_jwtClient impersonation without shared secretsRFC 7523

CC6.8 — Browser-side supply chain protection

Section titled “CC6.8 — Browser-side supply chain protection”

CC6.8 covers protection against malicious software, including supply chain attacks that target the browser. Granit.Bff implements the Backend For Frontend pattern: tokens are stored server-side in an encrypted, HttpOnly, SameSite=Strict session cookie. The browser never receives an access_token or refresh_token. A successful XSS attack — including a compromised npm dependency injected via a supply chain attack — cannot exfiltrate tokens that the browser never had access to.

CC7 — System operations and incident response

Section titled “CC7 — System operations and incident response”

Granit.Observability exports three pillars via a single module registration:

Program.cs
builder.AddGranit(granit => granit
.AddModule<GranitObservabilityModule>());
  • Logs: Serilog with [LoggerMessage] source generation. No string interpolation, no PII in log output, structured output to the Grafana Loki stack.
  • Metrics: every module emits named metrics via IMeterFactory (e.g. granit.authorization.permission.check, granit.persistence.query.duration). Mimir stores and alerts on them.
  • Traces: ActivitySource per module, correlated with logs via trace_id. Grafana Tempo provides flame graphs for any production request.

The Grafana LGTM stack ships as a Docker Compose overlay for local development. The same configuration deploys to production.

AuditedEntityInterceptor populates four fields on every database write — automatically, inside the same transaction:

Invoice.cs
// Inheriting FullAuditedEntity<Guid> gives you:
// CreatedAt, CreatedBy, ModifiedAt, ModifiedBy
// DeletedAt, DeletedBy, IsDeleted (via ISoftDeletable)
public sealed class Invoice : FullAuditedAggregateRoot<Guid>
{
public decimal Amount { get; private set; }
public static Invoice Create(decimal amount)
{
// Factory method — audit fields set by interceptor, not by application code
return new Invoice { Amount = amount };
}
}

Application code cannot skip the audit trail. The interceptor runs unconditionally on every SaveChangesAsync call for entities that inherit from AuditedEntity.

For business-level events, Granit.Webhooks records delivery attempts with SuspendedAt and SuspendedBy fields — the auditor can trace exactly when a webhook was suspended and who triggered the state change.

CC6.7 and Confidentiality TSC — Encryption

Section titled “CC6.7 and Confidentiality TSC — Encryption”

Granit.Encryption provides field-level encryption via IStringEncryptionService. In development, it uses AES-256-CBC with a local key. In production, Granit.Vault.HashiCorp delegates to HashiCorp Vault Transit:

PatientService.cs
public class PatientService(IStringEncryptionService encryption)
{
public async Task<Patient> CreateAsync(
string name,
string nationalId,
CancellationToken cancellationToken)
{
string encryptedId = await encryption
.EncryptAsync(nationalId, cancellationToken)
.ConfigureAwait(false);
return Patient.Create(name, encryptedId);
// nationalId never persisted in plaintext
}
}

The Vault module disables itself automatically in Development environment — no running Vault instance is needed locally. The same application code runs in both environments.

Granit.Vault.HashiCorp provides automatic key rotation via the Transit engine. Key rotation produces a new key version; existing ciphertext can be decrypted with old key versions until you explicitly rewrap it. The key never leaves Vault — this satisfies CC6.7 (encryption key management) and ISO 27001 A.8.24.

Crypto-shredding — right to erasure without deleting rows

Section titled “Crypto-shredding — right to erasure without deleting rows”

Granit.Privacy implements crypto-shredding for the GDPR right to erasure. When a tenant is offboarded:

  1. The tenant’s encryption key is destroyed in Vault.
  2. All encrypted fields in the database become permanently unreadable.
  3. The audit trail remains intact — no rows are deleted.

This resolves the compliance contradiction between GDPR (erase personal data) and SOC 2 (maintain immutable audit trails): the data is effectively erased without touching the rows that the audit trail references.

Granit.RateLimiting protects endpoints against abusive traffic patterns with configurable sliding window, fixed window, and token bucket policies. Rate limit violations return a 429 Too Many Requests with a Retry-After header — standard behavior that load testing and synthetic monitoring can validate for the auditor.

Granit.Caching reduces database pressure with FusionCache: a two-tier cache (in-memory L1 + distributed Redis L2) with fail-silent semantics. The cache layer is transparent to application code — queries that miss L1 and L2 fall through to the database without application changes.

The three tenant isolation strategies map to different availability risk profiles:

StrategyRisk containmentUse case
SharedDatabaseOne tenant’s heavy queries can slow othersCost-optimized SaaS
SchemaPerTenantSchema-level separation reduces cross-tenant query contentionMid-tier isolation
DatabasePerTenantFull isolation — one tenant’s database issue cannot affect othersEnterprise / regulated

Named EF Core 10 query filters (HasQueryFilter(name, expr)) make individual filters togglable without disabling all tenant isolation — useful for administrative operations and incident investigation.

Granit.Privacy implements the GDPR processing principles that also satisfy SOC 2 Privacy TSC criteria:

PatientPersonalDataProvider.cs
public class PatientPersonalDataProvider : IPersonalDataProvider
{
public string Category => "Medical records";
public async Task<PersonalDataExport> ExportAsync(
string userId, CancellationToken cancellationToken)
{
// Aggregated by the Privacy module for data portability requests
}
}
  • Data minimization: modules store only what they need. No central 40-column user profile table.
  • Processing restriction (IProcessingRestrictable): records can be suspended from processing on data subject request. Global query filter applied by ApplyGranitConventions.
  • No PII in logs: [LoggerMessage] source generation with explicit parameters prevents accidental PII interpolation into log strings. The Roslyn analyzer Granit.Analyzers flags direct string interpolation in log calls at compile time.
  • Data residency: DatabasePerTenant strategy supports EU-only database placement, satisfying auditors in regulated industries.
TSC criterionGranit controlModule
CC6.1 — AuthenticationJWT Bearer + DPoP + PKCEGranit.Authentication
CC6.1 — Encrypted transportRequireHttpsMetadata = trueGranit.Authentication
CC6.3 — AuthorizationDynamic RBAC/ABAC, runtime permissionsGranit.Authorization
CC6.3 — Tenant isolationQuery filters, 3 isolation strategiesGranit.MultiTenancy
CC6.6 — Third-party authFAPI 2.0 (PAR + DPoP + private_key_jwt)Granit.OpenIddict
CC6.7 — Encryption at restField-level AES / Vault TransitGranit.Encryption, Granit.Vault
CC6.7 — Key managementHashiCorp Vault Transit, auto-rotationGranit.Vault.HashiCorp
CC6.8 — Token protectionBFF pattern, HttpOnly cookiesGranit.Bff
CC7.1 — Anomaly detectionStructured logs + metrics + tracesGranit.Observability
CC7.2 — Audit trailInterceptor-based, immutable, actor-attributedGranit.Auditing
CC7.2 — Webhook auditDelivery log + suspension trackingGranit.Webhooks
A1.1 — AvailabilityRate limiting, distributed cachingGranit.RateLimiting, Granit.Caching
A1.2 — Tenant isolationDatabase/schema/filter strategiesGranit.MultiTenancy, Granit.Persistence
C1.1 — ConfidentialityField encryption + crypto-shreddingGranit.Encryption, Granit.Privacy
P3.1 — Data minimizationModule-scoped storage, no shared profile tableArchitecture
P4.1 — Personal data portabilityIPersonalDataProvider aggregationGranit.Privacy
P8.1 — Right to erasureCrypto-shredding via Vault key destructionGranit.Privacy

SOC 2 auditors do not just review your code. They review:

  • Incident response procedures: a documented runbook, not just Grafana dashboards
  • Access review cadence: quarterly reviews of who has production access
  • Change management: pull request policies, approval gates, deployment procedures
  • Employee training: security awareness, phishing simulation records
  • Vendor management: due diligence records for your own third-party dependencies
  • Penetration testing: an annual external pen test with findings and remediation

Granit gives you the technical controls. The observation window requires that those controls operated effectively — which means your team followed the procedures, reviewed the alerts, and acted on the findings every day for six to twelve months.

  • SOC 2 Type 2 certifies that your controls operated effectively over 6–12 months — the observation window starts when you engage an auditor, not when you think you are ready.
  • Granit.Authorization + Granit.Authentication.DPoP + Granit.Bff covers the CC6 access control family; Granit.Observability + Granit.Auditing covers CC7.
  • Granit.Vault.HashiCorp satisfies CC6.7 key management — Vault Transit provides key rotation and HSM backing with the key never leaving Vault.
  • Granit.Privacy crypto-shredding resolves the GDPR-vs-SOC2 audit trail contradiction: keys are destroyed, rows stay.
  • The framework covers technical controls. Operational controls — procedures, training, access reviews — are the part that requires organizational investment.