Reading time: 40 minutes

While AWS does have pretty thorough documentation on IAM policy variables, and the various context keys that can be used as policy variables, after reading through those guides several times something wasn’t clicking for me. These concepts are central to ABAC on AWS. I could get things working, but not with full confidence or in a way that I could explain.

Here are a bunch of example IAM policies that I reference to remind myself how some of the less obvious parts of this works in the wild, and some information about that whats and whys of how they work. (I‘m using YAML in these examples, because it‘s a bit more readable and compact than JSON, even though IAM policies are always JSON.)

This post assumes you mostly understand IAM policy evaluation logic, and won’t get into the weeds on that.


This policy uses a wildcard in the resource. It allows creating buckets named like acme-assets-images, acme-assets-pdfs, etc. It doesn’t use any variables or conditions, but still offers some flexibility and dynamic behavior.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:CreateBucket
    Resource: acme-assets-*

This policy also uses a wildcard in the resource, but adds a policy variable.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:CreateBucket
    Resource: arn:aws:s3:::${aws:PrincipalAccount}-*

The variable is one of the global condition context keys, wrapped in ${…}. (See below for information about service-specific context keys.)

Some context keys are considered single-valued, meaning when they are resolved they resolve to a single value. aws:SourceIp is an example of a single-valued context key; anywhere it is used, it will be substituted with the single IP address value of the current request. You can think of single-valued context keys as having a type of String, Number, etc if we were in a programming language.

Other context keys are multivalued context keys, meaning they (may) represent multiple values when they are resolved. aws:TagKeys is an example of a multivalued context key. We’ll look at aws:TagKeys more closely below, so the specifics don’t matter at this point, it’s just an example of a multivalued context key. You can think of these as having a type of Array in code.

When a context key is plural or is called something like …List, that’s generally a sign that it is multivalued. But you should check the key you are interested in if you’re not sure.

Only single-valued context keys are allowed to be used as policy variables in a policy’s resource. Multivalued context keys cannot be used as policy variables in a policy’s resource.

The policy above allows for creating buckets named like 123456789012-images or 123456789012-db-backups, where 123456789012 is the AWS account ID of the principal performing the create bucket operation (that’s what ${aws:PrincipalAccount} resolves to). So, a principal belonging to account 111122223333 could create 111122223333-images and a principal belonging to 999988887777 could create 999988887777-db-backups with this same policy, since the aws:PrincipalAccount part of the allowed resource name is dynamic.

Other single-valued context keys work the same way. Resource: arn:aws:s3:::${aws:SourceIp}-* would allow a request coming from IP address 12.34.56.78 to create a bucket called 12.34.56.78-images.

Interestingly, and perhaps not obviously, when using properties of the resource as a variable, it seems that in most (all?) cases, those properties are available, even when the resource doesn’t exist, such as when creating a bucket.

For example, this policy allows creating a bucket in account 111122223333 called 111122223333-images, even though that bucket doesn’t exist. You may think ResourceAccount would be missing or null until the resource exists, but that doesn’t seem to be the case. For create actions, the hypothetical future resource’s properties are included as part of the IAM request context, so you can use them.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:CreateBucket
    Resource: arn:aws:s3:::${aws:ResourceAccount}-*

One slightly more complex variation of the previous examples is when using the aws:PrincipalTag/tag-key or aws:ResourceTag/tag-key condition keys in your variables.

For these, the keys themselves change, based on which tag you are trying to reference. They take a form like aws:PrincipalTag/environment or aws:ResourceTag/cost-center, with the key part of a tag coming after the slash.

(A reminder that AWS resource tags consist of two parts: a key, and a value.)

So for example, if you had an IAM role with the following resource tags:

  • environment: prod
  • cost-center: security
  • created-by: Alice
  • aws:cloudformation:stack-name: prod-roles-stack

These would translate to context keys like aws:PrincipalTag/environment or aws:PrincipalTag/cost-center, which could be used as policy variables like ${aws:PrincipalTag/environment} or ${aws:PrincipalTag/cost-center}.

