AWS Condition Context Keys for Reducing Risk

A Least Privilege cheat sheet on using AWS global condition context keys to achieve least privilege.

Lior Zatlavi By Lior Zatlavi
AWS Condition Context Keys for Reducing Risk

A Least Privilege Cheat Sheet

I recently noticed a tweet by Cloud Security expert, Scott Piper, regarding the perceived mess around global condition keys like: aws:PrincipalIsAWSService, aws:ViaAWSService and aws:CalledVia:

Scott’s tweet is, in fact, a retweet citing a blog post by AWS’s Ilya Epshteyn and Harsha Sharma that explains how and when to use AWS condition context keys to better secure the access to AWS resources. After a close read of the referenced post and others on the topic, like this one, and a review of AWS’s documentation, we decided to create an easy reference for using these keys.

As strong advocates of least privilege, we believe these AWS keys can be quite effective if they could be easily understood. And we believe it's possible to understand them if explained in a straightforward manner. We hope this short article achieves that.

Let’s begin. To make things easier, we’ll look at the aws:CalledVia* and aws:ViaAWSService keys separately from aws:PrincipalIsAWSService - as they are designed to perform a very different validation. We will address the two different purposes, specify each key and its relevant use cases, and provide an example of how to use it in a condition. Note that we have intentionally oversimplified the examples for ease of understanding.

Make Sure Access is Done By AWS Service Directly

Let’s kick things off with the new key in town. The aws:PrincipalIsAWSService key, which was recently announced is meant to allow you to create a simple data perimeter around certain areas of your cloud environment which only allows access directly for service principals which are AWS services. The most common example of such access can be CloudTrail writing to Amazon S3 buckets (with the service principal “cloudtrail.amazonaws.com”).

The key aws:PrincipalIsAWSService indicates when this is actually the case or not, and allows limiting the use of a resource to only direct use by AWS services; for example, by applying it on a resource-based policy.

So, for example - if you have an S3 bucket you want to be made accessible to AWS Services (e.g. - CloudTrail) only you can use a “Deny” on anything which does NOT have true flagged on this context key (as shown in the example below).

Key(s) to Use: aws:PrincipalIsAWSService
Condition Example:

{
    "Effect":"Deny",
    "Condition":{
        "Bool":{
            "aws:PrincipalIsAWSService":"false"
        }
    }
} 

Limit Principals from Independently Accessing Services Required By Other Services

aws:PrincipalIsAWSService wasn’t released in a vacuum, and while it’s pretty easy to understand on its own - the confusion usually comes from the differences between it and some existing keys - more specifically - the aws:CalledVia* and aws:ViaAWSService keys.

The purpose of the aws:CalledVia* and aws:ViaAWSService keys is to make sure that access to certain resources can be done only by an AWS service using the identity of a principal which triggered an action on it – or, better yet, by a specific AWS service for which this is the case. This is opposed to the principal calling it independently.

When a principal such as an IAM user or role needs to use AWS services which themselves have to make subsequent calls to other AWS services, access to the latter must also be given to the principal as the subsequent calls are done using the principal’s credentials.

For a simple example, consider a call to reading a piece of data in a DynamoDB table, encrypted with a KMS key. In order to decrypt the data, DynamoDB will have to call kms:Decrypt - and this will be done using the credentials of the principal that asked for the data to be read from the DynamoDB table. Therefore - that principal will have to have permissions to the DynamoDB table AND to kms:Decrypt on the KMS key which has to be used to decrypt it.

This could get more elaborate, such as in an example AWS provide in their documentation - using CloudFormation to read and write from a DynamoDB table, and DynamoDB then uses encryption supplied by KMS. CloudFormation and DynamoDB will use the credentials of the original principal to make subsequent calls so the principal needs access to DynamoDB and KMS, respectively. Here is a short description of what each of the keys in this use case is meant to do:

aws:CalledVia - Will hold a list with the service principal values that made subsequent calls on behalf of the IAM user, in the order in which they were made
aws:CalledViaFirst - Will hold the service principal value of the first AWS service that started the subsequent calls
aws:CalledViaLast - Will hold the service principal value of the most recent AWS service via which a call was made
aws:ViaAWSService - Is a boolean that indicates if the call was made by an AWS service using the IAM user’s credentials (opposed to directly by the service principal - see the next section)

In the scenarios below, our goal is to make sure that a call that needs to be made by an AWS service using a principal’s credentials is made by this mechanism and NOT by the principal independently. The main difference between them is the level of granularity in which we can define via which service the access is to be made, in an increasing order of precision.

Scenario #1 - Validate that the call was made via AWS service

If you know a call needs to be made via an AWS service but are not sure which, you can use this key, which determines if the call was made via an AWS service regardless of its service principal. This is of course the least precise key you can use - however, it can still be useful in that it helps fight most of the battle: guaranteeing that a principal won’t be able to use a service that needs to be called via an AWS service independently.

Using this key could be done when you want to configure a policy to prevent all potential principals from accessing a certain resource - yet still allow calls made via AWS services, as shown here. This is very similar to the use we saw before of aws:PrincipalIsAWSService, except the permissions granted here would be for calls made via AWS services not directly by their principals.

