AWS Tagging Gotcha

Recently while working on some AWS policies, I discovered a tricky gotcha in how the DeleteTags action works with conditioning. It took me a bit of head-scratching to figure out what was going on, so I wanted to share what I learned.

Let’s say we have an AWS policy with one statement:

"Statement": [
    {
        "Sid": "DeleteTagFoo",
        "Effect": "Allow",
        "Action": [
            "ec2:DeleteTags"
        ],
        "Resource": [
            "arn:aws:ec2:*:*:subnet/*"
        ],
        "Condition": {
            "ForAllValues:StringLike": {
                "aws:TagKeys": [
                    "foo"
                ]
            }
        }
    }
]

The effect here should be that deleting a tag with the key “foo”, and only that tag, should be allowed on any subnet.

And the following works as expected:

$ aws ec2 delete-tags --resource-id $SUBNET_ID --tags Key=foo
# This succeeds as expected, and the tag is removed from the subnet

$ aws ec2 delete-tags --resource-id $SUBNET_ID --tags Key=bar
# This fails as expected with an error of
`$ROLE is not authorized to perform: ec2:DeleteTags on resource $RESOURCE_ID because no identity-based policy allows the
ec2:DeleteTags action`

Everything as expected so far. But what if you don’t specify a specific tag to delete?

$ aws ec2 delete-tags --resource-id $SUBNET_ID

This works, and uses our policy to delete every tag on the subnet, which is not what we want! When no tags are specified, ForAllValues evaluates to true because there are no values to check.

After digging through the AWS docs, I found that this behavior seems to be expected, and they have an example where they recommend adding an additional condition to prevent passing a request with no tags specified. To fix this in our policy, we need to update our condition to include a null check. In our example, the condition then becomes

        "Condition": {
            "ForAllValues:StringLike": {
                "aws:TagKeys": [
                    "foo"
                ]
            },
            "Null": {
                "aws:TagKeys": "false"
            }
        }

With this additional condition in place, $ aws ec2 delete-tags --resource-id $SUBNET_ID now fails as expected, protecting all tags on the subnet from being accidentally deleted in one go.

You’d expect that restricting deletions to specific tag keys would cover all cases, but AWS treats an empty DeleteTags request differently and will happily remove everything. When using ForAllValues to restrict tag operations, always pair it with a null check. Otherwise, your “restrictive” policy has a gaping hole: an empty request bypasses all restrictions.