A policy using tag-based context key variables in the resource would look like this:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:CreateBucket
    Resource: arn:aws:s3:::acme-${aws:PrincipalTag/environment}-*

This policy would allow the example role to create buckets like acme-prod-images or acme-prod-db-backups. Because the policy is dynamic, the same policy would allow a different role tagged with environment=stag to create acme-stag-images.


That’s about as complicated as things get when using IAM variables in policy resources.

It’s worth noting that you can use more than one variable in a resource:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:CreateBucket
    Resource: arn:aws:s3:::${aws:PrincipalAccount}-${aws:PrincipalTag/environment}-*

Things get more interesting, more useful, and more confusing once you start using policy conditions, especially conditions with variables.

There’s a few things that contribute to the confusion.

The syntax can feel a bit awkward at times, because the operator doesn’t come between the things you’re comparing.

For example, whereas you may be used to wrting an equality check like this in code:

if (principalAccount == "111122223333")

The equivalent IAM policy would look more like:

Condition:
  StringEquals:
    aws:PrincipalAccount: '111122223333'

You have to remember to mentally move the operator in between the JSON property and value, like aws:PrincipalTag/environment StringEquals "prod".

For me, with equality operators like StringEquals, NumericEquals, I don’t have to work too hard to make sense of the condition, because we’re looking for the two values sitting next to each other to be the same. When using inequality operators, like StringNotEquals or NumericNotEquals, my brain starts to wrinkle a bit. This is when mentally re-ordering things really helps. "aws:PrincipalAccount" StringNotEquals "111122223333" feels a lot more like "aws:PrincipalAccount" != "111122223333" to me than the actual condition format in the policy.

As you use IAM policy conditions more and more, this syntax becomes more familiar and requires fewer mental gymnastics to decipher.

Another point of confusion is once you start having multiples of things. Multiple policy statements with conditions, multiple conditions within a single statement, using some operator multiple times, multivalued context keys, or multiple values for single-valued context keys…

Both how to structure these cases of multiples, and how they interact (logical AND? logical OR?) can be tricky. The following examples will hopefully clarify all these different situations.


Let’s start by looking at a pretty simple policy:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringEquals:
        aws:ResourceTag/environment: stag

This policy’s resource includes a wildcard, but no variables (just to reduce complexity; everything we covered about about variables in resources would still apply). We have now added a condition block to the policy statement. Each IAM policy statement can have a single condition block, which can contain one or more conditions. In this case, we have a single condition in this condition block.

Every condition tests some property of the request context against some value (or values) using an operator.

So in this case, we are comparing the value of the environment tag value of the target SQS queue to the static string value stag, using the StringEquals operator.

Hopefully that syntax structure is starting to make a little bit of sense, where the operator (e.g., StringEquals) precedes the two elements being tested.

This policy would allow principals to send messages to SQS queues with names starting with acme- when that queue is tagged with environment=stag.


It’s important to realize that in a condition like { StringEquals: { aws:PrincipalTag/environment: stag } }, the aws:PrincipalTag/environment part is not a IAM policy variable. In an IAM condition, the left-hand side is always a context key, meaning it’s always representing some dynamic value(s) from the request context. You do not make the left-hand side of the comparison an IAM policy variable.

{ StringEquals: { aws:PrincipalTag/environment: stag } } # Correct
{ StringEquals: { ${aws:PrincipalTag/environment}: stag } } # Incorrect

We can expand the previous example a bit to see how we could introduce more tests to this policy statement (getting into some of those multiples I mentioned).

Let’s say we wanted to compare two different strings as part of this policy. Within a condition block, each operator (StringEquals, NumericLessThan, DateGreaterThanEquals, etc) can only appear once. But we can list multiple context keys under each operator to reuse that operator. So we could augment the previous example like this:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringEquals:
        aws:ResourceTag/environment: stag
        aws:PrincipalTag/team: security # <- This was added

This policy is now performing two tests, both which are making string equality comparisons.

When a condition operator contains multiple context keys, those tests are evaluated using logical AND.

So in this case, the policy is saying: allow sending SQS messages when the queue is tagged with environment=stag AND the principal is tagged with team=security. Both of those conditions must be met for this policy to allow the message to be sent.


