Most teams treat SOC 2 as a project with an end date. The auditor treats it as a question about every day in a six-month window. Terraform is how you answer for all of them at once.
A B2B SaaS team I advised was three weeks from the start of their SOC 2 Type II observation window. They had a checklist tool, a stack of policies, and a plan to "tighten things up before the window opens." The plan was the problem. Type II does not sample audit day. It samples the whole window. A reviewer pulls a random change from month four and asks: was this peer-reviewed, did it pass a control gate, can you show the encryption was on the entire time. Tightening up the week before the window opens proves nothing about month four.
The teams that pass Type II cleanly do not scramble before the window. They make the controls a property of the infrastructure, so the control is operating every single day by construction, and the evidence is produced continuously as a side effect. That is exactly what Terraform plus a couple of native AWS services give you. This post maps the SOC 2 Trust Services Criteria to a Terraform module layout, with the code, and shows where the operating-effectiveness evidence comes from.
This is the SOC 2 counterpart to the HIPAA Terraform reference architecture. The module shapes are the same; the framing differs, because where HIPAA names safeguards, SOC 2 grades operating effectiveness over time. If you are deciding which controls overlap, the HIPAA vs SOC 2 control comparison maps where they meet and where they part.
Section 01What SOC 2 actually asks of your infrastructure
SOC 2 is built on the Trust Services Criteria. The Security category, the Common Criteria CC1 through CC9, is required in every report. Availability, Confidentiality, Processing Integrity, and Privacy are added when they are relevant to what you sell. Most infrastructure work lands in a handful of the Common Criteria.
- CC6.1 Logical access. Who can reach systems and data, under what identity, with what privilege.
- CC6.6 and CC6.7 Boundary protection and encryption. Data protected in transit and at rest, the perimeter controlled.
- CC7.1 and CC7.2 Detection and monitoring. The ability to detect configuration changes and security events.
- CC8.1 Change management. Changes are authorized, tested, and reviewed before they reach production.
The distinction that drives everything is Type I versus Type II. Type I asks whether the control was designed correctly at a point in time. Type II asks whether it operated effectively across the observation window, usually three to twelve months. Type II is what customers ask for, and Type II is the one that punishes point-in-time fixes.
Section 02Why Terraform fits Type II specifically
A control that lives in a runbook operates when someone remembers to run it. A control that lives in Terraform operates on every apply, the same way, with a reviewable record. For a framework grading consistency over months, that difference is the whole game.
Consistency is structural. The encryption is on because the module sets it, not because someone checked a box. There is no month where it quietly lapsed, because lapsing would require a code change that the gate would reject.
The change record is the evidence. Every modification is a pull request with an author, a reviewer, and a timestamp. When the auditor samples a change from month four, the pull request is the CC8.1 evidence, already captured, no reconstruction required.
Detection is continuous. Native config-monitoring services watch the live account and flag drift away from the codified baseline. That continuous record is the CC7.1 evidence, generated every day whether or not anyone is looking.
Section 03CC6.1: logical access as a role catalog
CC6.1 is about restricting logical access to authorized users and processes. The pattern is a small set of named roles with explicit, narrow policies, assumed through a central identity provider, with no standing human credentials.
# modules/iam/app_role.tf
# A workload role scoped to exactly the resources the service needs.
resource "aws_iam_role" "service" {
name = "svc-${var.service_name}"
max_session_duration = 3600
assume_role_policy = data.aws_iam_policy_document.oidc_trust.json
}
resource "aws_iam_role_policy" "service" {
role = aws_iam_role.service.id
policy = data.aws_iam_policy_document.scoped.json
}
data "aws_iam_policy_document" "scoped" {
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject"]
resources = ["${var.data_bucket_arn}/${var.service_name}/*"]
}
}
The role can reach one prefix in one bucket. An auditor asking "how do you restrict access" reads the policy and sees the answer is enforced, not described. Federated trust means the access list is your identity provider, not a pile of IAM users nobody prunes.
Section 04CC6.6 and CC6.7: encryption in transit and at rest
The encryption criteria are satisfied by customer-managed keys for data at rest and enforced TLS for data in transit. Both belong in code so neither can lapse silently.
# modules/data-store/rds.tf
resource "aws_db_instance" "app" {
identifier = var.name
engine = "postgres"
storage_encrypted = true
kms_key_id = var.kms_key_arn # customer-managed key
multi_az = true # also serves Availability (A1)
deletion_protection = true
}
# Enforce TLS in transit at the load balancer. No plaintext listener.
resource "aws_lb_listener" "https" {
load_balancer_arn = var.alb_arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.cert_arn
}
Pinning a modern ssl_policy in code is the control. There is no listener that accepts plaintext, because the module does not create one. The HIPAA version of the key management module, with rotation and split key policies, is in the HIPAA Terraform reference architecture, and it applies here unchanged.
Section 05CC7.1 and CC7.2: detection that runs itself
The detection criteria are where SOC 2 most rewards automation. The job is to notice when the environment drifts from its intended configuration or when a security-relevant event occurs. AWS Config plus an organization CloudTrail, both codified, produce that detection and the evidence trail behind it.
# modules/detection/config.tf
resource "aws_config_configuration_recorder" "this" {
name = "soc2-recorder"
role_arn = var.config_role_arn
recording_group { all_supported = true }
}
# A managed rule that flags any unencrypted volume. Continuous evidence
# that the encryption control held every day of the window.
resource "aws_config_config_rule" "ebs_encrypted" {
name = "encrypted-volumes"
source {
owner = "AWS"
source_identifier = "ENCRYPTED_VOLUMES"
}
depends_on = [aws_config_configuration_recorder.this]
}
# Route non-compliance to an alert so detection is timely, not retrospective.
resource "aws_config_config_rule" "sg_open" {
name = "restricted-ssh"
source {
owner = "AWS"
source_identifier = "INCOMING_SSH_DISABLED"
}
}
Each Config rule emits a timestamped compliant or non-compliant evaluation, continuously. When the auditor asks for evidence that the control operated through the window, the Config timeline is the answer, and it was generated automatically the entire time.
Know how many controls you are signing up to operate.
The number of controls you have to run and evidence for SOC 2 is set by your architecture. Our Compliance Scope and Audit-Cost Estimator maps your setup to the SOC 2 baseline and returns your control count, your first-cycle evidence cost, and the moves that shrink the scope before the observation window starts.
Estimate your scope →Section 06CC8.1: the Terraform workflow is the change control
CC8.1 requires that changes are authorized, designed, tested, and approved before they go live. A Terraform workflow satisfies this without a separate change-management process, because the workflow itself is the process: propose in a pull request, review, gate, apply.
# .github/workflows/terraform.yml (excerpt)
# The pipeline IS the CC8.1 control: review + gate before apply.
jobs:
plan:
steps:
- run: terraform plan -out=plan.tfplan
- run: terraform show -json plan.tfplan > plan.json
- run: conftest test plan.json # policy gate fails the build
apply:
needs: plan
if: github.ref == 'refs/heads/main' # only reviewed, merged code
environment: production # named approver required
steps:
- run: terraform apply plan.tfplan
Branch protection requires a review before merge. The production environment requires a named approver before apply. The policy gate blocks a non-compliant plan. Three structural controls, all evidenced by the pipeline's own logs. The same gating pattern, applied to deploys instead of infrastructure, is in the HIPAA CI/CD implementation guide.
Section 07Where the Type II evidence comes from
The payoff of building it this way is that evidence collection stops being a quarterly fire drill. Each control produces its own evidence as a property of operating. This is the table that turns "trust us" into "here is the artifact."
| Criterion | Terraform module | Evidence the auditor samples |
|---|---|---|
| CC6.1 Logical access | iam | Scoped role policies, federation, pull-request history |
| CC6.6 Boundary / transit | network | TLS-only listeners, security group rules in state |
| CC6.7 Data at rest | kms + data-store | CMKs, encrypted stores, Config evaluations |
| CC7.1 Detection | detection | Config rule timeline, drift evaluations |
| CC7.2 Monitoring | detection | CloudTrail events, alert routing |
| CC8.1 Change management | policy + pipeline | PR reviews, gate results, apply approvals |
Section 08Common mistakes to avoid
- Tightening up right before the window. Type II samples the whole window. Stand the controls up in code well before it opens, so month one looks like month six.
- Detection with no alerting. A Config rule that flags non-compliance into a dashboard nobody reads is not detection. Route findings to an alert with an owner.
- Manual changes alongside Terraform. One console change outside the workflow breaks the
CC8.1story for the whole window. Everything goes through the pipeline, or the control is not real. - Treating the GRC tool as the control. A compliance automation platform collects evidence; it does not enforce the control. The enforcement lives in Terraform and the gate. The tool reads the result.
- One giant role for the app. A broad role makes
CC6.1impossible to defend. Scope each service to exactly what it touches.
Section 09Conclusion
SOC 2 Type II is a question about consistency over time, and consistency over time is what infrastructure as code is for. Put the Trust Services Criteria in modules, gate every change through a reviewed pipeline, and let Config and CloudTrail produce the timeline. The observation window stops being something you brace for and becomes something your infrastructure documents on its own.
The teams that struggle with Type II are the ones proving their controls by hand at sample time. The teams that breeze through it built the control and the evidence as the same artifact, months earlier, and never thought about it again until the auditor asked.
If you are heading into a SOC 2 Type II window: Stonebridge builds the Terraform and the detection pipeline that produce your operating-effectiveness evidence, and maps each module to the Trust Services Criteria your auditor will sample. See how we engage, founder-led, fixed fee.
Keep reading: the HIPAA version of this module pattern is the HIPAA Terraform reference architecture. Where the two frameworks overlap and diverge is in HIPAA CI/CD vs SOC 2 CI/CD. Size your control count with the scope estimator.