Hitchhiker's Guide to the Range — Phase 09

Operation Cedar Lattice, Cloud Phase. This guide walks you through Meridian Freight International's cloud footprint exactly as FIN-LATTICE would approach it: starting from the foothold (a compromised CI/CD runner) and mapping every permission edge toward IAM admin. No exploits. No payloads. The value is in reading the graph.


Table of Contents

  1. Meridian Freight AWS Footprint
  2. Meridian Freight Azure Footprint
  3. Cedar Lattice Cloud Phase — Artifact 09-A
  4. Reading the Permission Graph
  5. The Lowest-Detection Path
  6. CloudTrail Forensics — What the SOC Sees
  7. Container Escape in the EKS Environment
  8. Detection Coverage Summary
  9. Engagement Timeline

Meridian Freight AWS Footprint

Meridian Freight International uses a two-account AWS organization:

Account: meridian-prod (114700000000)

  • EKS cluster meridian-freight-eks running the Freight Management System (FMS) backend
  • EC2 ASG meridian-app-servers running the web tier
  • RDS PostgreSQL meridian-freight-db (freight manifests, customer data)
  • S3 buckets: meridian-freight-manifests (freight data), meridian-artifacts (build outputs)
  • Secrets Manager: meridian/db/credentials, meridian/api/keys

Account: meridian-dev (114700000001)

  • CI/CD runner fleet (GitHub Actions self-hosted runners on EC2)
  • Development EKS cluster meridian-dev-eks
  • ECR registries (container images for both accounts pull from here)

IAM roles of interest:

Role ARNTrust relationshipNotable permissions
arn:aws:iam::114700000001:role/meridian-cicd-runnerEC2 servicests:AssumeRole on meridian-deploy-staging
arn:aws:iam::114700000001:role/meridian-deploy-stagingmeridian-cicd-runneriam:AttachUserPolicy, ecr:*, s3:* on dev account
arn:aws:iam::114700000000:role/meridian-eks-nodeEC2 serviceecr:GetDownloadUrlForLayer, ecr:BatchGetImage, s3:GetObject on artifacts
arn:aws:iam::114700000000:role/meridian-ops-adminmeridian-deploy-staging (cross-account)iam:*, ec2:*, s3:*, rds:* on prod account

The critical cross-account trust: meridian-deploy-staging in the dev account is listed in the trust policy of meridian-ops-admin in the prod account. This is the intended mechanism for CI/CD to deploy to production — and it is also the lateral-movement path from dev to prod.

VPC layout:

  • meridian-vpc-prod (10.0.0.0/16): EKS node groups in private subnets, RDS in isolated subnets
  • meridian-vpc-dev (10.1.0.0/16): CI/CD runners in private subnets, NAT gateway for outbound

Meridian Freight Azure Footprint

Meridian Freight also has an Azure subscription used for the European freight tracking system (integrated with EU logistics partners).

Subscription: meridian-prod-eu (sub-id: mf-eu-prod-0001)

Resource groups:

  • meridian-eu-compute-rg: 3 VMs running the EU Freight Tracking Service (FTS)
  • meridian-eu-storage-rg: Storage account meridianfreighteu with freight data blobs
  • meridian-eu-net-rg: Virtual network, NSGs, Azure Load Balancer

Managed identities:

  • System-assigned managed identity on each EU FTS VM
  • RBAC assignments: Contributor on meridian-eu-storage-rg, Reader on meridian-eu-compute-rg

Service principals:

  • meridian-eu-deploy-sp: Used by the EU CI/CD pipeline. Has Contributor on the entire meridian-prod-eu subscription. Client secret stored in Azure Key Vault.

Key Vault: meridian-eu-kv contains:

  • eu-db-connection-string: PostgreSQL connection string for the EU freight database
  • eu-deploy-sp-secret: The client secret for meridian-eu-deploy-sp

The Key Vault access policy grants Get and List on secrets to the EU FTS VMs' managed identities. This means any compromise of a VM running the EU FTS can fetch the deployment service principal's secret — which has Contributor on the entire subscription.