Let’s say, instead, that you want to test only the principal’s team tag, but you want allow multiple teams to send these SQS messages. That would look something like this:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringEquals:
        aws:PrincipalTag/team:
          - security
          - devops

This is an example of a condition testing a single-valued context key against a set of values. In this case, the policy would check that the principal is tagged with either team=security OR team=devops.

When a condition has a single-valued context key on the left-hand side, and multiple values on the right-hand side, the evaluation of that condition is done using logial OR.

Or to put it another way, the policy is checking if any of the values in the set are a match with the context key’s value.


To continue to build a complex example, consider:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringEquals:
        aws:ResourceTag/environment: stag
        # AND
        aws:PrincipalTag/team:
          - security
          # OR
          - devops

This policy allows sending SQS messages when the queue is tagged environment=stag AND [the principal is tagged either team=security OR team=devops].


Using multiple condition operators in a policy results in more AND evaluations.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringEquals:
        aws:ResourceTag/environment: stag
      # AND
      DateGreaterThanEquals:
        aws:CurrentTime: '2020-01-01T00:00:00Z'
      # AND
      IpAddress:
        aws:SourceIp: 10.20.0.0/16

This checks that the queue’s environment tag equals stag AND the current time is after 2019 AND the source IP is in that /16 block of IPs.

If we were to add more conditions to any of those operators, or were to provide a list of values for any of the conditions, they would work the same as in the more simple examples above (AND for multiple conditons, OR for a list of values).


Some operators are considered negated matching operators, like StringNotEquals, ArnNotLike, etc. When using these operators with a list of multiple values, the evaluation of those values is made with logical NOR, rather than logical OR.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringNotEquals:
        aws:PrincipalTag/team:
          - hackers
          # NOR
          - interns

This policy will allow a principal to send a message to a queue so long as that principal doesn’t belong to either of the teams listed. The condition is satisfied when the team tag is neither hackers NOR interns.

Or to put it another way, the policy is checking if none of the values in the set are a match with the context key’s value.


All the conditions we’ve written so far have used single-valued context keys. Even in cases where we have tested against a set of values, the context key itself represented a single value (like aws:SourceIp resolving to the single IP of the request, as mentioned earlier).

Let’s now create a condition that uses a multivalued context key.

aws:TagKeys is probably the most common multivalued context key you will see in examples, and that you will end up writing into your policies. We will look at aws:TagKeys shortly, but it has some quirks that make it a bad introduction to multivalued context keys. In fact, most of the multivalued context keys are pretty quirky, so to get started I’m going to invent something.

Imagine that our request context for some request looks like this:

  • aws:SourceIp: "12.34.56.78"
  • aws:PrincipalAccount: "111122223333"
  • aws:username: "alice"
  • fake:Weather: ["Sunny", "Warm", "Windy"]

The fake:Weather context key is our made up example, and you can see that unlike the other elements of the context, it represents a set of values.

We want to write policy conditions that can evaluate these weather conditions in various ways. To do that we add qualifiers to our condition operators, which provide the existing operators with the ability to operate on sets of data in the left-hand side of the comparison (the request context side). There are two such qualifiers: ForAllValues and ForAnyValue.

It’s very important to know that you should never use these qualifiers with single-valued context keys. IAM will allow you to write policies that do that, and it’s common to think you need them when testing sets of values against a singled-valued context key. It’s not necessary, and there are significant potential security issues that can be introduced. Only use set qualifiers with multivalued context keys.

Qualifiers are added to the front of operators. So we can take an operator like StringEquals, and turn it into a set-aware operator like ForAllValues:StringEquals or ForAnyValue:StringEquals.

Here’s an example of a policy that uses ForAnyValue:StringEquals:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      ForAnyValue:StringEquals:
        fake:Weather:
          - Rainy
          - Humid
          - Warm
          - Cloudy

ForAnyValue works by looking for any value in the set of values in the context for the given context key (in this case, Sunny, Warm, and Windy) that satisfies the operator for any value in the set of values we provide in the condition (in this case, Rainy, Humid, Warm, and Cloudy). Because we are using the StringEquals operator in this example, which is looking for exact matches, and since we can find a match in the two sets (Warm), the condition is satisfied.

