Language:English VersionChinese Version

SOC 2 compliance is one of those things that every B2B SaaS company eventually needs and no developer wants to deal with. It sits at the intersection of engineering, legal, and business operations — a space where most engineers feel uncomfortable and most compliance consultants speak a language that bears no resemblance to actual software development.

I went through SOC 2 Type II certification at a 30-person startup where I was the lead engineer responsible for the technical controls. The process took 8 months, cost approximately $45,000 (auditor fees, tooling, and engineering time), and taught me that 70% of SOC 2 readiness is engineering work that any competent team already does — they just need to document it properly.

This article translates SOC 2 from auditor-speak into engineer-speak. No legalese. Concrete technical controls with implementation details.

What SOC 2 Actually Is

SOC 2 is an auditing standard created by the American Institute of Certified Public Accountants (AICPA). It evaluates your organization against five “Trust Service Criteria”:

  1. Security (required): Protection against unauthorized access
  2. Availability: System uptime and reliability
  3. Processing Integrity: Data processing is complete, valid, and accurate
  4. Confidentiality: Protection of confidential information
  5. Privacy: Personal information handling

Most startups certify against Security only, or Security + Availability. You choose which criteria to include.

Type I is a point-in-time assessment: “Do these controls exist today?” Type II covers a period (usually 6-12 months): “Did these controls operate effectively over this period?” Enterprise customers almost always require Type II.

The Technical Controls That Matter

SOC 2 defines criteria, not specific controls. Your auditor evaluates whether your controls satisfy the criteria. This means you have flexibility in how you implement things, as long as the what is covered.

1. Access Control (CC6.1-CC6.8)

This is the largest category and the one where most engineering work lives.

What the auditor wants to see:

  • Role-based access control (RBAC) with least-privilege principles
  • Multi-factor authentication (MFA) for all internal systems
  • Access reviews (quarterly is standard)
  • Automated deprovisioning when employees leave
  • Audit logs of access changes

Technical implementation:

# Terraform: IAM roles with least-privilege access
# This is what auditors love — infrastructure as code
# serves as both implementation AND documentation

resource "aws_iam_role" "app_server" {
  name = "app-server-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "ec2.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy" "app_server" {
  name = "app-server-policy"
  role = aws_iam_role.app_server.id

  # Principle of least privilege: only the permissions needed
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject"
        ]
        Resource = "arn:aws:s3:::myapp-uploads/*"
      },
      {
        Effect = "Allow"
        Action = [
          "sqs:SendMessage",
          "sqs:ReceiveMessage",
          "sqs:DeleteMessage"
        ]
        Resource = aws_sqs_queue.app_events.arn
      }
    ]
  })
}

# No wildcards. No admin access. Auditors verify this.
# Automated access review script
# Run quarterly, output goes into evidence folder

#!/bin/bash
echo "=== Quarterly Access Review: $(date +%Y-%m-%d) ==="

echo -e "\n## AWS IAM Users"
aws iam list-users --query "Users[].{User:UserName,Created:CreateDate,LastLogin:PasswordLastUsed}" --output table

echo -e "\n## Users with Console Access"
aws iam get-credential-report --output text --query Content | base64 -d |   awk -F, '{if($4=="true") print $1, $5, $11}'

echo -e "\n## Users without MFA"
aws iam list-users --query "Users[].UserName" --output text | while read user; do
  mfa=$(aws iam list-mfa-devices --user-name "$user" --query "MFADevices" --output text)
  if [ -z "$mfa" ]; then
    echo "WARNING: $user has no MFA enabled"
  fi
done

echo -e "\n## GitHub Organization Members"
gh api orgs/myorg/members --jq ".[].login"

echo -e "\n## Database Users"
psql "$DATABASE_URL" -c "SELECT usename, valuntil FROM pg_user ORDER BY usename;"

2. Change Management (CC8.1)

This is where your existing engineering practices likely already satisfy SOC 2 requirements — you just need to prove it.

What the auditor wants:

  • All changes go through code review before deployment
  • Separate development, staging, and production environments
  • Automated testing before production deployment
  • Ability to roll back changes

Technical implementation:

# GitHub branch protection rules (serves as evidence)
# Settings > Branches > main

