Skip to content
Unverified — AI-generated content. Help verify this page

SOC 2 for Engineers

SOC 2 (System and Organization Controls 2) is an auditing framework developed by the AICPA (American Institute of Certified Public Accountants) that evaluates how well an organization protects customer data. For B2B SaaS companies, SOC 2 is table stakes — enterprise customers will not sign contracts without it. For engineers, SOC 2 defines the security controls you need to build into your systems: access management, encryption, monitoring, change management, and incident response.

Unlike GDPR (which is a law with penalties), SOC 2 is a voluntary audit. An independent auditor evaluates your controls and issues a report. There are two types:

Report TypeWhat It CoversDuration
SOC 2 Type IAre controls properly designed? (point-in-time snapshot)~2-4 weeks of audit
SOC 2 Type IIAre controls operating effectively over time? (observation period)3-12 month observation + audit

Type II is what customers want — it proves your controls actually work over a sustained period, not just that they exist on paper.

Trust Services Criteria

SOC 2 evaluates organizations against five Trust Services Criteria (TSC). Security is mandatory; the other four are optional depending on what your service does.

When to Include Each Criteria

CriteriaInclude If...Common For
SecurityAlways (required)Everyone
AvailabilityYou make uptime commitments (SLAs)SaaS platforms, infrastructure providers
Processing IntegrityYou process data and accuracy mattersFinancial services, data analytics, billing
ConfidentialityYou handle confidential business dataLegal tech, healthcare, financial services
PrivacyYou handle consumer personal dataConsumer-facing SaaS, marketing tech

Security Controls (CC Series)

The Security criteria is the core of SOC 2. It covers nine Common Criteria (CC) categories:

CC1: Control Environment

What auditors check: Is there a security-aware culture with defined roles and accountability?

ControlEngineering Implementation
Security policies documentedPolicies stored in version-controlled repo, reviewed annually
Roles and responsibilities definedRBAC implemented in IAM; access tied to job function
Board/management oversightSecurity metrics reported in engineering reviews
Code of conductSecurity training completion tracked per employee

CC2: Communication and Information

What auditors check: Is security information communicated effectively?

ControlEngineering Implementation
Security policies accessibleInternal wiki/documentation site with policies
External communication of policiesPublic security page (trust center)
Incident communicationIncident response channels (Slack, PagerDuty) documented

CC3: Risk Assessment

What auditors check: Are risks identified, analyzed, and managed?

python
# Risk register automation
@dataclass
class Risk:
    id: str
    title: str
    category: str  # "technical", "operational", "compliance"
    likelihood: int  # 1-5
    impact: int      # 1-5
    risk_score: int  # likelihood × impact
    mitigations: list[str]
    owner: str
    review_date: str
    status: str      # "open", "mitigated", "accepted"

# Automated risk scoring
def calculate_risk_score(likelihood: int, impact: int) -> dict:
    score = likelihood * impact
    if score >= 20:
        level = "critical"
    elif score >= 12:
        level = "high"
    elif score >= 6:
        level = "medium"
    else:
        level = "low"
    return {"score": score, "level": level}

CC4: Monitoring Activities

What auditors check: Are controls monitored for effectiveness?

ControlEngineering Implementation
Continuous monitoringInfrastructure monitoring (Prometheus, Datadog)
Log reviewCentralized logging with automated alerting
Vulnerability scanningWeekly automated scans (Snyk, Trivy)
Penetration testingAnnual third-party pen test

CC5: Control Activities

What auditors check: Are controls implemented and operating?

This is the broadest category — it covers the actual security controls.

CC6: Logical and Physical Access Controls

What auditors check: Is access properly restricted?

yaml
# Access control checklist for SOC 2
access_controls:
  authentication:
    - mfa_required: true
      scope: "all production systems"
      evidence: "IdP configuration export showing MFA enforcement"

    - sso_enabled: true
      provider: "Okta / Azure AD / Google Workspace"
      evidence: "SSO configuration screenshots"

    - password_policy:
        min_length: 12
        complexity: true
        rotation_days: 90
        evidence: "IdP password policy configuration"

  authorization:
    - rbac_implemented: true
      evidence: "IAM role definitions, access matrix document"

    - least_privilege: true
      evidence: "Quarterly access review logs"

    - privileged_access:
        just_in_time: true
        time_limited: true
        approval_required: true
        evidence: "PAM tool (e.g., HashiCorp Vault) access logs"

  access_reviews:
    - frequency: "quarterly"
      scope: "all production access"
      evidence: "Access review tickets with approve/deny decisions"

    - offboarding:
        timeline: "within 24 hours of termination"
        evidence: "HR/IT offboarding workflow logs"

