Phase 09 WARMUP — Cloud & Container Red Team
Operation Cedar Lattice, Phase 09. Before the labs, build the mental model that makes the graph-solver meaningful. This guide takes you from first principles — what is an IAM principal, why does the evaluation order matter, what does a Linux namespace actually isolate — to the practitioner level where you can look at a permission graph and immediately see the lowest-detection path, or look at a container config and immediately see the escape vector and the Falco rule that catches it.
Safety boundary (unchanged). Every offensive concept ends in its detection. No working exploits, no shellcode, no deployable bypasses appear in this document.
Table of Contents
- Chapter 1: Cloud IAM Mental Model
- Chapter 2: AWS IAM Privilege Escalation Paths
- Chapter 3: IMDS v1 vs v2
- Chapter 4: Azure RBAC
- Chapter 5: Container Security Model
- Chapter 6: Container Escape Paths
- Chapter 7: CloudTrail and Activity Log Telemetry
- Chapter 8: Misconceptions
- Lab Walkthrough
- Success Criteria
- Common Mistakes
- Interview Q&A
- References
Chapter 1: Cloud IAM Mental Model
What IAM is, from zero
Identity and Access Management (IAM) is the authorization layer between a principal (who is
making a request) and a resource (what they are trying to act on). Every API call in AWS —
whether it comes from a human logging into the console, a Lambda function running in us-east-1,
or a CI/CD runner calling s3:PutObject — is evaluated by IAM before the service processes it.
A principal is anything that can make an authenticated request: an IAM user, an IAM role (assumed by a service or a human), an AWS service acting on your behalf, or a federated identity from an external IdP. Principals are identified by their ARN (Amazon Resource Name).
ARN format:
arn:partition:service:region:account-id:resource-type/resource-id
Examples:
arn:aws:iam::114700000000:user/alice
arn:aws:iam::114700000000:role/meridian-cicd-runner
arn:aws:iam::114700000000:policy/AdminPolicy
arn:aws:s3:::meridian-freight-manifests
The region field is empty for IAM resources because IAM is global. The account ID is the 12-digit AWS account number.
Policy types
AWS has six policy types, evaluated in a strict order:
| # | Type | Who attaches it | Scope |
|---|---|---|---|
| 1 | Service Control Policy (SCP) | AWS Organizations (management account) | All principals in OU or account |
| 2 | Resource-based policy | Resource owner | Who can access the resource |
| 3 | Identity-based policy | Principal (inline or managed) | What the principal can do |
| 4 | Permission boundary | Administrator on the principal | Maximum allowed, not granted |
| 5 | Session policy | Caller at sts:AssumeRole time | Limit a session's effective permissions |
| 6 | VPC endpoint policy | VPC endpoint | Requests through that endpoint only |
The golden rule: deny always wins
The evaluation logic has one overriding rule: an explicit Deny in any applicable policy always wins, regardless of how many Allows exist elsewhere. The order of evaluation is:
- If any policy has an explicit Deny matching the action+resource → DENY (final).
- If no Deny, is there an explicit Allow in any applicable policy? → potential ALLOW.
- If no Allow → implicit DENY (default).
But the evaluation happens in layers:
Request arrives
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 1: SCP evaluation │
│ Does the organization SCP allow this action? │
│ NO → DENY (end) YES → continue │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 2: Resource-based policy │
│ Does the resource have a policy? Does it Allow? │
│ Explicit Deny → DENY (end) │
│ Explicit Allow (and no SCP block) → ALLOW (end) │
│ Not present or no Allow → continue │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 3: Identity-based policy │
│ Does the principal's policy Allow this action? │
│ Explicit Deny → DENY (end) │
│ No Allow → implicit DENY (end) │
│ Allow → continue to boundaries │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 4: Permission boundary (if set) │
│ Does the boundary also Allow this action? │
│ NO → DENY YES → continue │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Layer 5: Session policy (if AssumeRole was used) │
│ Does the session policy also Allow? │
│ NO → DENY YES → ALLOW (final) │
└─────────────────────────────────────────────────────┘
Key insight for attackers and defenders alike: SCPs are the organization-wide ceiling. A
developer cannot grant themselves more than the SCP allows, no matter how permissive their
identity policy is. But most organizations do not use SCPs for fine-grained control — they set
a coarse "Effect": "Deny", "Action": "organizations:LeaveOrganization" and call it done, which
means the identity-based policy layer is where the real escalation happens.
Resource vs identity policies
Identity-based policy — attached to the principal (user, role, group). Answers: "what can this
principal do?" Example: a policy attached to meridian-cicd-runner that allows s3:PutObject on
arn:aws:s3:::meridian-artifacts/*.
Resource-based policy — attached to the resource itself. Answers: "who can access this resource?" S3 bucket policies, KMS key policies, SQS queue policies, Lambda function policies are all resource-based. They can grant cross-account access directly without requiring the other account to have an identity-based policy that mirrors it.
The cross-account exception: For cross-account access, BOTH the resource-based policy (which must allow the foreign principal) AND the identity-based policy in the foreign account (which must allow the action) must agree. For same-account access, a resource-based policy Allow alone is sufficient without a matching identity-based policy.
Condition keys
Policies can have Condition blocks that restrict when the Allow or Deny applies. Common
condition keys used in defensive policies:
aws:RequestedRegion— restrict to specific regionsaws:SourceVpcandaws:SourceVpce— restrict to calls from a specific VPCaws:MultiFactorAuthPresent— require MFAaws:PrincipalTag/CostCenter— tag-based ABACiam:PassedToService— restrict what services a role can be passed to
From an attacker's perspective, conditions are bypass opportunities. A policy that says
"Condition": {"StringEquals": {"aws:RequestedRegion": "us-east-1"}} only applies to us-east-1
requests — the same action in eu-west-1 may be unconstrained.
Chapter 2: AWS IAM Privilege Escalation Paths
Privilege escalation in AWS IAM does not require a vulnerability in AWS itself. It requires only that an identity has one or more permissions that, individually, sound harmless but in combination allow creating or modifying a policy to grant admin access. These paths were systematically catalogued by Rhino Security Labs (see References).
The key insight: any permission that lets you modify policy attachments or create new policy versions is a privilege-escalation primitive.
Path 1: iam:CreatePolicyVersion
What you need: iam:CreatePolicyVersion on a managed policy that is attached to any principal
with broad permissions (or that you can attach to yourself).
What you do: Create a new version of an existing managed policy with "Action": "*" and
"Resource": "*", and set it as the default version. The policy now grants admin to everything
it is attached to.
CloudTrail event: CreatePolicyVersion in iam.amazonaws.com. The request parameters
include the policy ARN and the new policy document. The response includes the new version ID.
Detection signal: CreatePolicyVersion events where the policy document contains "*" in
Action or Resource are extremely high-signal. GuardDuty has a finding for this:
Policy:IAMUser/RootCredentialUsage (adjacent). A custom CloudTrail Insights alert on
CreatePolicyVersion by non-automation principals catches most cases.
Path 2: iam:AttachUserPolicy
What you need: iam:AttachUserPolicy (optionally scoped to Resource: "*").
What you do: Attach the AWS-managed AdministratorAccess policy
(arn:aws:iam::aws:policy/AdministratorAccess) to yourself or to a user you control.
CloudTrail event: AttachUserPolicy in iam.amazonaws.com. Request parameters include
userName (the target) and policyArn.
Detection signal: Any AttachUserPolicy event where policyArn contains AdministratorAccess
or the attaching principal is not a known administrative automation role is a critical alert.
This is one of the most obvious escalations — it appears in CloudTrail with both the actor and
the target user in the same event.
Path 3: iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction
What you need: iam:PassRole on a high-privileged role, lambda:CreateFunction, and
lambda:InvokeFunction.
What you do: Create a Lambda function and pass it a privileged IAM role as its execution
role. The Lambda function then runs as that role. Invoke the function with a payload that
calls iam:AttachUserPolicy or iam:CreatePolicyVersion on behalf of the privileged role.
The privilege escalation happens inside Lambda, attributed to the Lambda function's ARN.
CloudTrail events (sequence):
PassRole— logged as an IAM event in therequestParametersofCreateFunctionCreateFunctioninlambda.amazonaws.comInvokeFunctioninlambda.amazonaws.com- Inside the Lambda: the actual IAM write (e.g.,
AttachUserPolicy) attributed to the Lambda function's role, not the original attacker ARN.
Detection signal: This is a multi-step chain. The key detection is correlating
CreateFunction (where role in requestParameters is a high-privilege role ARN) with
subsequent IAM writes attributed to that Lambda function's ARN. Lambda creation by non-standard
principals should alert.
Path 4: iam:CreateLoginProfile
What you need: iam:CreateLoginProfile on IAM users.
What you do: Create a console-access login profile (username + password) for an IAM user that has no login profile configured but has high-privilege policies attached. You then log in as that user via the AWS console.
CloudTrail event: CreateLoginProfile in iam.amazonaws.com. Request parameters include
userName. No password is logged (AWS sanitizes it).
Detection signal: CreateLoginProfile events for users that previously had no console access.
GuardDuty's UnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B (console login from unusual IP)
is the downstream signal. The CreateLoginProfile call itself is the earlier, higher-confidence
indicator.
Path 5: sts:AssumeRole chains
What you need: sts:AssumeRole on a role, and that role has permissions not available to
your current principal. Chains of AssumeRole let you hop across accounts or assume roles with
broader permissions.
What you do: Assume a role, use that role's permissions (including potentially another
sts:AssumeRole), and repeat. In Meridian Freight's environment, meridian-cicd-runner can
assume meridian-deploy-staging, which can assume meridian-ops-admin in the production account.
CloudTrail events: One AssumeRole event per hop in sts.amazonaws.com. Each event logs:
- The
roleArnbeing assumed - The
principalArndoing the assuming - The
sourceIdentity(if set) - The resulting session ARN
Detection signal: Unusual AssumeRole chains — especially cross-account ones — are one of the
highest-value correlation opportunities. A SIEM rule that flags AssumeRole where the source
principal is not in the expected list of automation ARNs catches most human-driven lateral movement.
GuardDuty's UnauthorizedAccess:IAMUser/TorIPCaller and CredentialAccess:IAMUser/AnomalousBehavior
findings are adjacent.
Summary table
| Path | Permission needed | CloudTrail event | Noise level |
|---|---|---|---|
| CreatePolicyVersion | iam:CreatePolicyVersion | CreatePolicyVersion (IAM) | Very high |
| AttachUserPolicy | iam:AttachUserPolicy | AttachUserPolicy (IAM) | Very high |
| PassRole + Lambda | iam:PassRole + lambda:CreateFunction + lambda:InvokeFunction | CreateFunction, InvokeFunction | Medium |
| CreateLoginProfile | iam:CreateLoginProfile | CreateLoginProfile (IAM) | High |
| AssumeRole chain | sts:AssumeRole | AssumeRole (STS) | Low (very common) |
Chapter 3: IMDS v1 vs v2
What IMDS is
The Instance Metadata Service (IMDS) is an HTTP endpoint available from within every EC2 instance
at the link-local address 169.254.169.254. It provides metadata about the instance: the instance
ID, availability zone, security groups, user-data, and — critically — the temporary credentials
for the IAM role attached to the instance (the instance profile).
The credentials endpoint:
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>
This returns a JSON object containing:
AccessKeyIdSecretAccessKeyToken(the session token for the temporary credentials)Expiration(typically 6 hours from now)
These credentials are equivalent to calling sts:AssumeRole as the instance profile role. If
the role has broad permissions, obtaining these credentials from inside the instance (or from any
process that can reach the IMDS endpoint) gives the attacker those permissions.
IMDSv1: no authentication
IMDSv1 is a simple HTTP GET. Any process on the instance — or any process that can issue HTTP
requests to 169.254.169.254 (e.g., via an SSRF vulnerability in a web application) — can
retrieve the credentials. There is no token, no authentication, no request signing.
SSRF to IMDSv1 credential theft pattern:
- Attacker finds an SSRF vulnerability in a web application running on an EC2 instance.
- Attacker uses the SSRF to make the server request
http://169.254.169.254/latest/meta-data/iam/security-credentials/. - The response lists the role name.
- Attacker fetches
http://169.254.169.254/latest/meta-data/iam/security-credentials/<role-name>. - The response contains AccessKeyId, SecretAccessKey, Token.
- Attacker uses these from outside AWS to make API calls.
This was the exact path in the Capital One breach (2019). SSRF → IMDSv1 → role credentials → S3 data.
IMDSv2: token-based authentication
IMDSv2 requires a two-step process:
Step 1: PUT request to get a session token (TTL-limited):
PUT http://169.254.169.254/latest/api/token
X-aws-ec2-metadata-token-ttl-seconds: 21600
Response body: a token string (opaque, time-limited).
Step 2: GET request with the token in the header:
GET http://169.254.169.254/latest/meta-data/iam/security-credentials/
X-aws-ec2-metadata-token: <token>
The critical security property of IMDSv2 is the hop limit. The PUT request to get the token
has a default TTL for the IP packet of hop_limit=1. This means the PUT request cannot be
forwarded — it will be dropped by the first routing hop. A web application receiving an SSRF
request and forwarding it to 169.254.169.254 will fail to get a token, because the request is
coming from the application process, not from a fresh layer-3 hop. The SSRF chain is broken.
When hop limit is set to 2 or higher: The protection is weakened. Requests from containers inside the EC2 instance may need hop_limit=2 if they go through the Docker bridge network. Some EKS configurations set hop_limit=2 as a workaround. This restores the SSRF risk.
What credentials are available
The IAM security credentials response contains:
{
"Code": "Success",
"LastUpdated": "2024-01-15T10:00:00Z",
"Type": "AWS-HMAC",
"AccessKeyId": "ASIA...",
"SecretAccessKey": "...",
"Token": "...",
"Expiration": "2024-01-15T16:00:00Z"
}
The Token field makes these temporary credentials (STS-style). They expire, typically 6 hours
after issuance. The AccessKeyId starts with ASIA (not AKIA which is for long-term user keys).
Detection: IMDS abuse
CloudTrail signals:
- Credentials obtained from IMDS appear in CloudTrail as
userIdentity.type = "AssumedRole"with thearncontaining the instance profile role. If the source IP is not the instance's IP (i.e., the credentials are being used from a different machine), this is a strong signal. GetCallerIdentityis often the first call an attacker makes to confirm credentials work. AGetCallerIdentityfrom an unexpected source IP for an instance-profile role is high-confidence.
VPC flow logs:
- Flows to
169.254.169.254:80from unexpected processes or at unusual times. - In EKS environments, flows from the container network range (e.g.,
10.0.0.0/8) to169.254.169.254indicate a container is reaching IMDS.
Process-level detection:
- Any process in the container namespace making outbound connections to
169.254.169.254should be anomalous if the container has no legitimate need for it. - Falco rule:
fd.sip=169.254.169.254 and container.id != ""fires when a containerized process connects to IMDS.
Chapter 4: Azure RBAC
Scope hierarchy
Azure organizes resources in a four-level scope hierarchy:
Management Groups
└─ Subscriptions
└─ Resource Groups
└─ Resources (VMs, storage accounts, etc.)
Role assignments are made at a scope level. A role assigned at a higher scope (e.g., subscription) is inherited by all child scopes (resource groups, resources). A role assigned at the resource group level only applies to resources in that group.
Meridian Freight Azure layout:
- Management Group:
meridian-freight-root - Subscriptions:
meridian-prod(ERP + TMS),meridian-dev(developer workloads) - Resource Groups:
meridian-freight-rg(compute),meridian-data-rg(storage),meridian-net-rg(networking)
Built-in roles
Azure has three fundamental built-in roles:
| Role | Actions | Effect |
|---|---|---|
Owner | * | Full control including assigning roles to others |
Contributor | * (minus authorization) | Create and manage resources, cannot grant role assignments |
Reader | */read | View all resources, cannot change anything |
There are also over 70 service-specific built-in roles (e.g., Storage Blob Data Contributor,
Key Vault Secrets User). The principle of least privilege demands these over the broad roles.
Custom roles
Custom roles are defined with an Actions list (what is allowed), NotActions (deny-list
subtracted from Actions), DataActions (data plane access), and AssignableScopes (which scopes
the role can be assigned at). Custom roles in Meridian Freight's environment include
meridian-ops-read (read across all resource groups) and meridian-deploy (contributor on
meridian-dev subscription only).
Dangerous permissions
Microsoft.Authorization/roleAssignments/write — This is the Azure equivalent of
iam:AttachUserPolicy. Any principal with this permission can assign themselves (or anyone else)
the Owner role at the scope where they have this permission. If held at the subscription scope,
this is full account compromise.
Microsoft.Compute/virtualMachines/runCommand/action — Allows executing arbitrary shell
commands inside a VM via the Azure portal or API. Even a Reader with this additional permission
can execute code on any VM in scope, bypass network controls, and read files from the VM filesystem
(including secrets, cached credentials, etc.).
Microsoft.KeyVault/vaults/secrets/read and Microsoft.KeyVault/vaults/secrets/getSecret/action
— Access to Key Vault secrets. These may include storage account access keys, database passwords,
or service principal credentials — any of which can enable broader access.
Managed identity
A Managed Identity is a service principal that is automatically managed by Azure and attached to a resource (VM, App Service, Function App, AKS pod). There are two types:
- System-assigned: Tied to the resource lifecycle. Deleted when the resource is deleted.
- User-assigned: Independent resource, can be attached to multiple resources.
From a security perspective, managed identities are credentials without secrets — you never see
the underlying client secret, and Azure handles rotation. But they are still principals with RBAC
assignments. If a VM has a system-assigned managed identity with Contributor on the subscription,
any code running on that VM (including code in a compromised container) can call Azure Resource
Manager APIs using the managed identity.
Abuse path: Code running on the VM calls the Azure Instance Metadata Service (IMDS) at
http://169.254.169.254/metadata/identity/oauth2/token to get an access token for the managed
identity. This token can be used to call Azure Resource Manager or any Azure service the managed
identity has access to.
Detection: Azure Activity Log records Microsoft.Authorization/roleAssignments/write events
with the caller's objectId and the new role assignment. Monitor for role assignments made outside
of the infrastructure-as-code pipeline (e.g., Terraform or Bicep runs in CI/CD). Alert on any
Owner or Contributor assignment at subscription or management-group scope.
Chapter 5: Container Security Model
What makes containers "isolated"
Containers are not virtual machines. They run as processes on the host kernel with isolation provided by Linux kernel features: namespaces (which parts of the system a process can see) and cgroups (how many resources a process can consume). Both are kernel primitives that have been available in Linux since 2008 (namespaces) and 2007 (cgroups). Docker and Kubernetes use these primitives to provide the illusion of isolation.
Understanding what each namespace isolates — and what it does not isolate — is prerequisite knowledge for understanding every container escape technique.
Linux namespaces
Linux has 8 namespace types. Docker uses 6 of them by default:
PID namespace — Each container gets its own PID 1 (typically the container's init process).
Processes inside the container cannot see or signal host processes. PID 1 in the container is
a different PID on the host. Escape implication: --pid=host removes this isolation —
the container can see and ptrace all host processes.
Network namespace — The container gets its own network stack: its own interfaces, routing
table, iptables rules, port bindings. eth0 inside the container is a virtual ethernet pair
(veth) connected to the host's bridge (docker0). Escape implication: Host networking
(--network=host) removes this — the container shares the host's network stack and can bind to
host ports directly.
Mount namespace — The container sees a different filesystem root (a copy-on-write overlay).
Host mounts are not visible unless explicitly bind-mounted. Escape implication: Bind-mounting
host paths (e.g., /:/host) into the container allows the container to read/write the host
filesystem. Mounting /var/run/docker.sock is a special case (see Chapter 6).
UTS namespace — The container has its own hostname and domain name. Cosmetic, but it means
hostname inside the container does not return the host's hostname.
IPC namespace — The container has its own System V IPC and POSIX message queues. Prevents inter-process communication between host and container via shared memory.
User namespace — Allows mapping container UIDs to different host UIDs. If enabled, a container running as UID 0 (root) can be mapped to a non-privileged UID on the host. Docker does not enable user namespaces by default. This means root inside the container is root on the host from the perspective of any resource that is accessible (e.g., host-bind-mounted files).
Network namespace limitation: Namespace isolation does not prevent a process from making network connections. A container can still reach the host's network via the Docker bridge gateway and can reach external networks via NAT. Namespace isolation is about visibility and resource ownership, not network filtering — that requires firewall rules (iptables/nftables) or network policies (in Kubernetes).
cgroups
Control groups (cgroups) limit how much of a resource a process can use: CPU time, memory,
block I/O, number of PIDs. A container configured with --memory=512m --cpus=0.5 has a cgroup
that will OOM-kill the container if it tries to allocate more than 512 MB, and will throttle it
to 50% of one CPU core.
cgroups do not provide security isolation — they provide resource isolation. A container with no cgroup limits can consume all host memory (OOM-killing other containers and the host itself), all CPU, and can fork-bomb the host. cgroup limits are a stability and fairness mechanism, not a security boundary.
Capabilities
Linux capabilities divide the monolithic root permission into ~40 distinct privileges. Docker
drops most capabilities by default and only grants a small subset:
Capabilities Docker grants by default:
CAP_CHOWN— change file ownershipCAP_DAC_OVERRIDE— bypass file permission checksCAP_FSETID— allow setting SUID/SGID bitsCAP_FOWNER— bypass permission checks for file operationsCAP_MKNOD— create special filesCAP_NET_RAW— use raw sockets (ping, ARP spoofing)CAP_SETGID,CAP_SETUID— change GIDs/UIDsCAP_SETPCAP— modify process capabilitiesCAP_NET_BIND_SERVICE— bind to ports below 1024CAP_SYS_CHROOT— usechrootCAP_KILL— send signals to any process in namespace
Capabilities Docker drops by default (these are dangerous):
CAP_SYS_ADMIN— the "god capability." Mount filesystems, clone namespaces, load kernel modules, useioctlfor disk operations. If a container has this, most escape techniques work.CAP_SYS_PTRACE— ptrace host processes (if--pid=hostis set).CAP_NET_ADMIN— modify network stack, intercept traffic.CAP_SYS_MODULE— load/unload kernel modules. Loading a malicious kernel module = full host compromise.CAP_SYS_RAWIO— access/dev/mem,/dev/kmem(raw memory access).
Default seccomp profile
Docker applies a seccomp profile by default that blocks ~44 system calls considered dangerous:
keyctl, add_key, request_key (kernel keyring), ptrace, mbind, migrate_pages,
move_pages, set_mempolicy (NUMA controls), perf_event_open, reboot, setns,
unshare, syslog, acct, settimeofday, swapon, swapoff, mount, umount2, etc.
--privileged disables the seccomp profile entirely.
AppArmor and SELinux
Docker applies the docker-default AppArmor profile on systems where AppArmor is available. This
profile denies access to host kernel files, /proc/sysrq-trigger, /proc/sys/kernel/core_pattern,
and other host-level paths, even if the container's UID is root.
SELinux (used on RHEL/CentOS-based hosts) applies type enforcement labels. Containers get the
container_t type, which confines them to container-specific types. Even a container running as
root cannot write to host system files labeled system_u:object_r:lib_t:s0 without an SELinux
policy exception.
--privileged disables AppArmor and SELinux confinement as well.
What --privileged disables
When you run a container with --privileged:
- All Linux capabilities are granted (including
CAP_SYS_ADMIN,CAP_SYS_MODULE,CAP_SYS_RAWIO) - The seccomp filter is removed (all system calls allowed)
- AppArmor and SELinux confinement is disabled
- All host devices (
/dev/*) are accessible inside the container - The container can mount any filesystem that the host kernel supports
The result is that the only remaining isolation is the mount and PID namespace (and user namespace
if enabled). A privileged container with access to /dev/sda (the host disk) can mount the host
filesystem and modify anything. A privileged container can also call unshare --mount to create
a new mount namespace and mount the host filesystem inside it.
Chapter 6: Container Escape Paths
Escape 1: --privileged flag
What it requires: The container was launched with --privileged.
What the attacker does: Inside the privileged container:
- List block devices:
fdisk -lor examine/dev/for block devices (e.g.,/dev/xvda1). - Create a mount point directory inside the container.
- Mount the host root filesystem at the mount point.
- Read or modify any file on the host:
/etc/shadow,/etc/cron.d, SSH authorized_keys, environment files containing secrets, etc. - Write a cron job or systemd unit to execute code as root on the host at next reboot or cron cycle.
The escape does not require any vulnerability — it is a configuration choice that removes the security boundary.
Falco detection:
- rule: Launch Privileged Container
desc: Detect the launch of a container with --privileged flag
condition: >
container.privileged = true and
evt.type = container
output: >
Privileged container launched (user=%user.name container=%container.name
image=%container.image.repository:%container.image.tag)
priority: CRITICAL
tags: [container, privilege_escalation, T1610]
Escape 2: --pid=host
What it requires: The container was launched with --pid=host, sharing the host PID namespace.
What the attacker does:
- Inside the container,
ps auxshows all host processes, including PID 1 (systemdorinit). - With
CAP_SYS_PTRACE(which requires either--privilegedor explicit--cap-add), the attacker can ptrace host processes and inject code. - Even without ptrace, seeing host process names reveals running services, their PIDs, and allows sending signals to host processes.
/proc/<host-pid>/root/from inside the container points to the host's filesystem root for that process, potentially exposing host filesystem paths.
Falco detection:
- rule: Container with Host PID Namespace
desc: Detect containers running with host PID namespace
condition: >
container.host_pid = true and
evt.type = container
output: >
Container launched with host PID namespace (container=%container.name
image=%container.image.repository)
priority: HIGH
tags: [container, privilege_escalation]
Escape 3: Docker socket mounted
What it requires: /var/run/docker.sock bind-mounted into the container.
What the attacker does:
- The Docker socket is the Unix socket the Docker daemon listens on. Any process with read/write
access to it can send Docker API requests — equivalent to running
dockercommands as root. - Inside the container:
docker -H unix:///var/run/docker.sock run --privileged --pid=host -v /:/host <any-image> chroot /host bashlaunches a new privileged container from inside the compromised container, with the host filesystem mounted. - This effectively gives the attacker root on the host, even if the original container was running as a non-root user with no special capabilities (just the socket access).
This is also a common Kubernetes misconfiguration: DaemonSets for log collectors or monitoring agents are given Docker socket access so they can inspect running containers, but any compromise of that pod results in host compromise.
Falco detection:
- rule: Docker Socket Mounted in Container
desc: Detect container with Docker socket mounted
condition: >
fd.name = /var/run/docker.sock and
evt.type in (open, openat) and
container.id != ""
output: >
Docker socket opened from container (user=%user.name container=%container.name
image=%container.image.repository command=%proc.cmdline)
priority: CRITICAL
tags: [container, privilege_escalation, lateral_movement, T1610]
Escape 4: CAP_SYS_ADMIN
What it requires: Container has the cap_sys_admin capability (either from --privileged
or --cap-add=SYS_ADMIN).
What the attacker does:
mount()system call is allowed: mount the host's cgroup v1release_agentfilesystem, write a path to a writable script, trigger the release_agent with an empty cgroup. This is the "cgroup escape" technique.unshare --mount+mount /dev/<disk>: create a new mount namespace and mount the host disk.- Modify
/proc/sys/kernel/core_patternto redirect core dumps to an attacker-controlled program (arbitrary code execution on core dump).
Falco detection:
- rule: Container with CAP_SYS_ADMIN
desc: Detect container launched with CAP_SYS_ADMIN capability
condition: >
container.caps contains cap_sys_admin and
evt.type = container
output: >
Container launched with CAP_SYS_ADMIN (container=%container.name
image=%container.image.repository)
priority: HIGH
tags: [container, privilege_escalation]
Escape 5: Writable /etc mount
What it requires: The host's /etc (or a path containing sensitive configuration) is
bind-mounted into the container without the :ro (read-only) flag.
What the attacker does:
- Overwrite
/etc/cron.d/<name>with a cron job entry that runs as root on the host. - Overwrite
/etc/sudoersor a file in/etc/sudoers.d/to add the container's user to passwordless sudo. - Overwrite
/etc/ld.so.preloadto inject a shared library that runs as any user the next time any dynamically linked program is executed on the host. - Overwrite SSH configuration to allow password authentication or add keys to
/etc/ssh/authorized_keys(a non-standard path that some configs use).
The attacker does not need any special capabilities for this — just filesystem write access.
Falco detection:
- rule: Write to Sensitive Host Path from Container
desc: Detect writes to sensitive host paths mounted in container
condition: >
container.id != "" and
evt.type in (write, writev, rename, link) and
fd.name pmatch (/etc/cron.d, /etc/sudoers, /etc/ld.so.preload,
/etc/passwd, /etc/shadow)
output: >
Sensitive file write from container (container=%container.name
file=%fd.name command=%proc.cmdline)
priority: HIGH
tags: [container, persistence, T1543]
Chapter 7: CloudTrail and Activity Log Telemetry
CloudTrail event structure
Every API call to an AWS service that is covered by CloudTrail produces a log record (event) in the CloudTrail log. Events are delivered to an S3 bucket (and optionally CloudWatch Logs) in JSON format.
Key fields in every CloudTrail event:
{
"eventVersion": "1.08",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROA...:session-name",
"arn": "arn:aws:sts::114700000000:assumed-role/meridian-cicd-runner/session-name",
"accountId": "114700000000",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROA...",
"arn": "arn:aws:iam::114700000000:role/meridian-cicd-runner"
}
}
},
"eventTime": "2024-01-15T14:23:41Z",
"eventSource": "iam.amazonaws.com",
"eventName": "AttachUserPolicy",
"awsRegion": "us-east-1",
"sourceIPAddress": "10.0.1.45",
"requestParameters": {
"userName": "alice",
"policyArn": "arn:aws:iam::aws:policy/AdministratorAccess"
},
"responseElements": null,
"readOnly": false
}
userIdentity is the most important field for attribution. type can be:
Root— the AWS root accountIAMUser— an IAM user with long-term credentialsAssumedRole— temporary credentials from STS AssumeRole (most services, EC2 instance profiles)FederatedUser— federated identity (SAML, OIDC)AWSService— an AWS service acting on behalf of the account
eventName is the specific API action. This is what you correlate in your SIEM.
readOnly is true for read-only actions (List*, Describe*, Get*). Write actions
have readOnly: false — these are higher priority alerts.
Key events for red team activity
| eventName | eventSource | Significance |
|---|---|---|
AssumeRole | sts.amazonaws.com | Role assumption; log the roleArn and principalArn |
GetCallerIdentity | sts.amazonaws.com | First call by attacker confirming credentials; anomalous source IP = alert |
ListBuckets | s3.amazonaws.com | Reconnaissance; triggered by stolen creds being explored |
CreatePolicyVersion | iam.amazonaws.com | High-signal privesc attempt |
AttachUserPolicy | iam.amazonaws.com | High-signal privesc; log policyArn |
CreateLoginProfile | iam.amazonaws.com | Persistence; creating console access for a user |
CreateFunction | lambda.amazonaws.com | Lateral movement via Lambda |
GetSecretValue | secretsmanager.amazonaws.com | Credential theft from Secrets Manager |
RunInstances | ec2.amazonaws.com | Infrastructure abuse; cryptomining, C2 setup |
UpdateLoginProfile | iam.amazonaws.com | Password change for another user — backdoor |
GuardDuty findings
AWS GuardDuty is a threat detection service that consumes CloudTrail, VPC flow logs, and DNS query logs to produce finding objects. Key findings relevant to cloud red team:
CredentialAccess:IAMUser/AnomalousBehavior— API calls that deviate from a learned baselineUnauthorizedAccess:IAMUser/ConsoleLoginSuccess.B— console login from unusual geographyRecon:IAMUser/TorIPCaller— API calls from a Tor exit nodePrivilegeEscalation:IAMUser/AdministrativePermissions— attempts to grant broader permissionsUnauthorizedAccess:EC2/MetaDataDNSRebind— DNS rebinding to reach IMDS
CloudTrail Insights
CloudTrail Insights detects unusual write API call rates for an account. If an account normally
makes 5 CreatePolicyVersion calls per day and suddenly makes 50 in an hour, Insights generates
a finding. This provides anomaly-based detection on top of the rule-based GuardDuty findings.
SIEM correlation rules
Effective SIEM rules for cloud red team detection:
GetCallerIdentitywithin 60 seconds ofAssumeRolefrom a new source IP → credential theft verificationAttachUserPolicyorCreatePolicyVersionby any principal that is not an IaC automation ARN → privesc attemptAssumeRolechain depth > 2 (role A assumes role B which assumes role C) from a human session → lateral movementCreateFunctionwhereroleARN in request parameters has broad permissions → Lambda-based privesc setup- Any IAM write action from an IP outside the corporate egress range → stolen credentials in use
Chapter 8: Misconceptions
Misconception 1: "Root inside a container is not root on the host"
This is partly true and largely dangerous. Without user namespace remapping (which Docker does not enable by default), UID 0 inside the container is UID 0 on the host for any resource that leaks across the namespace boundary. A bind-mounted host path owned by root is fully writable by a root container process. The correct statement is: root inside a container that has no additional isolation (no user namespace, no AppArmor, writable host mounts) is effectively root on the host for those mounted resources.
Misconception 2: "The IMDS endpoint is only accessible from inside the VM"
IMDSv1 is accessible from any process that can reach 169.254.169.254. This includes:
- Application code running on the EC2 instance (obviously)
- A web application with an SSRF vulnerability on the instance
- A container running on the EC2 node if it can route to the link-local range
- A Lambda function (which has its own IMDS)
The restriction is network-level, not authentication-level. IMDSv2 adds authentication (the token requirement) as a second line of defense.
Misconception 3: "IAM permission boundaries prevent privilege escalation"
Permission boundaries limit what an identity can do, but they must be explicitly set. An
administrator who creates a new IAM user without setting a permission boundary leaves that user
with only their identity-based policy as the ceiling. More importantly, if an attacker can
create a new role (with iam:CreateRole) they choose not to set a boundary on that new role.
The boundary only restricts the identity it is attached to — the attacker creates a new identity
without one.
Misconception 4: "GuardDuty catches all attacks in real time"
GuardDuty has alerting latency (typically 5-15 minutes) and uses a 14-day baseline period for anomaly detection. A new attacker using credentials for the first time has no baseline — GuardDuty will not generate an anomaly finding until the behavior deviates from the learned pattern, which requires a pattern to have been learned. Rule-based findings (e.g., Tor exit node caller) are faster but require the attacker to use flagged infrastructure.
Misconception 5: "Container images are immutable, so containers are secure"
Image immutability means the base image layers cannot be changed after build — it says nothing
about runtime security. A container running an immutable image can still be launched with
--privileged, with host mounts, with the Docker socket, or with dangerous capabilities. The
image layer and the container runtime configuration are orthogonal security dimensions.
Misconception 6: "Secrets Manager and Parameter Store are always safer than environment variables"
Secrets Manager and Parameter Store are safer for storage (no cleartext in Dockerfiles or source
control). But at runtime, the secret must be fetched and made available to the application — often
as an environment variable. A container compromise that allows reading /proc/1/environ (the
environment of PID 1) will reveal secrets regardless of how they were stored. The improvement is
in storage and auditability (Secrets Manager logs every GetSecretValue to CloudTrail), not in
runtime isolation.
Misconception 7: "A read-only container filesystem prevents container escape"
Read-only filesystems (--read-only) prevent writes to the container's layered filesystem.
They do not prevent:
- Writing to explicitly mounted writable volumes (including host bind-mounts)
- Network operations
- System calls that don't require filesystem writes
- Capability-based escapes (
cap_sys_adminmount operations use syscalls, not writes)
Read-only filesystem is a defense-in-depth measure for persistence, not a container boundary.
Lab Walkthrough
Lab 01: Cloud IAM Attack Path Solver
Goal: Given a synthetic IAM permission graph, implement:
can_reach(graph, start, target)— BFS to determine reachabilityattack_path(graph, start, target)— Dijkstra to find lowest-detection-cost pathprivilege_escalation_edges(graph)— filter edges by PRIVESC_ACTIONSdetection_for_edge(edge)— look up CloudTrail event for an action
Understanding the graph structure:
graph = {
"nodes": ["dev-user", "ec2-role", "admin-role", "s3-data"],
"edges": [
{"principal": "dev-user", "action": "sts:AssumeRole",
"resource": "ec2-role", "condition": None},
{"principal": "ec2-role", "action": "iam:AttachUserPolicy",
"resource": "admin-role", "condition": None},
{"principal": "admin-role", "action": "s3:GetObject",
"resource": "s3-data", "condition": None},
]
}
Each edge represents a permission: principal has action on resource. Traversal means: if you
can become a principal (by reaching it via a prior edge's resource), you can use that principal's
edges.
BFS for can_reach:
- Start:
{start}is the initial reachable set - At each step: for every node in the reachable set, follow all edges where
principal == node, addresourceto reachable - Return True if
targetin reachable when no new nodes are being added
Dijkstra for attack_path:
- State: current node
- Cost: cumulative sum of
VISIBILITY_COSTS[action](lower = less detectable) - On reaching target: reconstruct path from predecessor map
- Return list of edge dicts with
detectionkey added (fromdetection_for_edge)
Step 3 — privilege_escalation_edges: Filter the edges list to only those where
edge["action"] in PRIVESC_ACTIONS.
Step 4 — detection_for_edge: Format string as
"CloudTrail: <event> | principal=<p> resource=<r>".
Lab 02: IMDS & Container Escape Analyzer
Goal: Given synthetic configuration facts, implement:
imds_findings(facts)— detect IMDSv1 and weak hop-limitcontainer_escape_findings(facts)— detect escape-enabling misconfigurationsanalyze(facts)— combine findings and determine overall risk leveldetection_for_finding(finding)— look up Falco rule or CloudTrail event
imds_findings logic:
- If
facts["imds_version"] == "v1"→ add finding with idimds_v1_accessible, severityHIGH - If
facts["imds_version"] == "v2"andfacts.get("imds_hop_limit", 1) > 1→ add finding with idimds_v2_no_hop_limit, severityMEDIUM
container_escape_findings logic:
"--privileged" in facts["container_flags"]→ CRITICAL findingprivileged_container"--pid=host" in facts["container_flags"]or"host_pid" in facts["container_flags"]→ HIGH findinghost_pid_namespace- Any volume string containing
"/var/run/docker.sock"→ CRITICAL findingdocker_socket_mounted "cap_sys_admin" in facts["capabilities"]→ HIGH findingcap_sys_admin- Any volume string containing
"/etc"and not ending in":ro"→ HIGH findingwritable_etc_mount
analyze logic: Combine both finding lists, sort by severity order
(CRITICAL=0, HIGH=1, MEDIUM=2, LOW=3), determine risk_level from highest severity present.
Success Criteria
You are ready to move to Phase 10 when you can:
- Draw the AWS IAM policy evaluation flowchart from memory, labeling each layer and the deny-always-wins rule.
- Name five IAM privilege-escalation paths and the specific CloudTrail event each emits, without looking at notes.
- Explain the IMDSv2 hop-limit mechanism and why it prevents SSRF-to-IMDS chains from containers, in two or three sentences.
- List the three Linux primitives that provide container isolation and explain what each one prevents (and what it does not prevent).
- Write a Falco rule condition for detecting a privileged container launch.
- Given any AWS CloudTrail event JSON, identify the principal, action, source IP, and target resource.
-
Both lab test suites pass with
LAB_MODULE=solution pytest -q.
Common Mistakes
Mistake 1: Confusing IAM roles and IAM users
Users have long-term credentials (AccessKeyId + SecretAccessKey). Roles have no credentials of
their own — they are assumed via sts:AssumeRole to get short-term temporary credentials.
Roles are the mechanism for granting permissions to services, Lambda functions, EC2 instances,
and cross-account access.
Mistake 2: Thinking permission boundaries block everything Permission boundaries limit the maximum permissions an identity can have, but they must be set. They also do not prevent the creation of new identities (roles, users) that do not inherit the boundary.
Mistake 3: Assuming --no-new-privileges is equivalent to dropping capabilities
--no-new-privileges (or the no_new_privs seccomp flag) prevents setuid/setgid binaries
and Linux Security Module (AppArmor/SELinux) profile transitions from granting additional
privileges. It does not drop capabilities — a container with CAP_SYS_ADMIN and
--no-new-privileges still has CAP_SYS_ADMIN.
Mistake 4: Treating CloudTrail as real-time CloudTrail events are delivered to S3 within approximately 15 minutes of the API call. CloudWatch Logs integration is faster (~5 minutes) but still not instant. A detection pipeline built on raw CloudTrail S3 logs may have significant latency. GuardDuty and Security Hub process events faster but are not real-time either.
Mistake 5: Overlooking read-only API calls in investigations
ListBuckets, DescribeInstances, GetCallerIdentity are all readOnly: true in CloudTrail.
Many SIEM rules filter to readOnly: false for noise reduction, which means reconnaissance
activity goes undetected. A complete detection strategy monitors both.
Mistake 6: Dijkstra edge case — unreachable nodes
In the lab's attack_path implementation: when there is no path from start to target, the
function must return an empty list [], not raise an exception. The Dijkstra loop exits when
the priority queue is exhausted without reaching the target — handle this by returning []
after the loop.
Interview Q&A
Q1: What is the difference between IMDSv1 and IMDSv2 and why does it matter for security?
A: IMDSv1 is a simple unauthenticated HTTP GET to http://169.254.169.254/latest/meta-data/.
Any process — or any SSRF vulnerability — that can issue an HTTP request to that address can
retrieve the IAM role credentials attached to the instance. This was the vector in the Capital
One breach: an SSRF vulnerability caused the server to fetch its own IMDS credentials.
IMDSv2 requires a two-step process: a PUT request (with a custom X-aws-ec2-metadata-token-ttl-seconds
header) to obtain a session token, then a GET request with that token in the
X-aws-ec2-metadata-token header. The critical protection is the IP TTL (hop limit) set
to 1 on the PUT request. This means the PUT cannot be forwarded by an application — it is dropped
by the first routing layer. An SSRF that causes the server to forward an HTTP request cannot
get a token and therefore cannot get the credentials. The protection is network-layer enforcement,
not software-layer validation.
Security maturity: IMDSv2 should be enforced at the organization level via an SCP condition
("Condition": {"StringEquals": {"ec2:MetadataHttpTokens": "required"}}) so that no instance
can be launched with IMDSv1 enabled.
Q2: Explain the AWS IAM policy evaluation order.
A: AWS evaluates policies in a strictly ordered pipeline. First, Service Control Policies
(SCPs) from AWS Organizations set the ceiling — any action not allowed by the SCP is denied
regardless of identity policies. Next, resource-based policies are evaluated; an explicit Deny
here ends evaluation immediately, while an Allow in the same-account case can be sufficient
without a matching identity policy. Then identity-based policies attached to the requesting
principal (inline + managed) are checked. If a permission boundary is set on the principal,
the action must be allowed by both the identity policy and the boundary — the boundary cannot
grant permissions, only restrict them. Finally, if the request uses temporary credentials from
sts:AssumeRole, the session policy passed at assume-role time is also intersected.
The single most important rule: an explicit Deny in any policy at any layer terminates evaluation with a denial, overriding all Allows. An implicit Deny (no matching Allow anywhere) also results in denial. Only an explicit Allow that survives all layers results in access.
Q3: What permissions does an attacker need to escalate privileges in AWS IAM?
A: Several single permissions (or small combinations) are sufficient:
iam:CreatePolicyVersionalone: create a new version of an existing managed policy withAction: "*", Resource: "*"and set it as default. Every principal with that policy now has admin.iam:AttachUserPolicyalone: attachAdministratorAccessto themselves or any target user.iam:PassRole+lambda:CreateFunction+lambda:InvokeFunction: create a Lambda with a privileged execution role, invoke it to perform IAM writes.iam:CreateRole+iam:AttachRolePolicy: create a new role with no permission boundary, attach admin policy to it, then assume it.iam:CreateLoginProfile: create console credentials for a user with existing admin policy.
The common thread is that any permission that allows modifying policy attachments or creating new policy versions is effectively a privilege escalation if the target policy or role has admin-level permissions — or if the attacker can choose which policy to create.
Q4: How does --privileged in Docker enable container escape?
A: --privileged grants the container all Linux capabilities (including CAP_SYS_ADMIN),
removes the seccomp filter, and disables AppArmor/SELinux confinement. It also makes all host
devices (block devices, character devices in /dev/) accessible inside the container.
With all host devices accessible and CAP_SYS_ADMIN enabling the mount() system call, the
container can mount the host's root filesystem (e.g., /dev/xvda1) at a path inside the
container and then read or write any file on the host. This is not a vulnerability in Docker —
it is the documented behavior of --privileged. The "escape" is simply mounting the host disk
or modifying a host file (cron job, sudoers, SSH keys) to gain persistence or execute code as
root on the host outside the container context.
Q5: What is a managed identity and how can it be abused?
A: A managed identity is an Azure Active Directory service principal with credentials automatically managed by Azure — the administrator never sees or handles the client secret. They come in two flavors: system-assigned (tied to a specific resource's lifecycle) and user-assigned (independent resource, attachable to multiple VMs or services).
Abuse path: any code running on a VM with a system-assigned managed identity (or with a
user-assigned managed identity attached) can call the Azure IMDS endpoint at
http://169.254.169.254/metadata/identity/oauth2/token?resource=https://management.azure.com/
to obtain an OAuth2 access token for that identity. This token can be used to call Azure
Resource Manager APIs. If the managed identity has Contributor or Owner RBAC assignments,
the attacker can create new resources, exfiltrate data, read Key Vault secrets, or assign roles
to themselves to gain persistent access.
Detection: Azure Activity Log records token issuance and Resource Manager API calls. Unusual source IPs for managed identity API calls (credentials used from outside the VM's IP) are the key indicator.
Q6: How would you detect IMDS abuse in a cloud environment?
A: Detection requires correlation across multiple data sources:
First, CloudTrail — credentials obtained from IMDS produce AssumedRole-type userIdentity
entries. If those credentials are used from an IP address that is not the EC2 instance's IP
(visible in sourceIPAddress), the credentials have been exfiltrated. GetCallerIdentity
is typically the first call made by an attacker to verify credentials — correlate GetCallerIdentity
events from unexpected source IPs with instance profile role ARNs.
Second, VPC flow logs — flows to 169.254.169.254:80 from container subnet CIDR ranges
(not the host IP) indicate container-to-IMDS access, which is anomalous unless the container
has a legitimate reason.
Third, process-level telemetry (Falco, eBPF-based agents) — detect network connections
from container PIDs to 169.254.169.254. The Falco condition fd.sip = 169.254.169.254 and container.id != "" fires on any containerized process connecting to IMDS.
Fourth, GuardDuty — UnauthorizedAccess:EC2/MetaDataDNSRebind catches DNS-based IMDS
bypass attempts.
Q7: What is the difference between an IAM role and an IAM user?
A: An IAM user is a persistent identity with long-term credentials: an Access Key ID and Secret Access Key that are valid indefinitely (until rotated or deleted). IAM users are intended for human access or legacy automation that cannot use roles.
An IAM role is an identity without its own credentials. It is assumed via sts:AssumeRole,
which returns temporary credentials (AccessKeyId starting with ASIA, SecretAccessKey, and a
Session Token) that expire after a configured duration (15 minutes to 12 hours). Roles are the
intended mechanism for granting permissions to:
- AWS services (EC2 instance profiles, Lambda execution roles)
- Applications running on EC2/EKS/ECS
- Cross-account access
- Federation (SAML, OIDC)
Security implications: long-term user credentials that are compromised remain valid until
explicitly revoked. Temporary role credentials expire automatically. GuardDuty and CloudTrail
can detect AssumeRole by anomalous principals, making role assumption more auditable than
static key usage.
Q8: How does Falco detect container escape attempts?
A: Falco is a runtime security tool for Linux that uses eBPF (or kernel module) to capture
system calls and kernel events. It evaluates these events against a set of rules written in
YAML with a condition language that can reference fields like container.id, container.privileged,
fd.name, proc.cmdline, and evt.type.
For container escape detection, Falco uses several mechanisms:
Container launch events (evt.type = container): Falco captures the container metadata at
launch time, including whether container.privileged = true, what capabilities are set, and
what volumes are mounted. Rules fire immediately at container start.
System call monitoring: For escape techniques that happen at runtime (e.g., a process in the
container calling mount()), Falco rules can match on evt.type = mount and container.id != ""
to detect mount operations from inside containers. Similarly, openat events for
/var/run/docker.sock from a container context indicate Docker socket abuse.
Process spawning: proc.name = nsenter and container.id != "" detects namespace entry attempts
from inside a container. proc.cmdline contains chroot and container.id != "" catches chroot
escape attempts.
The key limitation: Falco detects behaviors, not intentions. A legitimate monitoring tool that mounts the Docker socket will also fire these rules. Effective deployment requires tuning (allowlisting known-good containers) and alert correlation with IAM and CloudTrail data.
References
- AWS IAM Policy Evaluation Logic: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html
- Rhino Security Labs IAM Privilege Escalation Research: https://rhinosecuritylabs.com/aws/aws-privilege-escalation-methods-mitigation/
- AWS IMDS Documentation (IMDSv2): https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
- Falco Rules Documentation: https://falco.org/docs/rules/
- Falco Default Rules: https://github.com/falcosecurity/rules/blob/main/rules/falco_rules.yaml
- MITRE ATT&CK T1078.004 — Valid Accounts: Cloud Accounts: https://attack.mitre.org/techniques/T1078/004/
- MITRE ATT&CK T1552.005 — Unsecured Credentials: Cloud Instance Metadata API: https://attack.mitre.org/techniques/T1552/005/
- MITRE ATT&CK T1610 — Deploy Container: https://attack.mitre.org/techniques/T1610/
- Azure RBAC Documentation: https://learn.microsoft.com/en-us/azure/role-based-access-control/overview
- Azure Managed Identities: https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview
- AWS GuardDuty Finding Types: https://docs.aws.amazon.com/guardduty/latest/ug/guardduty_finding-types-active.html
- CloudTrail Event Reference: https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-event-reference.html
- Pacu — AWS Exploitation Framework (defensive reference): https://github.com/RhinoSecurityLabs/pacu
- Peirates — Kubernetes Penetration Testing Tool (defensive reference): https://github.com/inguardians/peirates