Defending Against the whoAMI Attack with AWS Declarative Policies
Cloud Security Researcher and Advocate, Seth Art, recently published the blog post whoAMI: A cloud image name confusion attack. In it, Seth describes how an AWS account can become compromised by an attacker who creates an AMI with a similar name to an existing AMI. Users of AWS can be tricked into using the attacker's AMI if they forget to specify the owner of the AMI they want to use, an easy mistake to make.
Luckily, we can craft an AWS Declarative Policy, a type of security guardrail, that can prevent this attack!
The Attack 💥
I highly encourage you to read Seth's blog post to fully understand the attack, but let's set the context with one of the examples from the post.
Often, cloud engineers use Terraform, an Infrastructure-as-Code (IaC) tool, to create resources in the cloud. The Terraform code example below creates an AWS EC2 instance using the latest Ubuntu AMI.
# Find the latest Ubuntu AMI
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
}
# Create a new EC2 instance with the Ubuntu AMI
resource "aws_instance" "web_server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
}
The issue here is the owners attribute is not specified in the data "aws_ami" "ubuntu" block. This means the most_recent Ubuntu AMI from any AWS account will be used.
So, an attacker can create a more recent AMI with a similar name to ubuntu-focal-20.04-amd64-server-* e.g., ubuntu-focal-20.04-amd64-server-attacker-server-20250301 which would cause the Terraform code to use the attacker's AMI instead of a trusted one created by amazon or canonical (the owners of Ubuntu).
Here's an example of how to specify the
owners
attribute in the Terraform code.# Find the latest Ubuntu AMI
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
}
owners = ["099720109477"] # Canonical
}
Defining Security Guardrails with Declarative Policies 🛡️
In December 2024, AWS launched Declarative Policies, a new AWS Organizations policy type allowing for defining and enforcing controls across multiple accounts. As of this writing, Declarative Policies specifically support select controls for Amazon VPC, Amazon EBS, and Amazon EC2. The ladder is what we're interested in for this blog post.
For this to work, there are a few pre-requisites:
- AWS Organizations must be set up. See the AWS documentation.
- AWS Organizations Management Policies must be enabled. See the AWS documentation.
Below is an example Declarative Policy that defines only AMIs from Amazon and Canonical (
099720109477
) are allowed to be used. We can choose from three different state options:- enabled - Any AMI that does not meet the policy will be blocked from use for **new** instances.
- disabled - Nothing will happen as the policy is disabled.
- audit_mode - Any AMI that does not meet the policy will be marked with `Not Allowed` in the console and `ImageAllowed: false` in the CLI but these can still be used.
{
"ec2_attributes": {
"allowed_images_settings": {
"state": {
"@@assign": "audit_mode"
},
"image_criteria": {
"criteria_1": {
"allowed_image_providers": {
"@@assign": [
"amazon",
"099720109477"
]
}
}
}
}
}
}
Enabling the Declarative Policy in Audit Mode
When in Audit Mode, the policy will not block any AMIs that do not meet the policy. Instead, it will mark them as Not Allowed in the AWS Console and ImageAllowed: false in the AWS CLI. Terraform won't provide any indication that the AMI is not allowed by the policy.