Access Control Implementation

python
# Just-in-time (JIT) access for production systems
class JITAccessManager:
    MAX_DURATION_HOURS = 8
    REQUIRES_APPROVAL_FOR = ["production_database", "admin_panel", "billing_system"]

    async def request_access(
        self,
        user: str,
        resource: str,
        reason: str,
        duration_hours: int = 4,
    ) -> dict:
        if duration_hours > self.MAX_DURATION_HOURS:
            raise ValueError(f"Maximum access duration is {self.MAX_DURATION_HOURS} hours")

        request = {
            "user": user,
            "resource": resource,
            "reason": reason,
            "duration_hours": duration_hours,
            "requested_at": datetime.utcnow().isoformat(),
            "expires_at": (datetime.utcnow() + timedelta(hours=duration_hours)).isoformat(),
            "status": "pending" if resource in self.REQUIRES_APPROVAL_FOR else "approved",
        }

        if request["status"] == "approved":
            await self._grant_access(request)
            await self._schedule_revocation(request)

        # Record in audit log
        await self.audit_log.record(
            event="access_requested",
            details=request,
        )

        return request

    async def _schedule_revocation(self, request: dict):
        """Automatically revoke access when it expires."""
        self.scheduler.schedule(
            at=request["expires_at"],
            action=self._revoke_access,
            args=[request["user"], request["resource"]],
        )

CC7: System Operations

What auditors check: Are systems monitored and incidents handled?

ControlEngineering ImplementationEvidence
Monitoring and alertingPrometheus + Grafana dashboardsDashboard screenshots, alert configurations
Incident managementPagerDuty + incident response processIncident tickets, postmortem documents
Vulnerability managementSnyk/Trivy scans in CI/CDScan reports, remediation tickets
Malware/threat detectionEDR (CrowdStrike, SentinelOne)Detection logs, response records

CC8: Change Management

What auditors check: Are changes controlled and tested?

SOC 2 requires evidence of:

RequirementImplementationEvidence
Changes are authorizedPR approval required before mergeGit history showing approved PRs
Changes are testedCI runs automated testsCI/CD pipeline logs
Changes are reviewedCode review by at least one peerPR review comments and approvals
Changes are documentedPR description, commit messagesGit history
Rollback capabilityFeature flags, canary deploymentsDeployment configurations
Segregation of dutiesThe person who wrote the code cannot approve their own PRBranch protection rules
yaml
# GitHub branch protection rules (SOC 2 evidence)
# Settings > Branches > Branch protection rules
branch_protection:
  branch: main
  rules:
    require_pull_request_reviews:
      required_approving_review_count: 1
      dismiss_stale_reviews: true
      require_code_owner_reviews: true
    require_status_checks:
      strict: true
      contexts:
        - "ci/tests"
        - "ci/security-scan"
        - "ci/lint"
    require_signed_commits: true
    enforce_admins: true  # Even admins must follow these rules
    restrictions:
      users: []  # No direct push access

CC9: Risk Mitigation

What auditors check: Are identified risks properly mitigated?

This maps to vendor management, business continuity, and disaster recovery plans.

Evidence Collection Automation

The Evidence Problem

SOC 2 auditors need evidence — screenshots, logs, configurations, reports — proving your controls are operating. Collecting this manually is painful:

Manual ApproachAutomated Approach
Screenshot dashboards quarterlyAPI pulls dashboard configs automatically
Export access lists from each systemScript queries IAM APIs across all systems
Manually compile incident reportsIncident tool (PagerDuty/Jira) auto-generates reports
Ask managers to confirm access reviewsAutomated access review workflow with audit trail

Automation With Compliance Platforms

Modern compliance platforms (Vanta, Drata, Secureframe) automate evidence collection by integrating with your infrastructure:

Custom Evidence Collection Script

python
# Automated SOC 2 evidence collector
import boto3
from datetime import datetime, timedelta

