
How To Allow AWS Principals To Modify Only Resources They Create
Why would you want to do that anyway?
If you're serious about enforcing least privilege, you've probably run into a situation like this before:
You've written an amazing cloud-native service that you want to deploy to AWS, so you begin working on your infrastructure-as-code and get to the point where you need to write the IAM policy for your service. Your service is going to be deployed to an account that has other services running in it, so you and your InfoSec people want to make absolutely sure that it can't access or interfere with resources that don't belong to it.
In particular, your service needs to be able to create and delete ECS clusters, but you want to make sure that it can't delete clusters that don't belong to it. So you decide on a prefix for your cluster names and grant your service permissions like so:
{
"Effect": "Allow",
"Action": [
"ecs:CreateCluster",
"ecs:DeleteCluster"
],
"Resource": [
"arn:aws:ec2:us-east-1:123412341234:cluster/MyCoolService-*"
]
}
Mission accomplished! Everyone is happy.
Trouble in Paradise
You continue writing out your IAM policy and realize that your service also needs to be able to start and stop EC2 instances. Again, you don't want your service to be able to stop any EC2 instances that don't belong to it.
Unfortunately, you have no control over the ARN for EC2 instances, so you can't use the same trick. But after some digging, you come up with a clever solution that combines attributed-based access control (ABAC) and ec2:CreateAction
:
[{
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": "*"
}, {
"Effect": "Allow",
"Action": "ec2:CreateTags",
"Resource": "arn:aws:ec2:us-east-1:123412341234:instance/*",
"Condition": {
"StringEquals": {
"ec2:CreateAction" : "RunInstances"
}
}
}, {
"Effect": "Allow",
"Action": "ec2:StopInstances",
"Resource": "arn:aws:ec2:us-east-1:123412341234:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/ManagedByMyCoolService" : "true"
}
}
}]
You've done it again! Your service can only stop instances with your special tag, which it can add to new instances, but not to pre-existing instances.
The Hidden Technique 🥷
Cloud Snitch recently gained the ability to restrict account activity via service control policies. When implementing this functionality, we wanted to be absolutely sure that when you grant the required permissions to Cloud Snitch, you can rest assured knowing that it is literally impossible for Cloud Snitch to do anything that would reduce your security posture. That means Cloud Snitch should be able to create service control policies, but not modify, detach, or delete policies that it didn't create for you.
Unfortunately, there's no equivalent to ec2:CreateAction
for service control policies. In fact, the vast majority of resources in AWS don't have an equivalent condition that can be used.
However, if we dig deep, we can unlock a secret jutsu that works for any AWS resource that supports tagging. The key is in the documentation for aws:ResourceTag
:
This key is included in the request context when the requested resource already has attached tags or in requests that create a resource with an attached tag. This key is returned only for resources that support authorization based on tags. There is one context key for each tag key-value pair.
Note that the when the resource already exists, aws:ResourceTag
refers to the tags already on the resource. However, when the resource is being created, aws:ResourceTag
refers to the desired tags for the resource.
This means the above example could also be written like this:
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:StopInstances",
"ec2:CreateTags"
],
"Resource": "arn:aws:ec2:us-east-1:123412341234:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/ManagedByMyCoolService" : "true"
}
}
}
This allows your service to manipulate its own instances, while enforcing least privilege, and without relying on service-specific conditions. It also has the benefit of being concise!
You could even combine multiple services together into a single statement without sacrificing security:
{
"Effect": "Allow",
"Action": [
"ec2:RunInstances",
"ec2:StopInstances",
"ec2:CreateTags",
"ecs:CreateCluster",
"ecs:DeleteCluster",
"ecs:TagResource"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/ManagedByMyCoolService" : "true"
}
}
}
Try it Yourself
Don't take our word for it. Try it out yourself. Configure your AWS CLI with two sets of credentials: one with admin powers and one with the above statement, then...
# Create two clusters, one owned by our cool service and one that is not.
aws --profile admin ecs create-cluster --cluster-name uncool-service-cluster
aws --profile my-cool-service ecs create-cluster --cluster-name my-cool-service-cluster --tags 'key=ManagedByMyCoolService,value=true'
# Try to delete the uncool service cluster (It won't work)
aws --profile my-cool-service ecs delete-cluster --cluster uncool-service-cluster
An error occurred (AccessDeniedException) when calling the DeleteCluster operation: User: arn:aws:iam::********:user/my-cool-service is not authorized to perform: ecs:DeleteCluster on resource: arn:aws:ecs:us-east-1:********:cluster/uncool-service-cluster because no identity-based policy allows the ecs:DeleteCluster action
# But we can delete our own cluster
aws --profile my-cool-service ecs delete-cluster --cluster my-cool-service-cluster
# And we cannot add our tag to the uncool service cluster
aws --profile my-cool-service ecs tag-resource --resource-arn arn:aws:ecs:us-east-1:********:cluster/uncool-service-cluster --tags 'key=ManagedByMyCoolService,value=true'
An error occurred (AccessDeniedException) when calling the TagResource operation: User: arn:aws:iam::********:user/my-cool-service is not authorized to perform: ecs:TagResource on resource: arn:aws:ecs:us-east-1:********:cluster/uncool-service-cluster because no identity-based policy allows the ecs:TagResource action