Article

EC2 Policies: security, freedom, and both

Introduction

Amazon's EC2 service is one of many solutions that have enabled us, like many organisations, to readily deploy scalable and on-demand computing infrastructure. It has allowed us to host a variety of corporate resources and, at the same time, has granted us the freedom to arbitrarily deploy infrastructure as and when we need it. However, this freedom can come at the cost of security, just as security can often come at the cost of freedom.

One of the challenges we faced while trying to configure EC2 permissions was working within the constraints of these two ideals. It turns out that granting users the ability to deploy and manage their own infrastructure is not that easy – unless you grant them these same permissions to everything else as well. This, however, would mean that the user who needs to spin up infrastructure to tinker with the next latest and greatest 'something' would only be able to do so if they were granted access to the production instance of the corporate web server as well.

Last I checked, there were over 250 permissions for EC2 alone. With all of these permissions, it was hard to believe that AWS hadn't already accounted for this scenario. I just want to caveat this post right now by saying that they absolutely have and that all of the information covered in this post already exists within AWS documentation. However, given how long it took me to find, and how often I came across others facing the same problem, I thought this post could be beneficial to others.

At the cost of freedom 

If the requirement for others to deploy their own infrastructure is ignored, then securing access to EC2 resources is well documented and relatively straightforward to implement. AWS allows you to classify resources by marking them with one or more tags, which can be used to granularly manage permissions. If a resource is unlikely to change, an obvious scheme would be one where an admin user simply deployed infrastructure on a user's behalf and granted them the permissions required to manage it. For example, a policy statement for permitting a specific set of actions on resources tagged with WEBSERVER could look like this:

        {
"Sid": "ManageWEBSERVERTaggedInstances",
"Effect": "Allow",
"Action": [
"ec2:RebootInstances",
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:TerminateInstances"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/Group": "WEBSERVER"
}
}
},

This is similar to traditional, non-cloud infrastructure management, in which hardware is first procured, delivered, and configured by dedicated personnel before it is made available. However, by doing this we lose many of the obvious benefits cloud computing provides, as users can no longer readily deploy their own infrastructure. 

At the cost of security

Conversely, if access is managed using tags and users are granted the ability to deploy their own infrastructure, security can suffer. If a user isn't permitted to tag resources themselves, they won't be able to tag any of the EC2 instances they've created and won't be able to do anything without the help of an admin. If, however, users are allowed to add tags themselves, then there would be little to prevent them from tagging resources they should not have any control over. A user governed by the WEBSERVER policy example could gain control over a database server by just tagging it with the WEBSERVER tag. This would allow them to perform various actions on the server, or even terminate it altogether. 

At the cost of neither 

One solution I frequently encountered suggested using an AWS Lambda in conjunction with a CloudWatch Events rule to automatically tag newly created instances. Essentially, this solution would automate the behaviour of the admin user discussed previously by tagging instances with the names of the users that had created them.

This solution, however, seemed far from ideal. For one, the idea of managing permissions programmatically seemed cumbersome. In addition, the solution, by design had limitations: all rules would have to rely on information present within instance creation metadata, and, as such, would be largely inflexible with the values tags could assume.

Delving deeper into AWS policy permissions reveals some interesting information that is perhaps not immediately apparent when modifying policy information using the visual editor; in particular, resource and resource condition nuances –  two of the fields present in policy statements. The Resource field can be used to granularly configure resource-level permissions by specifying what resources actions apply to.

The Conditions field offers more granularity still and can be used to define when an action is applicable. In the previous example policy statement, the ec2:ResourceTag condition key ensured that various operations would only be permitted against instances assigned the WEBSERVER tag. This condition key, like all other condition keys, is applicable to only specific actions and resources. Using a condition key that is not supported by all resources and conditions will produce unexpected results. In these cases, policy statements can be subdivided and each conflicting action, condition, and resource, defined in its own policy statement.

One of the less obvious features offered by resource conditions is their ability to provide context to specific, supported actions, the most notable of which in our case is ec2:CreateTags – the action that is performed when tagging instances. If the ec2:CreateAction condition key is applied to an ec2:CreateTags action, it is possible to enforce that users are only permitted to tag instances when they are created or, more specifically, run. An example of such a policy statement follows:

        {
"Sid": "CreateTagsInRunInstancesContextOnly",
"Effect": "Allow",
"Action": "ec2:CreateTags",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:CreateAction": "RunInstances"
}
}
},

