Skip to content
Cloud security and infrastructure protection

Every major 2024-2025 cloud breach was a known, preventable misconfiguration

Last updated: April 2026 - Covers AWS security services, real 2024-2025 breach case studies, SOC 2 for startups, multi-tenant SaaS patterns, and a week-by-week implementation plan.

IAM Hardening

IAM misconfigurations are the #1 cause of cloud breaches. Lock this down first.

The Non-Negotiables

  • MFA on every human account - FIDO2/passkeys for admins, TOTP minimum for everyone else
  • Delete root access keys - the root account should have MFA and nothing else
  • No long-lived access keys - use IAM roles, OIDC federation for CI/CD, and IAM Identity Center for humans
  • Least privilege - never Action: "*". Use IAM Access Analyzer to right-size policies.
# Lock down root account
aws iam delete-access-key --user-name root --access-key-id AKIAXXXXXXXX

# Enable IAM Access Analyzer (free)
aws accessanalyzer create-analyzer \
  --analyzer-name account-analyzer \
  --type ACCOUNT

# Block public S3 at the account level (prevents ALL future public buckets)
aws s3control put-public-access-block \
  --account-id $(aws sts get-caller-identity --query Account --output text) \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Least Privilege - What It Actually Looks Like

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::my-app-uploads/*"
    },
    {
      "Effect": "Allow",
      "Action": ["secretsmanager:GetSecretValue"],
      "Resource": "arn:aws:secretsmanager:us-east-1:*:secret:production/*"
    },
    {
      "Effect": "Allow",
      "Action": ["sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage"],
      "Resource": "arn:aws:sqs:us-east-1:*:my-app-queue"
    }
  ]
}

CI/CD - No More Access Keys

# GitHub Actions OIDC (zero stored credentials)
permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/github-deploy
      aws-region: us-east-1

Network Security

ControlWhatPriority
Private subnetsRDS, app servers, caches in private subnets. Only ALB/CloudFront public.🔴 Critical
Security groupsNo 0.0.0.0/0 except on ALB port 443. Restrict SSH to VPN/bastion only.🔴 Critical
VPC Flow LogsEnable on all VPCs. Send to S3 (cheaper) or CloudWatch.🟡 High
VPC EndpointsPrivateLink for S3, DynamoDB, Secrets Manager - keeps traffic off the internet.🟡 High
WAFAWS WAF on ALB/CloudFront. OWASP managed rules + rate limiting.🟡 High
TLS 1.2+Enforce on all load balancers and CloudFront. Use TLSv1.2_2021 policy.🔴 Critical

WAF - Minimum Viable Rules

# Create a WAF WebACL with AWS managed rules
aws wafv2 create-web-acl \
  --name "saas-protection" \
  --scope REGIONAL \
  --default-action '{"Allow":{}}' \
  --rules '[
    {
      "Name": "AWSManagedRulesCommonRuleSet",
      "Priority": 1,
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesCommonRuleSet"
        }
      },
      "OverrideAction": {"None": {}},
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "CommonRules"
      }
    },
    {
      "Name": "RateLimit",
      "Priority": 2,
      "Statement": {
        "RateBasedStatement": {
          "Limit": 2000,
          "AggregateKeyType": "IP"
        }
      },
      "Action": {"Block": {}},
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLimit"
      }
    }
  ]' \
  --visibility-config '{"SampledRequestsEnabled":true,"CloudWatchMetricsEnabled":true,"MetricName":"SaaSProtection"}'

Data Protection & Encryption

ServiceEncryption at RestEncryption in Transit
S3SSE-S3 (default) or SSE-KMS for sensitive dataEnforce via bucket policy aws:SecureTransport
RDSEnable at creation (can't add later without recreation)Force SSL via parameter group rds.force_ssl=1
DynamoDBEncrypted by default (AWS owned key or KMS)TLS by default
EBSEnable default encryption in account settingsN/A (block storage)
SecretsSecrets Manager (auto-rotation) or SSM Parameter StoreTLS by default
# Force HTTPS on S3 bucket
aws s3api put-bucket-policy --bucket my-bucket --policy '{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "ForceHTTPS",
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:*",
    "Resource": ["arn:aws:s3:::my-bucket", "arn:aws:s3:::my-bucket/*"],
    "Condition": {"Bool": {"aws:SecureTransport": "false"}}
  }]
}'

# Enable default EBS encryption for the account
aws ec2 enable-ebs-encryption-by-default

# Set up Secrets Manager with auto-rotation
aws secretsmanager create-secret \
  --name production/db-password \
  --secret-string '{"username":"app","password":"CHANGE_ME"}' \
  --kms-key-id alias/production-key

Application Security

The OWASP Essentials

  • Injection: Parameterized queries everywhere. Never concatenate user input into SQL/commands.
  • Broken auth: Use OAuth 2.0/OIDC via Cognito or Auth0. Short-lived JWTs (15 min access, 7 day refresh).
  • SSRF: Validate and allowlist outbound URLs. Block access to 169.254.169.254 (EC2 metadata).
  • Dependency vulns: Dependabot (free), Snyk, or Trivy in CI/CD. Block merges with critical CVEs.

Security Headers (CloudFront Response Headers Policy)

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; frame-ancestors 'none'
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()

CI/CD Security Scanning

# GitHub Actions - scan on every PR
- name: Dependency scan
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: fs
    severity: HIGH,CRITICAL
    exit-code: 1

- name: SAST scan
  uses: returntocorp/semgrep-action@v1
  with:
    config: p/owasp-top-ten

Logging & Monitoring

ServiceCostWhat It Catches
CloudTrailFree (mgmt events)Every API call. Who did what, when, from where.
GuardDuty~$30-50/mo (small)Compromised instances, credential abuse, crypto mining, recon.
Security Hub~$10-30/moCIS benchmark compliance, aggregates findings from all services.
IAM Access AnalyzerFreeResources shared outside your account (public S3, cross-account roles).
Config$0.003/rule evalConfiguration drift, non-compliant resources.

Critical Alerts (Set Up Day 1)

# Alert on root account login
aws cloudwatch put-metric-alarm \
  --alarm-name "RootAccountLogin" \
  --metric-name "RootAccountUsage" \
  --namespace "CloudTrailMetrics" \
  --statistic Sum \
  --period 300 \
  --threshold 1 \
  --comparison-operator GreaterThanOrEqualToThreshold \
  --evaluation-periods 1 \
  --alarm-actions arn:aws:sns:us-east-1:*:security-alerts

# Alert on security group changes
# Alert on IAM policy changes
# Alert on S3 bucket policy changes
# (Same pattern - create CloudWatch metric filters from CloudTrail)

Incident Response

Compromised Credentials Playbook

  1. Disable the key/user immediately - aws iam update-access-key --status Inactive
  2. Check CloudTrail - what did the attacker do? Look for CreateUser, AttachPolicy, RunInstances
  3. Revoke all sessions - attach an inline deny-all policy, then rotate credentials
  4. Check for persistence - new IAM users, new access keys, Lambda backdoors, EC2 instances in unusual regions
  5. Contain blast radius - isolate affected resources, revoke cross-account access
  6. Notify - legal, customers (if data exposed), AWS support (if needed)

Auto-Remediation: Disable Compromised Keys

# Lambda triggered by GuardDuty finding via EventBridge
import boto3

def handler(event, context):
    finding = event['detail']
    if finding['type'].startswith('UnauthorizedAccess:IAMUser'):
        access_key = finding['resource']['accessKeyDetails']['accessKeyId']
        user = finding['resource']['accessKeyDetails']['userName']
        
        iam = boto3.client('iam')
        iam.update_access_key(
            UserName=user,
            AccessKeyId=access_key,
            Status='Inactive'
        )
        
        # Notify via SNS
        sns = boto3.client('sns')
        sns.publish(
            TopicArn='arn:aws:sns:us-east-1:*:security-alerts',
            Subject=f'ALERT: Access key disabled for {user}',
            Message=f'GuardDuty detected unauthorized access. Key {access_key} disabled.'
        )

Multi-Tenant SaaS Security

PatternIsolationCostBest For
SiloSeparate DB/infra per tenantHighRegulated industries, enterprise
PoolShared DB, tenant_id columnLowMost SaaS startups
BridgeShared infra, separate schemas/tablesMediumMid-market SaaS

Row-Level Security (PostgreSQL)

-- Enable RLS on the table
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy: users can only see their tenant's data
CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

-- Set tenant context at the start of each request
SET app.current_tenant = 'tenant-uuid-here';

-- Now SELECT * FROM orders only returns that tenant's rows
-- Even if application code has a bug, RLS prevents cross-tenant access
Never trust application code alone for tenant isolation. A single missing WHERE clause = data breach. RLS is your safety net - it enforces isolation at the database level regardless of what the app does.

SOC 2 & Compliance

SOC 2 for SaaS Startups

WhatCostWhen
Compliance automation (Vanta/Drata/Secureframe)$10K-$20K/yearStart collecting evidence from day 1
SOC 2 Type 1 audit$15K-$30KWhen enterprise prospects require it (~$50K+ deals)
SOC 2 Type 2 audit$20K-$50K6-12 months after Type 1 (covers a review period)

What auditors actually look for: MFA enforced, access reviews documented, change management process, incident response plan, encryption at rest and in transit, logging enabled, vulnerability scanning, employee security training. Most of this is what you should be doing anyway.

Real-World Breaches (2024-2025)

WorkComposer (Feb 2025): 21 million employee screenshots exposed via a public S3 bucket. Passwords, API keys, internal chats - all publicly accessible. 39 days between notification and fix. Prevention: one CLI command to enable S3 Block Public Access.
GitHub credential harvesting (ongoing): Automated bots scan every public commit within seconds. An AWS access key committed at 2:00 PM is exploited by 2:05 PM. Prevention: git-secrets pre-commit hooks + GitHub push protection (free).
Overly permissive IAM (systemic): Apps deployed with Action: "*" because "it works." When the app is compromised via SSRF or dependency vuln, the attacker inherits full account access. Prevention: least-privilege policies from day 1.

Common thread: Every major 2024-2025 cloud breach was a known, preventable misconfiguration. Not zero-days. Not sophisticated attacks. Automated scanners, basic misconfigs, minutes to exploitation.

Week-by-Week Implementation Plan

Week 1: Stop the Bleeding

  • Enable S3 Block Public Access at account level
  • Enable MFA on all IAM users (FIDO2 for admins)
  • Delete all root access keys
  • Enable CloudTrail (multi-region)
  • Enable GuardDuty (free 30-day trial)
  • Verify no public RDS instances

Week 2: Access Control

  • Set up IAM Identity Center (SSO)
  • Replace access keys with OIDC federation for CI/CD
  • Implement least-privilege roles for applications
  • Enable IAM Access Analyzer
  • Add git-secrets pre-commit hooks

Week 3: Network & Encryption

  • Audit VPC: RDS/app servers in private subnets
  • Review security groups (no 0.0.0.0/0 on non-ALB resources)
  • Enable RDS encryption + force SSL
  • Enable default EBS encryption
  • Enforce TLS 1.2+ on all load balancers

Week 4: Detection & Response

  • Enable Security Hub with CIS benchmark
  • Set up CloudWatch alarms (root login, IAM changes, SG changes)
  • Write IR playbook (compromised credentials, data breach)
  • Deploy auto-remediation Lambda for compromised keys
  • Enable VPC Flow Logs

Month 2-3: Hardening

  • Deploy WAF on public-facing ALB/CloudFront
  • Implement dependency scanning in CI/CD (Trivy + Semgrep)
  • Set up Secrets Manager with rotation
  • Add security headers via CloudFront
  • Implement SCPs (region restriction, prevent security service disable)

Month 3-6: Compliance

  • Sign up for compliance automation (Vanta/Drata/Secureframe)
  • Document access control, change management, and IR procedures
  • Implement row-level security for multi-tenant data
  • Begin SOC 2 Type 1 preparation

The Bottom Line

Cloud security for a small team isn't about buying expensive tools - it's about not leaving the door open. S3 Block Public Access, MFA, least-privilege IAM, and CloudTrail cost almost nothing and prevent 80% of breaches. Start with Week 1, iterate weekly, and you'll have a security posture that passes SOC 2 audits within 6 months. The attackers are automated - your defenses should be too.