The two could be combined in order to provide a general permission for actions done in one of the two methods while limiting access to principals independently. When, for example, you want to set up a network perimeter outside of which only AWS Services could perform actions on a resource - using both context keys to exclude calls from AWS Services makes sense. However - when you want to set up an identity based perimeter you should not exclude calls with the aws:ViaAWSService context key set to true from the Deny, as by doing so you will prevent it from limiting actions which may originate from calls made by any identity - which defeats the purpose of the perimeter.

Key(s) to Use: aws:ViaAWSService
Condition Example:

{
    "Effect":"Deny",
    "Condition":{
        "Bool":{
            "aws:ViaAWSService":"false"
        }
    }
}

Scenario #2 - Validate that a certain service is included in the call chain

As described before, subsequent calls made by AWS services with the credentials of the principal making the initial call which triggers are held in an array which is the value of the aws:CalledVia key.

If you know of a specific AWS service via which a call is made during ANY of the subsequent calls and can name its service principal, you can make the restriction even more granular by specifying it. Using aws:CalledVia lets you make sure that, when a specific AWS service is one of the callers on the chain, the access is being carried out by an AWS service.

If there’s a resource which is only to be used by a specific service - e.g. a KMS key which is meant to serve CloudFormation - regardless of what other services are part of the process, you can specify the condition as including the CloudFormation service principal as part of the aws:CalledVia array.

Key(s) to Use: aws:CalledVia
Condition Example:

"ForAnyValue:StringEquals": {
     "aws:CalledVia": ["cloudformation.amazonaws.com"]
}

Note that should you want to achieve an even higher level of granularity - and if you know the first and/or last service making the call and another service that has to be involved in the process - you can use this call along with a condition on aws:CalledViaFirst and/or aws:CalledViaLast (discussed next).

Scenario #3 - Validate the chain of service principals making the calls

Getting more granular, you can validate the specific chain of service principals making calls using the principal’s credentials. This will ensure that calls are only made via the appropriate services AND as part of the proper sequence they belong to. It requires a very intimate understanding of the mechanisms in which the calls are made and can be very restrictive - but is, of course, the most secure way to go.

Using the keys aws:CalledViaFirst and aws:CalledViaLast you can check that a call to a service is done as part of a specific chain of calls. What these keys do is check the service principals of the first and last calls of a specific chain of calls.

Let’s demonstrate how this could be helpful using the previous example where CloudFormation calls DynamoDB which calls KMS. On the call between CloudFormation to DynamoDB, both aws:CalledViaFirst and aws:CalledViaLast keys will be cloudformation.amazonaws.com. On the call from DynamoDB to KMS, aws:CalledViaFirst will be cloudformation.amazonaws.com (as it’s the first service which made the call on the principal’s behalf) and aws:CalledViaLast will be dynamodb.amazonaws.com. If you are certain this process is the only use case in which the permissions would be put into use, you can validate these values on the permissions granted to DynamoDB and KMS and thus check in a more precise way that calls to them are not only made by the appropriate services - but also at the correct order as part of the process they are meant to serve.

Key(s) to Use: aws:CalledViaFirst and / or aws:CalledViaLast (respectively)
Condition Example:

"StringEquals": {
     "aws:CalledViaFirst": "cloudformation.amazonaws.com",
     "aws:CalledViaLast": "dynamodb.amazonaws.com"
}

Note that you don’t have to use both if you only have definite knowledge of one end of the process (that is, the origin of the call chain or the most recent service which makes the call). The more precise you are, the closer you are to achieving least privilege.

Bonus Scenario - Validating The Source of Calls to KMS Operations

While the global condition context keys aws:CalledVia are relatively new, KMS has a condition key specific to it - kms:ViaService which serves a very similar purpose. Using it you can limit the access granted to operations on KMS keys to only be allowed from specific services, by providing a list of one or several service principals which are allowed to perform the operation.

It’s important to know this key since the aws:CalledVia* keys currently can only be used with four services:

  • Amazon Athena
  • AWS CloudFormation
  • Amazon DynamoDB
  • AWS Key Management Service (AWS KMS)

This is quite a limited inventory compared to the list of services kms:ViaService supports.

So, for example, if you have an S3 bucket which uses a KMS key for encryption and you want to validate that access to this KMS key is only done when called via the S3 service - you can do so using the kms:ViaService - while you couldn’t do so using the aws:CalledVia key, which currently doesn’t support S3.

Make sure you notice the service principal in the case of kms:ViaService also requires the specification of the region from which the call is made.

Key(s) to Use: kms:ViaService
Condition Example:

"StringEquals": {
      "kms:ViaService": [
        "ec2.us-west-2.amazonaws.com",
        "s3.us-west-2.amazonaws.com"
      ]
}

Moving Forward

We hope you found this post useful to understanding these AWS global condition context keys and how you can use them to achieve least privilege. We highly recommend that you visit the posts referenced earlier and use this acquired knowledge to have an easier time understanding how they work. You can also try and apply these keys in your policies.

If you have any questions about how to set up a permissions perimeter to your resources, feel free to reach out.