Cedar Lattice Cloud Phase — Artifact 09-A

Foothold: FIN-LATTICE compromised a GitHub Actions self-hosted runner EC2 instance in the dev account by exploiting a dependency confusion attack in the build pipeline. The runner has the meridian-cicd-runner instance profile.

The permission graph (synthetic metadata for the labs):

[meridian-cicd-runner]
    │
    ├── sts:AssumeRole → [meridian-deploy-staging]  (cost: 1, CloudTrail: AssumeRole in STS)
    │
    └── s3:ListBucket → [meridian-artifacts]        (cost: 1, CloudTrail: ListBucket in S3)

[meridian-deploy-staging]
    │
    ├── iam:AttachUserPolicy → [meridian-ops-admin] (cost: 5, CloudTrail: AttachUserPolicy in IAM)
    │
    ├── sts:AssumeRole → [meridian-ops-admin]       (cost: 1, CloudTrail: AssumeRole in STS)
    │
    └── ecr:GetDownloadUrlForLayer → [meridian-ecr] (cost: 1, CloudTrail: GetDownloadUrlForLayer)

[meridian-ops-admin]
    │
    ├── iam:CreatePolicyVersion → [meridian-ops-policy] (cost: 5, CloudTrail: CreatePolicyVersion)
    │
    ├── s3:GetObject → [meridian-freight-manifests]     (cost: 1, CloudTrail: GetObject in S3)
    │
    └── secretsmanager:GetSecretValue → [meridian/db/credentials] (cost: 2, CloudTrail: GetSecretValue)

[meridian-eks-node]
    │
    └── iam:CreatePolicyVersion → [meridian-ops-policy] (cost: 5, CloudTrail: CreatePolicyVersion)

Two paths from meridian-cicd-runner to meridian-freight-manifests:

Path A (via AssumeRole chain — total cost 3):

meridian-cicd-runner
  --[sts:AssumeRole, cost=1]--> meridian-deploy-staging
  --[sts:AssumeRole, cost=1]--> meridian-ops-admin
  --[s3:GetObject, cost=1]--> meridian-freight-manifests

Path B (via AttachUserPolicy — total cost 7):

meridian-cicd-runner
  --[sts:AssumeRole, cost=1]--> meridian-deploy-staging
  --[iam:AttachUserPolicy, cost=5]--> meridian-ops-admin
  --[s3:GetObject, cost=1]--> meridian-freight-manifests

Path A is preferred because sts:AssumeRole blends in with normal CI/CD traffic (cost=1) while iam:AttachUserPolicy is a high-signal IAM write (cost=5) that triggers GuardDuty.


Reading the Permission Graph

The permission graph has two types of nodes:

  • Principal nodes: entities that can make API calls (meridian-cicd-runner, meridian-deploy-staging, meridian-ops-admin)
  • Resource nodes: things that can be acted on (meridian-freight-manifests, meridian/db/credentials)

Edges are directed: a principal can perform an action on a resource. When the resource is another IAM principal (a role), the action is typically sts:AssumeRole — and the result of traversing that edge is that you become the target role (can use its edges).

Reading rule: Start at the foothold node. Follow edges where you are the principal. When you reach a node that is itself a principal (a role ARN), you can now follow that node's outgoing edges too. Repeat until you reach the target resource or run out of edges.

This is exactly what the lab's can_reach function implements with BFS, and what attack_path optimizes with Dijkstra using edge visibility costs.

Privilege escalation edges are edges where the action belongs to the PRIVESC_ACTIONS set: iam:CreatePolicyVersion, iam:AttachUserPolicy, iam:PassRole, etc. In the graph above, meridian-deploy-staging → iam:AttachUserPolicy → meridian-ops-admin is a privilege escalation edge because iam:AttachUserPolicy can be used to grant admin permissions.


The Lowest-Detection Path

Given the permission graph above, the Dijkstra solver finds Path A:

StepPrincipalActionResourceCloudTrail EventVisibility Cost
1meridian-cicd-runnersts:AssumeRolemeridian-deploy-stagingAssumeRole (STS)1
2meridian-deploy-stagingsts:AssumeRolemeridian-ops-adminAssumeRole (STS)1
3meridian-ops-admins3:GetObjectmeridian-freight-manifestsGetObject (S3)1

Total visibility cost: 3 (three AssumeRole + GetObject events — all extremely common in production AWS environments, low signal-to-noise ratio in SIEM).

Compare to Path B (cost 7): the AttachUserPolicy event (cost=5) would immediately trigger a GuardDuty PrivilegeEscalation:IAMUser/AdministrativePermissions finding and a SIEM alert. Any competent SOC with CloudTrail monitoring would catch it in minutes.

Report language (what you write in the engagement report):

"Starting from the compromised CI/CD runner instance (meridian-cicd-runner), FIN-LATTICE can reach production freight data in the meridian-freight-manifests S3 bucket via a three-step AssumeRole chain totaling three CloudTrail events. All three events are readOnly: false but produce audit records indistinguishable from normal CI/CD pipeline activity without baselining the specific role assumption chain. Detection requires SIEM rules that alert on cross-account AssumeRole where the source role is not in the approved automation ARN list, or GuardDuty enabled on the production account with anomaly detection trained on baseline CI/CD patterns."


CloudTrail Forensics — What the SOC Sees

The three CloudTrail events from Path A:

Event 1: AssumeRole (dev account)

{
  "eventName": "AssumeRole",
  "eventSource": "sts.amazonaws.com",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::114700000001:assumed-role/meridian-cicd-runner/i-0abc123"
  },
  "requestParameters": {
    "roleArn": "arn:aws:iam::114700000001:role/meridian-deploy-staging",
    "roleSessionName": "meridian-deploy-session"
  },
  "sourceIPAddress": "10.1.2.34",
  "readOnly": false
}

Event 2: AssumeRole (cross-account to prod)

{
  "eventName": "AssumeRole",
  "eventSource": "sts.amazonaws.com",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::114700000001:assumed-role/meridian-deploy-staging/meridian-deploy-session"
  },
  "requestParameters": {
    "roleArn": "arn:aws:iam::114700000000:role/meridian-ops-admin",
    "roleSessionName": "meridian-ops-session"
  },
  "sourceIPAddress": "10.1.2.34",
  "readOnly": false
}

Note: This event appears in the prod account's CloudTrail, not the dev account. Many organizations have separate SIEM ingestion for each account and may not correlate cross-account AssumeRole events unless their SIEM is configured to do so.

Event 3: GetObject (prod account)

{
  "eventName": "GetObject",
  "eventSource": "s3.amazonaws.com",
  "userIdentity": {
    "type": "AssumedRole",
    "arn": "arn:aws:sts::114700000000:assumed-role/meridian-ops-admin/meridian-ops-session"
  },
  "requestParameters": {
    "bucketName": "meridian-freight-manifests",
    "key": "2024/Q4/manifest-batch-4421.json"
  },
  "sourceIPAddress": "10.1.2.34",
  "readOnly": false
}

What makes this detectable:

  • The source IP 10.1.2.34 is in the dev VPC range, not a prod VPC range. A SIEM rule that alerts on GetObject on meridian-freight-manifests from IP addresses outside the prod VPC CIDR would catch Event 3.
  • The cross-account AssumeRole in Event 2 (a dev-account role assuming a prod-account role from a non-standard session name) would alert on a SIEM rule baselining AssumeRole source ARN → roleArn pairs.

Container Escape in the EKS Environment