$ aws --region us-east-1 ec2 describe-images --owners 111111111111
{
"Images": [
{
[SNIP]
"Description": "Do you trust me?",
"EnaSupport": true,
"Hypervisor": "xen",
"Name": "Attacker created AMI",
"RootDeviceName": "/dev/xvda",
"RootDeviceType": "ebs",
"SriovNetSupport": "simple",
"VirtualizationType": "hvm",
"BootMode": "uefi-preferred",
"ImdsSupport": "v2.0",
"SourceInstanceId": "i-0fa570802e5bec65a",
"ImageAllowed": false,
"SourceImageId": "ami-05b10e08d247fb927",
[SNIP]
Enabling the Declarative Policy in Enabled Mode
When in Enabled Mode, the policy will block any AMIs that do not meet the policy from being used for new instances. In this case, the AWS Console and AWS CLI, will not show any AMIs that do not meet the policy requirements. When using an AMI that does not meet the policy within Terraform, you will receive an image id does not exist error when trying to deploy.
resource "aws_instance" "unauthorized_ami" {
ami = "ami-0010edd796fd9c04d"
instance_type = "t2.micro"
}
$ terraform apply -auto-approve
│ Error: creating EC2 Instance: operation error EC2: RunInstances, https response error StatusCode: 400, RequestID: c0416b31-be2d-4d9b-a502-a5b24243d2fb, api error InvalidAMIID.NotFound: The image id '[ami-0010edd796fd9c04d]' does not exist
│
│ with aws_instance.example,
│ on main.tf line 6, in resource "aws_instance" "example":
│ 6: resource "aws_instance" "example" {
Understanding the Declarative Policy Impact in Enabled Mode
You may be interested in understanding the impact of enabling this Declarative Policy in your AWS account. This section will help to answer some of the questions you may have.
What happens to existing EC2s in an account using an AMI denied by policy?
- No impact. This only impacts newly created EC2s, not existing ones.
What happens if I make a copy of an AMI that is not allowed by policy but was already running in my account before the policy was enforced?
- You can make a copy and it will be allowed because you're now the account owner of the copied AMI.
Discovering AMIs that Violate the Declarative Policy 🕵️♂️
Because the Declarative Policy does not impact existing AMIs even in `enabled` mode, you may want to audit your environment to see which AMIs used violate your new policy.
To do so you could enumerate the Owner IDs of the AMIs in your account which could be automated via the AWS CLI, SDK, or a third-party tool like Datadog's whoAMI-scanner tool.
whoAMI-scanner
We can install the whoAMI-scanner tool via GO.
$ GOBIN=/usr/local/bin/ go install -v https://github.com/DataDog/whoAMI-scanner@latest
Then we can run the tool to scan our account's AMIs.
$ whoAMI-scanner --profile dev --region us-east-1 --verbose
[ 👀 whoAMI-scanner v1.0.0 👀 ] AWS Caller Identity: arn:aws:iam::111111111111:user/dev_user
[*] Verbose mode enabled.
[*] Starting AMI analysis...
[*] [us-east-1] Allowed AMI Accounts status: Audit mode
[1/3][us-east-1] ami-05b10e08d247fb927 being analyzed (Instance: i-0fcc6ee95cac86d93)
[1/3][us-east-1] ami-05b10e08d247fb927 is a community AMI from an AWS verified account.
[2/3][us-east-1] ami-0010edd796fd9c04d being analyzed (Instance: i-017f0fad886bbe247)
[2/3][us-east-1] ami-0010edd796fd9c04d is a AWS marketplace AMI from a verified account.
[3/3][us-east-1] ami-04b4f1a9cf54c11d0 being analyzed (Instance: i-0fed27593d49ac4fc)
[3/3][us-east-1] ami-04b4f1a9cf54c11d0 is a community AMI from an AWS verified account.
Summary Key:
+-------------------------------+-----------------------------------------+
| Term | Definition |
+-------------------------------+-----------------------------------------+
| Self hosted | AMIs from this account |
| Allowed AMIs | AMIs from an allowed account per the AWS Allowed AMIs API |
| Trusted AMIs | AMIs from an trusted account per user input to this tool |
| Verified AMIs | AMIs from Verified Accounts (Verified by Amazon) |
| Shared with me (Private) | AMIs shared privately with this account but NOT from a |
| | verified, trusted or allowed account. If you trust this |
| | account, add it to your Allowed AMIs API or specify it as |
| | trusted in the whoAMI-scanner command line. |
| Public, unverified, but known | AMIs from unverified accounts, but we found the account |
| | ID in fwdcloudsec's known_aws_accounts mapping: |
| | https://github.com/fwdcloudsec/known_aws_accounts. |
| | These are likely safe to use but worth investigating. |
| Public, unverified, & unknown | AMIs from unverified accounts. Be cautious with these |
| | unless they are from accounts you control. If not from |
| | your accounts, look to replace these with AMIs from |
| | verified accounts |
+-------------------------------+-----------------------------------------+
Summary:
AWS's "Allowed AMI" config status by region
Enabled/Audit-mode/Disabled: 0/1/0
Total Instances: 3
Total AMIs: 3
Self hosted AMIs: 0
Allowed AMIs: 0
Trusted AMIs: 0
Verified AMIs: 3
Shared with me (Private) AMIs: 0
Public, unverified, but known: 0
Public, unverified, & unknown AMIs: 0
[!] Looks like you have started to use AWS's "Allowed AMIs" feature.
Only configuring "Allowed AMIs" in "enabled" mode protects you against the whoAMI attack.
Visit https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-allowed-amis.html for more information.
In these results, we can see where the AMIs are coming from which we can use to help craft our Declarative Policy.
Wrap-Up and Resources 🗞️
In this blog post, we've learned about the whoAMI attack discovered by Seth Art and how to prevent it using AWS Declarative Policies. We've also learned to audit AWS accounts' AMI usage with the
whoAMI-scanner
tool.Here are some additional resources to help you learn more about the topics discussed in this blog post:
- whoAMI: A cloud image name confusion attack
- whoAMI-scanner
- Declarative Policies
- Declarative Policy Syntax