# Required:
# - Require pull request reviews before merging (1+ reviewer)
# - Require status checks to pass (CI must pass)
# - Require branches to be up to date before merging
# - Do not allow bypassing the above settings

# Your CI pipeline IS your change management control
# .github/workflows/ci.yml
name: CI Pipeline
on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
      - run: npm run lint
      - run: npm run type-check

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  # This job existing + branch protection = SOC 2 change management

3. Logging and Monitoring (CC7.1-CC7.4)

Auditors want evidence that you can detect unauthorized access, unusual activity, and system failures.

// Structured audit logging — this is SOC 2 gold
// Every security-relevant action gets a structured log entry

interface AuditEvent {
  timestamp: string;
  actor: {
    id: string;
    email: string;
    ip: string;
    userAgent: string;
  };
  action: string;
  resource: {
    type: string;
    id: string;
  };
  result: "success" | "failure";
  metadata: Record;
}

class AuditLogger {
  async log(event: AuditEvent): Promise {
    // Write to append-only audit log
    // Auditors verify this log cannot be modified or deleted
    await this.writeToImmutableStore(event);

    // Alert on suspicious patterns
    if (this.isSuspicious(event)) {
      await this.alertSecurityTeam(event);
    }
  }

  private isSuspicious(event: AuditEvent): boolean {
    return (
      event.result === "failure" && event.action === "login" ||
      event.action === "role.changed" ||
      event.action === "data.exported" ||
      event.action === "api_key.created"
    );
  }
}

// Usage throughout the application
await auditLog.log({
  timestamp: new Date().toISOString(),
  actor: { id: user.id, email: user.email, ip: req.ip,
           userAgent: req.headers["user-agent"] },
  action: "project.deleted",
  resource: { type: "project", id: projectId },
  result: "success",
  metadata: { projectName: project.name },
});

4. Encryption (CC6.7)

In transit: TLS everywhere. This means HTTPS for all web traffic, TLS for database connections, encrypted connections between services.

At rest: Database encryption, encrypted backups, encrypted file storage.

# PostgreSQL: enforce TLS connections
# postgresql.conf
ssl = on
ssl_cert_file = '/etc/ssl/certs/server.crt'
ssl_key_file = '/etc/ssl/private/server.key'
ssl_min_protocol_version = 'TLSv1.3'

# pg_hba.conf: reject unencrypted connections
# TYPE  DATABASE  USER  ADDRESS       METHOD
hostssl all       all   0.0.0.0/0     scram-sha-256
hostnossl all     all   0.0.0.0/0     reject
# AWS S3: enforce encryption at rest
resource "aws_s3_bucket_server_side_encryption_configuration" "uploads" {
  bucket = aws_s3_bucket.uploads.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3_encryption.arn
    }
    bucket_key_enabled = true
  }
}

# Deny unencrypted uploads
resource "aws_s3_bucket_policy" "enforce_encryption" {
  bucket = aws_s3_bucket.uploads.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Sid       = "DenyUnencryptedUploads"
      Effect    = "Deny"
      Principal = "*"
      Action    = "s3:PutObject"
      Resource  = "${aws_s3_bucket.uploads.arn}/*"
      Condition = {
        StringNotEquals = {
          "s3:x-amz-server-side-encryption" = "aws:kms"
        }
      }
    }]
  })
}

5. Incident Response (CC7.3-CC7.5)

You need a documented incident response plan. It does not need to be a 50-page document. A clear runbook that your team actually follows is what auditors want.

# incident-response.md — Keep it actionable

## Severity Levels

| Level | Definition | Response Time | Example |
|-------|-----------|---------------|---------|
| SEV1  | Data breach or complete outage | 15 min | Unauthorized data access |
| SEV2  | Significant degradation | 1 hour | API error rate > 5% |
| SEV3  | Minor issue, workaround exists | 4 hours | Non-critical feature broken |

## Response Steps

### 1. Detect and Triage (First 15 minutes)
- Acknowledge the alert in PagerDuty
- Determine severity level
- Open incident channel: #incident-YYYY-MM-DD in Slack
- Assign Incident Commander (on-call engineer)

### 2. Contain (First hour)
- Identify the blast radius
- Implement immediate mitigation (rollback, feature flag, rate limit)
- Communicate status to affected customers if SEV1/SEV2

### 3. Resolve
- Implement fix
- Verify fix in staging, then production
- Monitor for recurrence (1 hour minimum)