class SOC2EvidenceCollector:
    def __init__(self):
        self.aws = boto3.Session()
        self.evidence = []

    def collect_all_evidence(self, period_start: str, period_end: str):
        """Collect evidence for a SOC 2 audit period."""
        evidence_report = {
            "period": {"start": period_start, "end": period_end},
            "generated_at": datetime.utcnow().isoformat(),
            "controls": {},
        }

        # CC6: Access Controls
        evidence_report["controls"]["cc6_access"] = {
            "mfa_enforcement": self._check_mfa_enforcement(),
            "iam_policies": self._export_iam_policies(),
            "access_reviews": self._export_access_review_logs(
                period_start, period_end
            ),
        }

        # CC7: System Operations
        evidence_report["controls"]["cc7_operations"] = {
            "monitoring_alerts": self._export_alert_configs(),
            "incident_reports": self._export_incidents(
                period_start, period_end
            ),
            "vulnerability_scans": self._export_vuln_reports(
                period_start, period_end
            ),
        }

        # CC8: Change Management
        evidence_report["controls"]["cc8_changes"] = {
            "branch_protection": self._check_branch_protection(),
            "deployment_logs": self._export_deployments(
                period_start, period_end
            ),
            "code_reviews": self._export_pr_reviews(
                period_start, period_end
            ),
        }

        return evidence_report

    def _check_mfa_enforcement(self) -> dict:
        """Check that MFA is enforced for all IAM users."""
        iam = self.aws.client("iam")
        users = iam.list_users()["Users"]
        mfa_status = []
        for user in users:
            mfa_devices = iam.list_mfa_devices(
                UserName=user["UserName"]
            )["MFADevices"]
            mfa_status.append({
                "user": user["UserName"],
                "mfa_enabled": len(mfa_devices) > 0,
                "mfa_device_count": len(mfa_devices),
            })

        return {
            "total_users": len(users),
            "mfa_enabled": sum(1 for u in mfa_status if u["mfa_enabled"]),
            "mfa_missing": [
                u["user"] for u in mfa_status if not u["mfa_enabled"]
            ],
            "compliant": all(u["mfa_enabled"] for u in mfa_status),
        }

    def _check_branch_protection(self) -> dict:
        """Check GitHub branch protection rules."""
        # Uses GitHub API to verify branch protection
        import requests
        headers = {"Authorization": f"token {self.github_token}"}
        resp = requests.get(
            f"https://api.github.com/repos/{self.repo}/branches/main/protection",
            headers=headers,
        )
        protection = resp.json()
        return {
            "pr_reviews_required": protection.get(
                "required_pull_request_reviews", {}
            ).get("required_approving_review_count", 0) >= 1,
            "status_checks_required": bool(
                protection.get("required_status_checks")
            ),
            "admin_enforcement": protection.get(
                "enforce_admins", {}
            ).get("enabled", False),
            "compliant": True,  # Calculate based on checks above
        }

Common SOC 2 Failures and Fixes

Common FailureWhy It FailsEngineering Fix
Shared credentialsNo individual accountabilitySSO + individual IAM users; eliminate shared accounts
No MFA on productionCritical access without second factorEnforce MFA via IdP; block access without it
Missing access reviewsCannot prove least privilegeAutomated quarterly access review workflow
No change approvalChanges deployed without reviewBranch protection rules; PR reviews enforced
No encryption at restData exposed if disks are compromisedEnable encryption on all storage (RDS, S3, EBS)
No vulnerability scanningKnown CVEs in productionAutomated scanning in CI/CD pipeline
No incident response planUndefined response processDocumented IR plan; tested annually via tabletop exercise
Logging gapsCannot reconstruct events for auditorsCentralized audit logging; see Audit Logging Patterns

The Most Common SOC 2 Audit Finding

Missing or incomplete access reviews. Auditors consistently find that organizations grant access but never review whether it is still needed. Implement automated quarterly access reviews: pull the list of users from each system, send it to the appropriate manager for review, and record their approve/remove decisions. This single automation eliminates the most common audit finding.

SOC 2 Readiness Timeline

PhaseDurationActivities
Assessment2-4 weeksGap analysis, identify missing controls
Remediation2-3 monthsImplement missing controls, document policies
Type I Audit2-4 weeksPoint-in-time audit of control design
Observation Period3-12 monthsControls operating; collect evidence continuously
Type II Audit4-6 weeksAudit of control effectiveness over observation period
OngoingContinuousMaintain controls, collect evidence, annual re-audit

Start With Type II in Mind

Many companies rush to get a Type I report, then struggle to sustain controls for the Type II observation period. Design your controls for Type II from the start — automate everything, implement continuous monitoring, and build evidence collection into your daily operations.

Further Reading

"What I cannot create, I do not understand." — Richard Feynman