In other words, this policy will allow the request to send an SQS message when the weather for the request is at least one of Rainy, Humid, Warm, or Cloudy. Because the weather for our example context was Warm, the policy allows the message to be sent.

We can also include wildcards in the set of values we put in our policy, but we have to use the StringLike operator:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      ForAnyValue:StringLike:
        fake:Weather:
          - W*

Here the policy is checking that at least one of the weather values in the context (Sunny, Warm, Windy) starts with a W. Since there is such a value (two, actually: Warm and Windy), this condition is satisfied for our example request.

Note how the set of values listed in the condition can be a single value.

Hopefully you can see how this pattern would apply to other operators when qualified with ForAnyValue.

The ForAllValues qualifier works in a similar way, but checks that every value in the set of values from the context (Sunny, Warm, and Windy) satisfies the operator for some value in the policy condition’s set of values.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      ForAllValues:StringEquals:
        fake:Weather:
          - Sunny
          - Cloudy
          - Warm
          - Cold
          - Windy
          - Calm

This policy would allow the request from our example context, because each of the weather conditions in the context appears in the policy. If another request was made that included fake:Weather: ["Sunny", "Warm", "Windy", "Humid"], this policy would not allow the request, since Humid would not match any of the values in the policy.

It’s pretty easy to think that ForAllValues also means that all the values in the policy condition set must have a match. That’s not the case. The All in ForAllValues is referring only to the set of values in the context (the left-hand side).

Like with ForAnyValue, the policy could include a set that only contains a single value. Just realize that this a very restrictive policy. For example, ForAllValues:StringEquals: { fake:Weather: ["Sunny"] } would require that every weather condition present in the request is Sunny, which basically means that the only weather condition present in the request is Sunny.

Hopefully you can see how this pattern would apply to other operators when paired with the ForAllValues qualifier, including using StringLike with wildcards.


There’s a major caveat to be aware of when usnig the ForAllValues qualifier: if the request context doesn’t include the context key your policy is testing, or if that context key is present but resolves to a null value or an empty string, the condition will always be satisfied, no matter what set of values you include in the policy.

For example, if we again consider this policy:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      ForAllValues:StringEquals:
        fake:Weather:
          - Sunny
          - Cloudy
          - Warm
          - Cold
          - Windy
          - Calm

If that policy is evaluted for something like this context:

  • aws:SourceIp: "12.34.56.78"
  • aws:PrincipalAccount: "111122223333"
  • aws:username: "alice"

Or this context:

  • aws:SourceIp: "12.34.56.78"
  • aws:PrincipalAccount: "111122223333"
  • aws:username: "alice"
  • fake:Weather: ""

Then the policy will allow the SQS message to be sent, even though no weather conditions were actually matched.

In other words, the presence of a ForAllValues condition does not guarantee that the context key for that condition will actually resolve to any useful data.

When this behavior would be problematic, you should pair ForAllValues with the Null condition operator:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      Null:
        fake:Weather: false # This condition is satisfied when fake:Weather IS NOT NULL
      ForAllValues:StringEquals:
        fake:Weather:
          - Sunny
          - Cloudy
          - Warm
          - Cold
          - Windy
          - Calm

Adding the Null operator allows you to ensure that the given context key doesn’t resolve to a null value, so that the overall policy behaves the way you’d expect, even when ForAllValues itself does not.

I find the Null operator to be the hardest of all operators to grok. The Null: { fake:Weather: false } condition is satisfied when fake:Weather is not null – when fake:Weather’s null-ness is false. I just have to memorize it, I don’t think it will ever feel correct to me.

When using the Null operator with ForAllValues, you will generally only ever use false. There are other use cases of the Null operator where you’d use a true value, such as if you wanted a policy to only allow some action if a certain key doesn’t exist in the context (say, checking that aws:SourceIdentity isn’t present, if you don’t want any assumed roles to perform the action.)

You don’t need to use the Null operator with ForAnyValue in this same way. You may still have policies that use both, but it won’t be for dealing with this particular caveat.


