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.

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.

Before the window opens

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."

CriterionTerraform moduleEvidence the auditor samples
CC6.1 Logical accessiamScoped role policies, federation, pull-request history
CC6.6 Boundary / transitnetworkTLS-only listeners, security group rules in state
CC6.7 Data at restkms + data-storeCMKs, encrypted stores, Config evaluations
CC7.1 DetectiondetectionConfig rule timeline, drift evaluations
CC7.2 MonitoringdetectionCloudTrail events, alert routing
CC8.1 Change managementpolicy + pipelinePR reviews, gate results, apply approvals

Section 08Common mistakes to avoid

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.


About the author

Lucas Jones, Founder

Founder and Principal Platform Engineer at Stonebridge Tech Solutions. Six years building cloud infrastructure and CI/CD pipelines in regulated environments, including HIPAA, FedRAMP, and SOC 2 work for healthcare and defense engineering teams across AWS, GCP, Azure, and OCI.

See how we engage on compliance cloud work →