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
- Meridian Freight AWS Footprint
- Meridian Freight Azure Footprint
- Cedar Lattice Cloud Phase — Artifact 09-A
- Reading the Permission Graph
- The Lowest-Detection Path
- CloudTrail Forensics — What the SOC Sees
- Container Escape in the EKS Environment
- Detection Coverage Summary
- Engagement Timeline
Meridian Freight AWS Footprint
Meridian Freight International uses a two-account AWS organization:
Account: meridian-prod (114700000000)
- EKS cluster
meridian-freight-eksrunning the Freight Management System (FMS) backend - EC2 ASG
meridian-app-serversrunning 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 ARN | Trust relationship | Notable permissions |
|---|---|---|
arn:aws:iam::114700000001:role/meridian-cicd-runner | EC2 service | sts:AssumeRole on meridian-deploy-staging |
arn:aws:iam::114700000001:role/meridian-deploy-staging | meridian-cicd-runner | iam:AttachUserPolicy, ecr:*, s3:* on dev account |
arn:aws:iam::114700000000:role/meridian-eks-node | EC2 service | ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, s3:GetObject on artifacts |
arn:aws:iam::114700000000:role/meridian-ops-admin | meridian-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 subnetsmeridian-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 accountmeridianfreighteuwith freight data blobsmeridian-eu-net-rg: Virtual network, NSGs, Azure Load Balancer
Managed identities:
- System-assigned managed identity on each EU FTS VM
- RBAC assignments:
Contributoronmeridian-eu-storage-rg,Readeronmeridian-eu-compute-rg
Service principals:
meridian-eu-deploy-sp: Used by the EU CI/CD pipeline. HasContributoron the entiremeridian-prod-eusubscription. Client secret stored in Azure Key Vault.
Key Vault: meridian-eu-kv contains:
eu-db-connection-string: PostgreSQL connection string for the EU freight databaseeu-deploy-sp-secret: The client secret formeridian-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:
| Step | Principal | Action | Resource | CloudTrail Event | Visibility Cost |
|---|---|---|---|---|---|
| 1 | meridian-cicd-runner | sts:AssumeRole | meridian-deploy-staging | AssumeRole (STS) | 1 |
| 2 | meridian-deploy-staging | sts:AssumeRole | meridian-ops-admin | AssumeRole (STS) | 1 |
| 3 | meridian-ops-admin | s3:GetObject | meridian-freight-manifests | GetObject (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 themeridian-freight-manifestsS3 bucket via a three-step AssumeRole chain totaling three CloudTrail events. All three events arereadOnly: falsebut 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-accountAssumeRolewhere 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.34is in the dev VPC range, not a prod VPC range. A SIEM rule that alerts onGetObjectonmeridian-freight-manifestsfrom 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
AssumeRolesource ARN →roleArnpairs.
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-rootandblock-privileged-containerswould prevent the pod from being scheduled at all (if enforced — Meridian Freight has these policies inwarnmode, notdenymode)
Report finding:
"The
freight-manifest-apideployment in thefreight-backendnamespace runs withprivileged: trueand mounts/var/run/docker.sockfrom the host. Either misconfiguration independently enables complete host compromise from a container breakout. The EKS node's instance profile (meridian-eks-node) hasiam:CreatePolicyVersionon the sharedmeridian-ops-policy, making container-to-IAM-admin a single step from the EKS node. Immediate remediation: enforce OPA/Gatekeeper admission policies indenymode, removeprivileged: truefrom all pod specs, and remove the Docker socket hostPath volume from all non-system pods."
Detection Coverage Summary
| Technique | MITRE ID | Detection Source | Signal Strength |
|---|---|---|---|
| IMDS credential theft (IMDSv1) | T1552.005 | VPC flow logs to 169.254.169.254, CloudTrail GetCallerIdentity from unexpected IP | High |
| AssumeRole chain | T1078.004 | CloudTrail AssumeRole; SIEM correlation on cross-account chains | Medium |
| IAM AttachUserPolicy | T1098.001 | CloudTrail AttachUserPolicy; GuardDuty PrivilegeEscalation finding | High |
| IAM CreatePolicyVersion | T1098.001 | CloudTrail CreatePolicyVersion; CloudTrail Insights on write-rate spike | High |
| Lambda-based privesc | T1648 | CloudTrail CreateFunction + InvokeFunction correlation | Medium |
| Privileged container | T1610 | Falco: container.privileged=true; Kubernetes Audit Log | High |
| Docker socket mount | T1610 | Falco: fd.name=/var/run/docker.sock; Kubernetes Audit Log | High |
| IMDS from container | T1552.005 | Falco: fd.sip=169.254.169.254 and container.id != "" | Medium |
| Managed identity token theft | T1552.005 | Azure Activity Log: token issuance from unexpected IP | Medium |
| Role assignment escalation | T1098 | Azure Activity Log: Microsoft.Authorization/roleAssignments/write | High |
Engagement Timeline
| Day | Activity | Finding |
|---|---|---|
| 1 | Foothold: CI/CD runner compromise via dependency confusion | meridian-cicd-runner role credentials |
| 1 | IMDS credential theft (IMDSv1 enabled on runner nodes) | AccessKeyId + SecretAccessKey for meridian-cicd-runner |
| 2 | Permission graph enumeration via sts:GetCallerIdentity, iam:SimulatePrincipalPolicy | Mapped all role assumptions, identified cross-account path |
| 2 | Path A: meridian-cicd-runner → meridian-deploy-staging → meridian-ops-admin | Access to meridian-freight-manifests S3 bucket |
| 3 | EKS pod inspection: discovered freight-manifest-api misconfiguration | Privileged pod + Docker socket mount + writable /etc |
| 3 | Container escape analysis: three escape vectors from single pod | EKS node access → meridian-eks-node role → iam:CreatePolicyVersion |
| 4 | Azure footprint: EU FTS VM managed identity → Key Vault | eu-deploy-sp-secret — service principal credentials for entire EU subscription |
| 4 | Azure RBAC: meridian-eu-deploy-sp → Contributor on subscription | Simulated scope of compromise: all EU resources |
| 5 | Reporting: CloudTrail forensics, detection gap mapping, remediation roadmap | Artifact 09-A delivered to Meridian Freight CISO |