Peculiar Cloud · Sample deliverable

What a Cloud Security Review hands back

The honest answer to "what exactly do I get?" — shown, not described. Below is a condensed version of the four artifacts every review leaves in your repository.

Document
Security review · deliverable set
Scope
Cloud accounts & access in scope
Provenance
Illustrative scenario
Contents
Register · threat model · backlog · fix
01

The top attack path

Before the register, the picture: the single highest-ranked exposure, drawn as the path an attacker actually walks. The detail — and the fix — follow below.

Internet unauthenticated Jump host (EC2) SG :22 from 0.0.0.0/0 EXP-001 · rank 1 ci-deploy role assumed via profile Prod data buckets s3:* — full read/write SSH sts:AssumeRole reads Two hops from unauthenticated to production data — the reason this outranks its "Medium" scanner score.
The EXP-001 attack path, drawn from the exposure register below: a scanner rated the security-group finding "Medium" in isolation. Reachability and the role's blast radius are what make it rank 1. Resources illustrative.
02

The exposure register

The core document: every finding is an attack path — where an attacker enters, what identity they land on, what they reach — ranked by what we assess as most reachable and most damaging, not by scanner score. Five of the eight entries from the illustrative engagement:

exposure-register.md Sanitized sample
## EXP-001 — public jump host can assume the CI deploy role   [rank 1]

Path:  internet → EC2 (sg inbound 22 from 0.0.0.0/0) → instance profile
       → sts:AssumeRole ci-deploy → s3:* on the prod data buckets

Why rank 1: two hops from unauthenticated to prod data. The CSPM rated
the security-group finding "Medium" in isolation — reachability and the
role's blast radius are what make it critical.

Fix shipped: fixes/exp-001-jump-host.tf
Residual: none observed after apply — see threat-model/ci-deploy.md

## EXP-002 — public bucket policy fronts an internal dataset   [rank 2]

Path:  internet → s3:GetObject (bucket policy allows "*") →
       customer-derived analytics dataset (classified internal)

Why rank 2: one hop, no identity needed. The bucket was opened for a
one-off partner share; the policy never expired. The CSPM rated it
"Low" because the ACL — not the policy — was closed.

Fix shipped: fixes/exp-002-analytics-bucket.tf
Residual: partner share moves to presigned URLs — backlog #4

## EXP-003 — SSRF-reachable metadata on the reporting service  [rank 3]

Path:  authenticated user → PDF-render endpoint (fetches arbitrary
       URLs) → IMDSv1 → instance role → dynamodb:* on three tables

Why rank 3: needs a paid account, but the endpoint accepts internal
URLs and the instance still answers IMDSv1.

Fix shipped: fixes/exp-003-imdsv2.tf
Residual: URL allow-list in the app itself — backlog #2

## EXP-004 — vendor role trusts an entire AWS account          [rank 4]

Path:  any principal in the vendor's account → sts:AssumeRole
       vendor-integration → read on the billing exports

Why rank 4: needs vendor-side compromise first, but the trust policy
has no ExternalId and trusts the account root.

Fix shipped: fixes/exp-004-vendor-trust.tf

## EXP-005 — dormant IAM user, active key, admin policy        [rank 5]

Path:  leaked or cracked access key → IAM user deploy-legacy →
       AdministratorAccess (attached 2023, unused 14 months)

Why rank 5: no public exposure today — but it is a standing skeleton
key with no MFA and no rotation.

Fix shipped: fixes/exp-005-deploy-legacy.tf
Each entry: the path, why it holds its rank, the fix that already shipped, and what residual risk remains. No severity soup — a ranked argument you can challenge.
03

The threat model

The register tells you what to fix; the threat model tells you why those paths existed — the trust boundaries of your environment and the identities whose compromise costs the most. An excerpt:

threat-model/trust-boundaries.md Sanitized sample
# Trust boundaries — reviewed environment (excerpt)

TB-1  internet → edge
      CloudFront + ALB; only 443 terminates here. Two findings
      bypassed this boundary entirely: the EXP-001 jump host and
      the EXP-002 bucket policy.