This policy statement will ensure that users cannot tag instances that have already been created (i.e. the database server discussed previously); however, in isolation, it will not restrict the value of the tag that can be used, nor will it enforce that a tag is used at all. In order to enforce that users can only create instances using a specific tag, an additional policy statement is required:

        {
"Sid": "RunInstancesMandateWEBSERVERTagging",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/Group": "WEBSERVER"
}
}
},

This policy statement will mandate that all created instances are tagged with the key-value pair Group=WEBSERVER by using the condition key aws:RequestTag. This condition key, however, is only applicable to specific EC2 resources and, as such, will fail in cases where the possibility for an incompatible resource exists. In this case, resource-level permissions can be applied to explicitly refer to an instance and additional resources defined in separate policy statements:

            {
"Sid": "RunInstancesExplicitAllow",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
                "arn:aws:ec2:*:*:subnet/*",
                "arn:aws:ec2:*:*:launch-template/*",
                "arn:aws:ec2:*:*:key-pair/*",
                "arn:aws:ec2:*::snapshot/*",
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*:*:placement-group/*",
                "arn:aws:ec2:*:*:network-interface/*",
               "arn:aws:ec2:*::image/*"
]
},

As far as I'm aware, the resources specified in the above two policy statements account for all basic resources required to launch an EC2 instance from either Amazon's Web Console or the CLI using default options. This policy statement can, however, be restricted further or amended to include additional required resources.

Piecing it all together

If we consolidate all previously discussed policy statements into a single policy, we can effectively grant users the ability to deploy and manage their own infrastructure. Moreover, we can arbitrarily define what infrastructure specific users can deploy (for example, a production web server, or red-team infrastructure). For collaborative efforts, this would enable one user to deploy infrastructure that can immediately be managed by all other users affected by the same policy, while simultaneously ensuring that this infrastructure cannot be accessed by unauthorised personal – assuming that this same scheme is followed consistently.

A complete policy example, that we have begun to adopt for engagements that require red-team infrastructure, follows below. This policy incorporates all previously discussed policy statements, as well as two additional policy statements that grant EC2 read access and explicitly deny tag deletion. As a Deny will take precedence over an Allow, incorporating the  EC2DeleteTagsExplicitDeny policy statement will mitigate the risk of an additional erroneous policy, which grants tag deletion, undermining the security this policy offers.  Since ec2:TerminateInstances will delete all tags implicitly, this action is not required. 

{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateTagsInRunInstancesContextOnly",
"Effect": "Allow",
"Action": "ec2:CreateTags",
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:CreateAction": "RunInstances"
}
}
},
{
"Sid": "RunInstancesMandateREDTagging",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/RED": "CODENAME"
}
}
},
{
"Sid": "RunInstancesExplicitAllow",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
                "arn:aws:ec2:*:*:subnet/*",
                "arn:aws:ec2:*:*:launch-template/*",
                "arn:aws:ec2:*:*:key-pair/*",
                "arn:aws:ec2:*::snapshot/*",
                "arn:aws:ec2:*:*:volume/*",
                "arn:aws:ec2:*:*:security-group/*",
                "arn:aws:ec2:*:*:placement-group/*",
                "arn:aws:ec2:*:*:network-interface/*",
                "arn:aws:ec2:*::image/*"
]
},
{
"Sid": "EC2ReadAccess",
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"ec2:GetLaunchTemplateData"
],
"Resource": "*"
},
        {
            "Sid": "EC2DeleteTagsExplicitDeny",
            "Effect": "Deny",
            "Action": [
                "ec2:DeleteTags"
            ],
            "Resource": "*"
        },
{
"Sid": "ManageREDTaggedInstances",
"Effect": "Allow",
"Action": [
"ec2:RebootInstances",
"ec2:TerminateInstances",
"ec2:StartInstances",
"ec2:StopInstances"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"ec2:ResourceTag/RED": "CODENAME"
}
}
}
]
}

This policy can be tested by using the AWS Management Console or by making use of the AWS CLI:

aws ec2 run-instances --image-id ami-976152f2 --count 1 --instance-type t2.micro --tag-specifications 'ResourceType=instance,Tags=[{Key=RED,Value=CODENAME}]'