Using ForAllValues and ForAnyValue can feel confusing when working with negated operators, so here are a some examples.

We’ll keep using this example context:

  • aws:SourceIp: "12.34.56.78"
  • aws:PrincipalAccount: "111122223333"
  • aws:username: "alice"
  • fake:Weather: ["Sunny", "Warm", "Windy"]

Here’s a policy using ForAnyValue:StringNotEquals:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      ForAnyValue:StringNotEquals:
        fake:Weather:
          - Rainy

This condition will be satisfied if there is any weather data in the request that does not equal Rainy. That’s the case here, since we can find a value in the request context that does not equal Rainy (in fact, we can find three). Additionally, we would still be able to find at least one value in the request context that doesn’t equal Rainy even if Rainy did appear in the request (like ["Sunny", "Warm", "Windy", "Rainy]).

A slightly more complex example would include multiple values in the policy:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      ForAnyValue:StringNotEquals:
        fake:Weather:
          - Rainy
          - Snowy

The evaluation logic, though, is the same. The condition will be satisfied when any weather data in the request equals neither Rainy nor Snowy. For our example, we can find at least one value in Sunny/Warm/Window that is neither Rainy nor Snowy, so the condition is satisfied. Again, even if the request did include Rainy, or Snowy, or both Rainy and Snowy, the condition would be satisfied.

The only time where a ForAnyValue:StringNotEquals condition isn’t satisfied is when the context only includes values from the policy condition set. So for a request with ["Rainy"], or ["Snowy"], or ["Rainy", "Snowy"], or ["Rainy", "Rainy"], or [], in these cases we can’t find any weather data from the request that don’t match, so the condition is not satisfied.

In other words, ForAnyValue:StringNotEquals checks for the presence of at least one value not in the policy. The presence of values in the policy don’t matter.

Now let’s look at ForAllValues:StringNotEquals, and start simple where the policy only has a single value in the condition:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      Null:
        fake:Weather: false
      ForAllValues:StringNotEquals:
        fake:Weather:
          - Sunny

This policy requires that fake:Weather is not null, and requires that no values from the request context match the value in the policy. For our example context (["Sunny", "Warm", "Windy"]), Sunny does appear, so the context does not satisfy this condition. As long as Sunny does not appear, this condition is satisfied. So [], ["Rainy"], ["Warm", "Windy", "Partly Sunny"] all satisfy the condition.

If we add more values to the condition:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      Null:
        fake:Weather: false
      ForAllValues:StringNotEquals:
        fake:Weather:
          - Sunny
          - Partly Sunny

The policy requires that none of the values listed appear in the request context. So our ["Sunny", "Warm", "Windy"] context does not satisfy this condition. Neither does ["Partly Sunny", "Warm", "Windy"]. ["Warm", "Windy"], ["Rainy", "Cloudy"], or any other context that excludes both Sunny and Partly Sunny would satisfy the condition.

In other words, ForAllValues:StringNotEquals check that none of the values in the policy are present in the request context.


So just to look at things all in one place:

  • ForAnyValue:StringEquals: Requires that at least one value from the policy is present in the request. Does not limit any values from appearing in the request. Never requires that multiple values are present in the request.
  • ForAnyValue:StringNotEquals: Requires that the request includes at least one value that isn’t present in the policy. Does not limit any values from appearing in the request.
  • ForAllValues:StringEquals: Limits the values that can appear in the request to only those listed in the policy. Does not require that all values listed in the policy are present in the request.
  • ForAllValues:StringNotEquals: Prohibits the request from containing any values listed in the policy. Does not require any values to appear in the request.

Okay. With all that sorted out, we should have a solid foundation. There’s one big puzzle piece that hasn’t been covered yet, and that’s using policy variables within conditions.

From a conceptual standpoint, this may not seem like a big thing, and it isn’t. We will use variables in conditions the same way we used them in resources at the beginning of the post. And conditions will behave the same with variables as they did with static values.

The reason that variables-in-conditions carries a lot of weight is because it is the cornerstone of ABAC in AWS.

As we’ve seen, conditions allows us to write policies based on aspects of the current request context. Using policy variables in conditions allows us to compare one aspect of the context to another aspect of the same request context. This is really powerful, since it means that both sides of the equation are now dynamic.


Let’s start with something pretty basic:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:SendMessage
    Resource: acme-*
    Condition:
      StringEquals:
        aws:PrincipalOrgID: ${aws:ResourceOrgID}

This policy compares the organization ID of the principal performing an SQS SendMessage operation (i.e., the organization the principal belongs to) with the organization ID of the resource the message is being sent to (i.e., the organization the SQS queue belongs to). When those values are equal (i.e., the principal and the queue belong to the same organization), the action is allowed.

Notice how the left-hand side (aws:PrincipalOrgID) is a bare context key, and the right-hand side is a policy variable (${aws:ResourceOrgID}).

All of the previous examples we’ve looked at for building more and more complex conditions apply when you start to mix in variables. You do have to be careful, though, because not all operators allow variables to be used. It’s pretty rare that you’ll try to write a condition using a variable where it’s not supported, but it’s not impossible. You can look at the operator reference to see which do and do not.


As a quick recap:

  • In a policy’s resource, string literals and IAM policy variables can be used interchangeably, depending on your needs.
  • In a policy’s condition, the left-hand side is always a context condition key (never a literal value or a IAM policy variable).
  • In a policy condition, the right-hand side can be either a literal value(s) (string, number, list, etc) or an IAM policy variable, depending on your needs.

When we talk about implementing ABAC in AWS, generally what we mean is writing policies that use conditions and variables to compare resource tags on the principal and resource in a request context. In AWS, tags are the attributes in ABAC.

For example, let’s say you want a policy that allows principals with the team=security tag to be able to create any S3 buckets they want:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:CreateBucket
    Resource: '*'
    Condition:
      StringEquals:
        aws:PrincipalTag/team: security # Check if the principal making the request is tagged with `team=security`

If you also want to allow buckets to be deleted, but you want to ensure buckets used in production are protected from being deleted:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:DeleteBucket
    Resource: '*'
    Condition:
      StringNotEquals: # Note the "Not" in StringNotEquals
        aws:ResourceTag/environment: prod # Check that the bucket being targeted does **not** have an `environment` tag equal to `prod`

These two examples highlight the differences in using attributes of the principal making the request and attributes of the resources the request is targeting.

The two methods will often be combined. For example, if you wanted a policy that allows a team to delete any of their own buckets:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: s3:DeleteBucket
    Resource: '*'
    Condition:
      StringEquals:
        aws:ResourceTag/team: ${aws:PrincipalTag/team} # Compare the `team` tag on the requesting principal to the `team` tag on the target resource, and allow the deletion if they match

If this is all feeling very familiar, because of how many examples we’ve already looked at, that’s good, and it should.


Quick side note: For requests where both the identity policy and resource policy are being used to determine access (see evaluation logic), remember that both policies are evaluated in the same context. So while there may be context keys that seem more relevant to one or the other – they aren’t. The aws:Principal* properties work in both identity and resource policies, and, for any given request, the value of any given context key will be the same in both policies. Same thing goes for aws:Resource* properties.

So if you find yourself thinking that you need, for example, to use aws:ResourceAccount in a resource policy, and aws:PrincipalAccount in an identiy policy, you may want to revisit how the request context works, and the evaluation logic.


In addition to the aws:PrincipalTag/ and aws:ResourceTag/ context keys, there’s also aws:RequestTag/. Whereas PrincipalTag and ResourceTag are used to refer to resource tags that have already been applied to principals and resources, and are helpful in deciding who can do what, or which things they can operate on, aws:RequestTag/ is used to control the actual process of tagging resources.

Just like the others, it takes a form like aws:RequestTag/team or aws:RequestTag/cost-center.

aws:RequestTag/ is relevant for operations where tags are being changed.

There are some operations where aws:RequestTag/ is not relevant. For example SQS’s SendMessage does not create, update, or delete resource tags. It cannot; that is not a function of that particular API call. For operations like this, aws:RequestTag/ does not provide any real benefit.

It’s important to remember that aws:PrincipalTag/ and aws:ResourceTag/ are hugely useful for these types of operations, though. As we’ve seen in many examples above, we can control how SQS messages get sent using principal and resource tags in our policies.

aws:RequestTag/ gives you access to the tags that are being created, updated, or deleted as part of tagging operations. So while SQS’s SendMessage does not do any tagging, TagQueue and UntagQueue do. Many AWS services have these sort of tagging APIs: SNS’s TagResource and UntagResource, EC2’s CreateTags and DeleteTags, etc.

Some services support tagging but don’t have specific API endpoints for managing the tags. CodeBuild, for example, does not have any sort of explict tag or untag APIs. You manage tags as part of other operations. When you use CreateProject or UpdateProject, you manage the project’s tags as part of those operations.

Whether a service has explicit tagging operations, or they are encompassed within other operations, or a combination of the two, those operations are the ones where aws:RequestTag/ comes into play. The request context for those operation requests will include the context keys for the tags that are being created or updated.

You can look at the Service Authorization Reference document for a service to see which actions will include request tags in the context.


There’s a couple different reasons why it’s important to control how resources are tagged in AWS.

One is consistency. You may have a tagging system and want to ensure that resources don’t deviate from that. Here’s an example of how you may create a policy that only allows certain values to be used for the environment tag on SQS queues and SNS topics.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action:
      - sqs:TagQueue
      - sns:TagResource
    Resource: '*'
    Condition:
      StringEquals:
        aws:RequestTag/environment:
          - test
          - stag
          - prod

Note, however, that this policy does not control who can do the tagging, or which resources they can tag. This policy only controls what that one particular tag can be. It would allow, for example, someone to change the environment tag of some resource from test to prod. You may not want everyone to be able to do that, so you may also add other conditions to restrict tagging, beyond just which values can be used.

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action:
      - sqs:TagQueue
      - sns:TagResource
    Resource: '*'
    Condition:
      StringEquals:
        aws:RequestTag/environment:
          - test
          - stag
          - prod
        aws:PrincipalTag/team:
          - devops

Now our policy only allows members of the DevOps team to make those tagging changes, and even they are still restricted as to which values they can use.

The second, and by far the more important, reason is that once you start using tags as the basis for your access control, controlling the tags themselves becomes critical. This goes beyond consitency for the sake of consistency or something like billing. It determines who has access to private data, who can turn servers on and off, who can make decisions that cost many thousands of dollars.


aws:RequestTag/ is one part of that system for controlling tags. A separate context key called aws:TagKeys is the other.

aws:TagKeys is relevant to tagging operation requests, just like aws:RequestTag/. Unlike aws:RequestTag/, which is a single-valued context key where you reference each tag separately (e.g., aws:RequestTag/team and aws:RequestTag/environment), aws:TagKeys is a multivalued context key.

In the request context for any tagging operation, aws:TagKeys will be a list of just the keys of any tags that were included in the operation. This does not include the principal or resource tags that exist on the principal making the request, or the resource being targetted.

So imagine there’s an IAM role making a request, and that role has been tagged team=security. The role is making an SQS TagQueue request to add a cost-center=manufacturing tag to an existing queue. That queue has already been given a resource tag of environment=prod.

In this case, the request context would include the aws:TagKeys context key, which would have a value of ["cost-center"]. The team and environment tag on the principal and resource are not included in aws:TagKeys (though they would be available using aws:PrincipalTag/team and aws:ResourceTag/environment as always).

The entire context may look like this:

  • aws:SourceIp: "12.34.56.78"
  • aws:PrincipalAccount: "111122223333"
  • aws:PrincipalTag/team: "security"
  • aws:ResourceTag/environment: "prod"
  • aws:RequestTag/cost-center: manufacturing
  • aws:TagKeys: ["cost-center"]

As a multivalued context key, we can use the ForAnyValue and ForAllValues operation qualifiers to comprehensively control which tags can be added to resources with aws:TagKeys.

All the various ways that those qualifiers allow us to require values, restrict values, prevent certain values, etc, we can use to control which tags are added to resources.


It’s important to remember that aws:TagKeys and aws:RequestKey/ come into play only during tagging operations. They do not necessarily ensure that resources are tagged or tagged in any particular way, even if those resources are being manipulated in other ways.

For example, imagine you have an existing SQS queue that doesn’t have any tags, and you create a policy that requires queues to be tagged with a team and environment tag, perhaps with some limited set of values for each, for the sqs:TagQueue and sqs:UntagQueue actions. If a user comes along and calls SetQueueAttributes, that action is not governed by the tagging policy (nor could it be, since it’s not a tagging operation), so the attributes will be updated but queue will continue to be untagged.


Similar to how we can add ForAnyValue: and ForAllValues: to condition operators to create set-aware operators, we also have the option of adding …IfExists to operators, which makes the condition apply only if the given context key is present in the request.

Take this example:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: ec2:*
    Resource: '*'
    Condition:
      StringEqualsIfExists:
        ec2:InstanceType:
          - t4g.small
          - t4g.medium

This policy is ensuring that only two particular types of EC2 instances can be used. If it had used the standard StringEquals condition, most EC2 operations would be blocked by this policy, since most operations don’t include the ec2:InstanceType context key. For example, if someone tried to call ec2:CreateSubnet, that request wouldn’t include any InstanceType information, since it’s not an instance-based operation. We aren’t actually trying to prevent that request, and by using StringEqualsIfExists we can have this condition ignored, until it’s being used for an instance-based operation.

By creating the policy this way, we aren’t responsible for curating a list of all instance-based operations in the policy’s Action. If, over time, new actions are added that are considered instance-based, and IAM includes the ec2:InstanceType context key in those requests, the policy will kick in and continue to control which instance types are allowed.

Note that Null does not have an …IfExists variant.


The previous example illustrates a service specific context key (ec2:InstanceType). The other examples we’ve looked at so far used global context keys, which are available in all requests contexts, regardless of which AWS service is handling the operation.

Many services, though, can add context keys that are only relevant within that service, such as EC2’s intance type. You can look at the service’s service authorization documentation (e.g., EC2) to see if there are any service-specific keys.

You will probably notice that many of these documents will list some (but not all) global keys. For instance, EC2 lists aws:TagKeys explicitly, even though it’s a global condition key. I don’t have a good explanation for why they do that.

You’ll also come across some duplication between global and service-specific keys. For example ec2:ResourceTag/${TagKey} and aws:ResourceTag/${TagKey}. I believe these service-specific keys are maintained for backwards compatibility, and the values should be identical in cases where both are available. You may find reasons to reference the service-specific keys, though, even when a global key is available (like if you wanted to write a policy that applied to all services, but only dealt with tagging for EC2 resources).


You may be thinking that combining ForAnyValue and …IfExists would let you do some useful things.

Consider this policy:

Version: '2012-10-17'
Statement:
  - Effect: Allow
    Action: sqs:*
    Resource: '*'
    Condition:
      ForAnyValue:StringEqualsIfExists:
        aws:TagKeys:
          - team

On paper, it would appear that this policy is ensuring that the team tag is present on tagging operation requests (those where the aws:TagKeys key is present), and wouldn’t affect other, non-tagging operations (like sqs:SendMessage).

Unfortunately, this policy does not work that way. IAM resolves the ForAnyValue part of the condition before …IfExists, and ForAnyValue conditions can’t be satisfied when the given context key is missing or null.

So in the case of an sqs:SendMessage operation, the request context will never have aws:TagKeys, which means ForAnyValue:StringEqualsIfExists: aws:TagKeys is always false.

In other words, ForAnyValue:StringEqualsIfExists is treated the same as ForAnyValue:StringEquals, and you should avoid using …IfExists with other qualifiers to prevent confusion.


So now we have two tools to help control tagging. We have aws:TagKeys, which controls which tags can be used, and we have aws:RequestTag/ which controls which values can be used for those tags.

The building blocks of wildcards, variables, and conditions offer a lot of flexibility when it comes to writing powerful authorization policies in AWS, but there are gotchas and rough edges, so you do have to care when venturing into more complicated territory.

Relevant AWS documents