TB-2  workload → identity
      Every workload carries a role; the blast radius of a workload
      compromise IS that role. Of 31 roles reviewed, 6 carried
      wildcard actions — ranked in the register, top 3 fixed.

TB-3  account → account
      12 cross-account trusts. 9 scoped correctly, 2 trusted a whole
      account (EXP-004), 1 dormant (removed).

The identity that matters most: ci-deploy. It can write prod code
and read prod data, and three separate paths reached it (EXP-001,
EXP-003, EXP-005). Guarding it is worth more than closing any ten
scanner findings.
The point of this document: after the engagement, your team reasons about new changes against named boundaries — not against a scanner’s opinion.
04

The ranked backlog

Everything scoped but not implemented inside the fixed window, written so your team — or a follow-on engagement — can execute without us:

backlog.md Sanitized sample
# Fix-it backlog — scoped, not yet implemented (excerpt)

#1  [high]    Rotate all access keys older than 90 days; alarm on
              IAM-user key use (3 users remain after EXP-005).
              Evidence: threat-model/identities.md · Check: zero
              active keys > 90d in credential report.

#2  [high]    URL allow-list in the PDF-render service — closes the
              application half of EXP-003.
              Check: render endpoint refuses link-local + VPC CIDRs.

#3  [medium]  IMDSv2 as account default via SCP once the two legacy
              AMIs are rebuilt.
              Check: DescribeInstances shows zero IMDSv1 responders.

#4  [medium]  Partner analytics share → presigned URLs; retire the
              EXP-002 CloudFront exception.

#5  [medium]  Session policies on the shared read-only role used by
              the data team (44 principals inherit it today).
Each item names the evidence file behind it and an acceptance check — so "done" is testable, not a feeling.
05

One complete fix, as it ships

Fixes arrive as pull requests in your repository, not recommendations in an appendix. The full EXP-001 fix:

fixes/exp-001-jump-host.tf Sanitized sample
# fixes/exp-001-jump-host.tf
# EXP-001: public jump host could assume the CI deploy role.
# SSM Session Manager replaces inbound SSH; the deploy role's trust
# and permissions shrink to what CI actually does.

resource "aws_security_group" "jump" {
  name_prefix = "jump-host-"
  description = "Jump host - no inbound; SSM via VPC endpoints only"
  vpc_id      = var.vpc_id

  # No ingress blocks: Session Manager needs none.
  egress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }
}

resource "aws_iam_role" "ci_deploy" {
  name = "ci-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Action    = "sts:AssumeRole"
      Principal = { AWS = var.ci_runner_role_arn }
      Condition = {
        StringEquals = { "sts:ExternalId" = var.ci_external_id }
      }
    }]
  })
}

data "aws_iam_policy_document" "ci_deploy" {
  statement {
    sid = "ArtifactWriteOnly"
    actions = [
      "s3:PutObject",
      "s3:GetObject",
      "s3:ListBucket",
      "s3:GetBucketLocation",
    ]
    resources = [
      "arn:aws:s3:::acme-ci-artifacts",
      "arn:aws:s3:::acme-ci-artifacts/*",
      "arn:aws:s3:::acme-deploy-manifests",
      "arn:aws:s3:::acme-deploy-manifests/*",
    ]
  }
}

resource "aws_iam_role_policy" "ci_deploy" {
  name   = "ci-deploy-scoped"
  role   = aws_iam_role.ci_deploy.id
  policy = data.aws_iam_policy_document.ci_deploy.json
}
The complete EXP-001 remediation: no inbound SSH anywhere (SSM replaces it), the deploy role trusts only the CI runner with an ExternalId, and s3:* became the four actions CI actually performs.
06

What's not shown here

The full deliverable also includes the ADRs for any structural decision we made, the pull-request history your team reviewed as the fixes landed, and the live handover where we walk your engineers through all of it. The engagement ends when they can run it without us — the full engagement description is here. Also see the other sample: an edge-firewall security review from a real engagement →

Want this for your environment?

Tell us what's going on. If we're a fit, you get a fixed scope and price in writing; if we're not, we'll say so.