### 4. Post-Incident (Within 48 hours)
- Write incident report (what happened, timeline, root cause)
- Identify action items with owners and deadlines
- Share with team in weekly meeting

The Evidence Collection System

SOC 2 is fundamentally an evidence game. Your auditor will request evidence for every control. Automate evidence collection from day one:

# Directory structure for SOC 2 evidence
evidence/
├── 2026-Q1/
│   ├── access-reviews/
│   │   ├── aws-iam-review-2026-01-15.txt
│   │   ├── github-members-2026-01-15.txt
│   │   └── database-users-2026-01-15.txt
│   ├── change-management/
│   │   ├── github-branch-protection-screenshot.png
│   │   └── ci-pipeline-runs-summary.csv
│   ├── vulnerability-scans/
│   │   ├── snyk-report-2026-01.pdf
│   │   ├── snyk-report-2026-02.pdf
│   │   └── snyk-report-2026-03.pdf
│   └── incident-reports/
│       └── incident-2026-02-18-api-outage.md
# Automate evidence collection with a monthly cron job
#!/bin/bash
# collect-evidence.sh — runs on the 1st of each month

MONTH=$(date +%Y-%m)
DIR="evidence/$(date +%Y)-Q$((( $(date +%-m) - 1) / 3 + 1))"
mkdir -p "$DIR"/{access-reviews,change-management,vulnerability-scans}

# AWS access review
aws iam generate-credential-report
sleep 5
aws iam get-credential-report --query Content --output text |   base64 -d > "$DIR/access-reviews/aws-iam-$MONTH.csv"

# GitHub PR merge report (change management evidence)
gh api graphql -f query='
  query {
    repository(owner: "myorg", name: "myapp") {
      pullRequests(last: 100, states: MERGED) {
        nodes {
          title
          mergedAt
          reviews(first: 5) { nodes { state author { login } } }
        }
      }
    }
  }
' > "$DIR/change-management/merged-prs-$MONTH.json"

echo "Evidence collected for $MONTH"

Tooling That Reduces the Pain

Several platforms automate SOC 2 compliance tracking. They are worth the investment if you want to minimize engineering time:

  • Vanta ($5,000-15,000/year): Connects to your infrastructure (AWS, GitHub, HR systems) and continuously monitors controls. Auto-collects most evidence. This is what most startups use.
  • Drata (similar pricing): Similar to Vanta with slightly different integrations. Strong GRC (governance, risk, compliance) features.
  • Secureframe: More affordable option for smaller teams.

These tools do not replace the engineering work — you still need to implement the controls. But they dramatically reduce the evidence collection burden and flag gaps before your auditor finds them.

Timeline and Cost for a Typical Startup

Phase Duration Cost
Gap assessment 2 weeks $0 (internal) or $5K (consultant)
Remediation 2-3 months Engineering time (varies)
Compliance platform setup 1 week $5K-15K/year
Type I audit 1 month $10K-25K
Observation period (Type II) 6-12 months Operating costs
Type II audit 1 month $15K-35K

Total first-year cost for a 20-person startup: $30K-$75K, depending on your starting point and choice of auditor.

What Engineers Get Wrong

  1. Treating it as a checkbox exercise. SOC 2 controls should improve your actual security posture. If you implement a control just to pass the audit and then ignore it, you are wasting money and creating false confidence.
  2. Over-engineering controls. You do not need a zero-trust mesh network for SOC 2. You need MFA, encryption, access reviews, and logging. Start with the basics before adding complexity.
  3. Not involving engineers early. When compliance is owned entirely by a GRC team, the controls do not match the actual infrastructure. Engineers should define the controls; the GRC team should manage the evidence and auditor relationship.
  4. Waiting until a customer requires it. SOC 2 readiness takes 6-12 months. If a prospect requires it during a sales cycle, you have already lost. Start the process before you need it.

SOC 2 is not as painful as its reputation suggests. If your team already does code review, runs CI/CD, uses MFA, and has basic monitoring, you are 60% of the way there. The remaining 40% is documentation, evidence collection, and filling specific gaps. Approach it as an engineering project — with clear requirements, measurable outcomes, and automated processes — and it becomes manageable.

By

Leave a Reply

Your email address will not be published. Required fields are marked *