The EKS cluster meridian-freight-eks runs workloads in the freight-backend namespace. The freight manifest API pod was found to have the following misconfiguration in its PodSpec (discovered during the engagement's cloud misconfiguration review):

securityContext:
  privileged: true          # CRITICAL: enables container escape
  capabilities:
    add: ["SYS_ADMIN"]      # redundant with privileged, but present
volumes:
  - name: docker-sock
    hostPath:
      path: /var/run/docker.sock  # CRITICAL: Docker socket mounted
  - name: host-etc
    hostPath:
      path: /etc              # HIGH: /etc bind-mounted
      type: Directory

This configuration enables three separate escape vectors:

Vector 1 (Critical): --privileged + all capabilities + host devices → mount host disk, read/write any host file.

Vector 2 (Critical): Docker socket mounted → create a new privileged container from inside the compromised pod, bypassing all pod-level security policies.

Vector 3 (High): Writable /etc mount → write a cron job to /etc/cron.d/ that runs on the host as root at the next cron cycle.

Detection coverage:

  • Falco running on EKS nodes catches all three at container launch (privileged, docker socket, writable etc mount rules)
  • Kubernetes Audit Log records the pod creation with the misconfigured securityContext
  • OPA/Gatekeeper policy require-non-root and block-privileged-containers would prevent the pod from being scheduled at all (if enforced — Meridian Freight has these policies in warn mode, not deny mode)

Report finding:

"The freight-manifest-api deployment in the freight-backend namespace runs with privileged: true and mounts /var/run/docker.sock from the host. Either misconfiguration independently enables complete host compromise from a container breakout. The EKS node's instance profile (meridian-eks-node) has iam:CreatePolicyVersion on the shared meridian-ops-policy, making container-to-IAM-admin a single step from the EKS node. Immediate remediation: enforce OPA/Gatekeeper admission policies in deny mode, remove privileged: true from all pod specs, and remove the Docker socket hostPath volume from all non-system pods."


Detection Coverage Summary

TechniqueMITRE IDDetection SourceSignal Strength
IMDS credential theft (IMDSv1)T1552.005VPC flow logs to 169.254.169.254, CloudTrail GetCallerIdentity from unexpected IPHigh
AssumeRole chainT1078.004CloudTrail AssumeRole; SIEM correlation on cross-account chainsMedium
IAM AttachUserPolicyT1098.001CloudTrail AttachUserPolicy; GuardDuty PrivilegeEscalation findingHigh
IAM CreatePolicyVersionT1098.001CloudTrail CreatePolicyVersion; CloudTrail Insights on write-rate spikeHigh
Lambda-based privescT1648CloudTrail CreateFunction + InvokeFunction correlationMedium
Privileged containerT1610Falco: container.privileged=true; Kubernetes Audit LogHigh
Docker socket mountT1610Falco: fd.name=/var/run/docker.sock; Kubernetes Audit LogHigh
IMDS from containerT1552.005Falco: fd.sip=169.254.169.254 and container.id != ""Medium
Managed identity token theftT1552.005Azure Activity Log: token issuance from unexpected IPMedium
Role assignment escalationT1098Azure Activity Log: Microsoft.Authorization/roleAssignments/writeHigh

Engagement Timeline

DayActivityFinding
1Foothold: CI/CD runner compromise via dependency confusionmeridian-cicd-runner role credentials
1IMDS credential theft (IMDSv1 enabled on runner nodes)AccessKeyId + SecretAccessKey for meridian-cicd-runner
2Permission graph enumeration via sts:GetCallerIdentity, iam:SimulatePrincipalPolicyMapped all role assumptions, identified cross-account path
2Path A: meridian-cicd-runnermeridian-deploy-stagingmeridian-ops-adminAccess to meridian-freight-manifests S3 bucket
3EKS pod inspection: discovered freight-manifest-api misconfigurationPrivileged pod + Docker socket mount + writable /etc
3Container escape analysis: three escape vectors from single podEKS node access → meridian-eks-node role → iam:CreatePolicyVersion
4Azure footprint: EU FTS VM managed identity → Key Vaulteu-deploy-sp-secret — service principal credentials for entire EU subscription
4Azure RBAC: meridian-eu-deploy-sp → Contributor on subscriptionSimulated scope of compromise: all EU resources
5Reporting: CloudTrail forensics, detection gap mapping, remediation roadmapArtifact 09-A delivered to Meridian Freight CISO

« Phase 09 README | WARMUP Guide