Protect your AWS access keys by using Permission Boundaries

AWS Users and Roles can have their permissions limited by a Permission Boundary. The effective permissions are at the intersection of the user or role policy and the permission boundary (which is also defined as an IAM policy).

Permission Boundary in AWS

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Action": ["s3:*", "athena:*"],
           "Effect": "Allow",
           "Resource": "*"
       }
   ]
}

While we’re not fans of allowing Resource “*”, this is just an example to demonstrate Permission Boundaries, and we don’t want to make it unnecessarily complicated.

The policy could be attached to any user or role. It could be used by a data analyst, but it could also be used by an application that needs to have access to S3 and Athena. The credentials can be used within an EC2 instance (using a IAM instance role), or can be issued by an external identity provider. Both approaches end up with the same credentials somewhere accessible on an EC2 instance (through the metadata URL), or within the ~/.aws directory on an analysts’ machine.

Even though these credentials have a limited lifetime, they can still be leaked, copied, or stolen in numerous ways. What we want to achieve is to reduce the impact when credentials are used outside our network. We can do this by attaching a Permission Boundary policy to our users and roles.

A new Permission Boundary policy to reduce our attack surface could look like this:

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Action": "*",
           "Effect": "Allow",
           "Resource": "*",
           "Condition": {
             "ForAnyValue:StringEquals": {
               "aws:SourceVpc": ["vpc-abcd1234"]
             }
           }
       }
   ]
}

This permission boundary could also be applied to policies that use other services than s3 and athena. Permission Boundaries are typically written more generally, so that they can be re-used with different user and role policies. For this policy to work, we need to have the S3 Gateway endpoint enabled within our VPC. Traffic will then go directly from the VPC to S3, and we’ll be able to build IAM policies with aws:sourceVpc in our condition.

You might be in a situation where users go through a non-AWS network. To cover this use-case, your IAM policy would need to look like this:

{
   "Version": "2012-10-17",
   "Statement": [
       {
           "Action": "*",
           "Effect": "Allow",
           "Resource": "*",
           "Condition": {
             "IpAddress": {
               "aws:SourceIp": ["1.2.3.4/16"]
             }
           }
       }
   ]
}

You can also have both statements in 1 policy, to allow VPC access, and also through a public IP address.

API calls to S3 will work out of the box, but not Athena. Athena is unfortunately not a service within our VPC, so calls to S3 using these IAM credentials will fail. To make this work, we need to add one more statement. We need to include the aws:CalledVia condition to make sure calls from athena succeed, because they will not match our aws:SourceVpc. Below could be our final Permission Boundary policy:

   "Version": "2012-10-17",
   "Statement": [
       {
           "Action": "*",
           "Effect": "Allow",
           "Resource": "*",
           "Condition": {
             "ForAnyValue:StringEquals": {
               "aws:SourceVpc": ["vpc-abcd1234"]
             }
           }
       },
       {
           "Action": "*",
           "Effect": "Allow",
           "Resource": "*",
           "Condition": {
             "IpAddress": {
               "aws:SourceIp": ["1.2.3.4/16"]
             }
           }
       }
       {
           "Action": "*",
           "Effect": "Allow",
           "Resource": "*",
           "Condition": {
             "ForAnyValue:StringEquals": {
               "aws:CalledVia": ["athena.amazonaws.com"]
             }
           }
       }
   ]
}

Also a fun fact. AWS released aws:CalledVia only in February 2020. Before that we used aws:SourceIp athena.amazonaws.com instead to make this work. At some point when we wanted to update our IAM policies, we got an error message saying that athena.amazonaws.com is not a valid IP address. Our work-around stopped working, which was not a surprise, as it was pretty much undocumented (we used CloudTrail to figure out the filters). Luckily AWS released a proper way to implement a condition for this use case instead of our work-around. We now started using aws:CalledVia.

Edward Viaene
Published on July 15, 2020