How to Help Lock Down a User’s Amazon EC2 Capabilities to a Single VPC

As a cloud support engineer, I am frequently asked this question: “How can I lock down my user’s Amazon EC2 access to a single VPC?” This blog post will answer the question and explain how you can help control this level of access through the use of AWS Identity and Access Management (IAM) policies and IAM roles.

For EC2, you must consider two questions when locking down a user’s EC2 access to a single virtual private cloud (VPC):

  • Which API actions support Amazon Resource Names (ARNs) and conditions?
  • Which API actions do not support ARNs and conditions?

An API action with resource-level permissions is one that has support for an ARN in the Resource element of an IAM policy. We have covered the basics of EC2 resource-level permissions in Demystifying EC2 Resource-Level Permissions. The Supported Resource-Level Permissions for Amazon EC2 API Actions documentation page shows a full list of EC2 actions that support ARNs. This list of EC2 actions also shows which conditions can apply to which ARNs.

Inversely, the Unsupported Resource-Level Permissions documentation page shows a full list of EC2 API actions that only support a wildcard ( “*” ) for the resource. For example, you can see that ec2:DescribeInstances and ec2:CreateTags are not actions that can be limited in any way. You must declare them with a resource of “*” for them to function within your policy. You can also see that ec2:RunInstances has multiple ARNs that it supports, and each has its own set of conditions that apply to that ARN.

Before I explain how to lock down a user to a single VPC, I want to address the baseline permissions that are needed. First, users need to be able to see the instances and their attributes, so they must have ec2:Describe*. Our policy would start out as you see in the following code.

{
     "Version" : "2012-10-17",
     "Statement" : [{
               "Sid" : "NonResourceBasedPermissions",
               "Action" : "ec2:Describe*”,
               "Effect" : "Allow",
               "Resource" : "*"
          },

To reiterate, you’ll notice here that I am allowing all ec2:Describe* actions acess to all resources. This is because these APIs do not support a resource other than “*”.

Next, you want to add the user’s ability to control the instances. If you review Supported Resource-Level Permissions for Amazon EC2 API Actions, you will see the following actions that can reference the Instance ARN and allow the user to manage the lifecycle of the instance:

  • ec2:AttachVolume
  • ec2:DetachVolume
  • ec2:RebootInstances
  • ec2:RunInstances
  • ec2:StartInstances
  • ec2:StopInstances
  • ec2:TerminateInstances

Adding these actions to the policy gives you the following starting policy. Note that the API call ec2:RunInstances has multiple ARNs that must be addressed to properly launch instances. You will be adding Conditions later, which is why you have 2 statement blocks for the ec2:RunInstances action. Be sure to replace placeholder account numbers with your actual account numbers.

{    
"Sid" : "AllowInstanceActions",
      "Effect" : "Allow",
      "Action" : [
            "ec2:RebootInstances",
            "ec2:StopInstances",
            "ec2:TerminateInstances",
            "ec2:StartInstances",
            "ec2:AttachVolume",
            "ec2:DetachVolume"
      ],
      "Resource" : "arn:aws:ec2:us-east-1:123456789012:instance/*"
},
{
      "Sid" : "EC2RunInstances",
      "Effect" : "Allow",
      "Action" :"ec2:RunInstances",
      "Resource" : "arn:aws:ec2:us-east-1:123456789012:instance/*"
},
{
      "Sid" : "RemainingRunInstancePermissions",
      "Effect" : "Allow",
      "Action" : "ec2:RunInstances",
      "Resource" : [
            "arn:aws:ec2:us-east-1:123456789012:volume/*",
            "arn:aws:ec2:us-east-1::image/*",
            "arn:aws:ec2:us-east-1::snapshot/*",
            "arn:aws:ec2:us-east-1:123456789012:network-interface/*",
            "arn:aws:ec2:us-east-1:123456789012:key-pair/*",
            "arn:aws:ec2:us-east-1:123456789012:security-group/*",
            "arn:aws:ec2:us-east-1:123456789012:subnet/*"
      ]
}

Now that you have a starting policy, you can start to lock it down. Looking at the Control actions and the available conditions, you will see that a few are common across the board. I will use ec2:InstanceProfile for all of these actions, because it is in common with the actions in the AllowInstanceActions block and the EC2RunInstances block. These ensure that the EC2 instance must be launched with an IAM role attached to it, and any instance that has this role attached will be controllable via the permissions outlined in the AllowInstanceActions block.

You might ask why you can’t use the ec2:ResourceTag condition key. You don’t want to use this condition key because if you were to require a resource tag to be on an instance to control management access to it, you also would need to grant permission to the ec2:CreateTags API action. This action cannot be controlled via an ARN or condition, so it would allow the user to tag any instance and then have management access to any instance on the account.

Before you can specify an instance profile ARN, you must create an instance profile. This can be done in the IAM console. As a user with permission to create and edit IAM resources, sign in to the AWS Management Console and go to the IAM console. You need to create a new role, so click Roles and then click Create Role.

Name the role VPCLockdown, as shown in the following screenshot.

Next, select the EC2 service role, as shown in the following image.

You do not need to specify a policy, so on the Attach Policy step, you can just click Next Step. Then on the Review step, click Create Role.

The next section of the policy takes advantage of the role that you just created and specifies the ec2:InstanceProfile ARN in the Condition value. Again, be sure to replace placeholder account numbers with your actual account numbers.

{
      "Sid" : "AllowInstanceActions",
      "Effect" : "Allow",
      "Action" : [
            "ec2:RebootInstances",
            "ec2:StopInstances",
            "ec2:TerminateInstances",
            "ec2:StartInstances",
            "ec2:AttachVolume",
            "ec2:DetachVolume"
      ],
      "Resource" : "arn:aws:ec2:us-east-1:123456789012:instance/*",
      "Condition" : {
            "StringEquals" : {
            "ec2:InstanceProfile" : "arn:aws:iam::123456789012:instance-profile/VPCLockDown"
            }
      }
},
{
      "Sid" : "EC2RunInstances",
      "Effect" : "Allow",
      "Action" : "ec2:RunInstances",
      "Resource" : "arn:aws:ec2:us-east-1:123456789012:instance/*",
      "Condition" : {
            "StringEquals" : {
            "ec2:InstanceProfile" : "arn:aws:iam::123456789012:instance-profile/VPCLockDown"
            }
      }
},
{
      "Sid" : "RemainingRunInstancePermissions",
      "Effect" : "Allow",
      "Action" : "ec2:RunInstances",
      "Resource" : [
            "arn:aws:ec2:us-east-1:123456789012:volume/*",
            "arn:aws:ec2:us-east-1::image/*",
            "arn:aws:ec2:us-east-1::snapshot/*",
            "arn:aws:ec2:us-east-1:123456789012:network-interface/*",
            "arn:aws:ec2:us-east-1:123456789012:key-pair/*",
            "arn:aws:ec2:us-east-1:123456789012:security-group/*",
            "arn:aws:ec2:us-east-1:123456789012:subnet/*"
      ]
}

Now you need to isolate ec2:RunInstances to a single VPC. To do this, you need to take advantage of the ARNs that ec2:RunInstances uses, namely the subnet ARN. The subnet ARN supports the use of the VPC condition, so you can specify your VPC in a condition for this ARN and that would require the instance to be launched in that VPC and with ec2:InstanceProfile as designated with the instance ARN. This would change our ec2:RunInstances permissions to look more like the following.

{
      "Sid" : "EC2RunInstances",
      "Effect" : "Allow",
      "Action" : "ec2:RunInstances",
      "Resource" : "arn:aws:ec2:us-east-1:123456789012:instance/*",
      "Condition" : {
            "StringEquals" : {
            "ec2:InstanceProfile" : "arn:aws:iam::123456789012:instance-profile/VPCLockDown"
            }
      }
},
{
      "Sid" : "EC2RunInstancesSubnet",
      "Effect" : "Allow",
      "Action" : "ec2:RunInstances",
      "Resource" : "arn:aws:ec2:us-east-1:123456789012:subnet/*",
      "Condition" : {
            "StringEquals" : {
            "ec2:vpc" : "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-1234abcd"
            }
      }
},
{
      "Sid" : "RemainingRunInstancePermissions",
      "Effect" : "Allow",
      "Action" : "ec2:RunInstances",
      "Resource" : [
            "arn:aws:ec2:us-east-1:123456789012:volume/*",
            "arn:aws:ec2:us-east-1::image/*",
            "arn:aws:ec2:us-east-1::snapshot/*",
            "arn:aws:ec2:us-east-1:123456789012:network-interface/*",
            "arn:aws:ec2:us-east-1:123456789012:key-pair/*",
            "arn:aws:ec2:us-east-1:123456789012:security-group/*"
      ]
}

Because you are requiring the user to specify an instance profile at launch, you will need to give them the ability to see the role and pass it to the instance.

{"Version" : "2012-10-17",
 "Statement" : [{
      "Sid" : "NonResourceBasedReadOnlyPermissions",
      "Action" : [
            "ec2:Describe*",
            "iam:GetInstanceProfiles",
            "iam:ListInstanceProfiles"
      ],
      "Effect" : "Allow",
      "Resource" : "*"
},
{
      "Sid" : "IAMPassroleToInstance",
      "Action" : "iam:PassRole",
      "Effect" : "Allow",
      "Resource" : "arn:aws:iam::123456789012:role/VPCLockDown"
},

Finally, within the permission subset, there are some permissions that are not resource specific, but have the ability to leverage the same VPC condition—permissions such as deleting routes or changing security groups.

{
      "Sid" : "EC2VpcNonresourceSpecificActions",
      "Effect" : "Allow",
      "Action" : [
            "ec2:DeleteNetworkAcl",
            "ec2:DeleteNetworkAclEntry",
            "ec2:DeleteRoute",
            "ec2:DeleteRouteTable",
            "ec2:AuthorizeSecurityGroupEgress",
            "ec2:AuthorizeSecurityGroupIngress",
            "ec2:RevokeSecurityGroupEgress",
            "ec2:RevokeSecurityGroupIngress",
            "ec2:DeleteSecurityGroup"
      ],
      "Resource" : "*",
      "Condition" : {
            "StringEquals" : {
            "ec2:vpc" : "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-1234abcd"
            }
      }
}

Other common permissions that have been requested in this policy include ec2:CreateKeyPair and ec2:CreateSecurityGroup. Both of these permissions are not resource specific and do not support conditions, so you would add them in the same statement in which you have ec2:Describe*.

{
      "Sid" : "NonResourceBasedReadOnlyPermissions",
      "Action" : [
            "ec2:Describe*",
            "ec2:CreateKeyPair",
            "ec2:CreateSecurityGroup",
            "iam:GetInstanceProfiles",
            "iam:ListInstanceProfiles"
      ],
      "Effect" : "Allow",
      "Resource" : "*"
}

Putting all sections of the code together, the entire IAM user policy follows. Be sure to replace placeholder account numbers with your actual account numbers.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "NonResourceBasedReadOnlyPermissions",
            "Action": [
                "ec2:Describe*",
                "ec2:CreateKeyPair",
                "ec2:CreateSecurityGroup",
                "iam:GetInstanceProfiles",
                "iam:ListInstanceProfiles"
            ],
            "Effect": "Allow",
            "Resource": "*"
        },
        {
            "Sid": "IAMPassroleToInstance",
            "Action": [
                "iam:PassRole"
            ],
            "Effect": "Allow",
            "Resource": "arn:aws:iam::123456789012:role/VPCLockDown"
        },
        {
            "Sid": "AllowInstanceActions",
            "Effect": "Allow",
            "Action": [
                "ec2:RebootInstances",
                "ec2:StopInstances",
                "ec2:TerminateInstances",
                "ec2:StartInstances",
                "ec2:AttachVolume",
                "ec2:DetachVolume"
            ],
            "Resource": "arn:aws:ec2:us-east-1:123456789012:instance/*",
            "Condition": {
                "StringEquals": {
                    "ec2:InstanceProfile": "arn:aws:iam::123456789012:instance-profile/VPCLockDown"
                }
            }
        },
        {
            "Sid": "EC2RunInstances",
            "Effect": "Allow",
            "Action": "ec2:RunInstances",
            "Resource": "arn:aws:ec2:us-east-1:123456789012:instance/*",
            "Condition": {
                "StringEquals": {
                    "ec2:InstanceProfile": "arn:aws:iam::123456789012:instance-profile/VPCLockDown"
                }
            }
        },
        {
            "Sid": "EC2RunInstancesSubnet",
            "Effect": "Allow",
            "Action": "ec2:RunInstances",
            "Resource": "arn:aws:ec2:us-east-1:123456789012:subnet/*",
            "Condition": {
                "StringEquals": {
                    "ec2:vpc": "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-7bcd371e"
                }
            }
        },
        {
            "Sid": "RemainingRunInstancePermissions",
            "Effect": "Allow",
            "Action": "ec2:RunInstances",
            "Resource": [
                "arn:aws:ec2:us-east-1:123456789012:volume/*",
                "arn:aws:ec2:us-east-1::image/*",
                "arn:aws:ec2:us-east-1::snapshot/*",
                "arn:aws:ec2:us-east-1:123456789012:network-interface/*",
                "arn:aws:ec2:us-east-1:123456789012:key-pair/*",
                "arn:aws:ec2:us-east-1:123456789012:security-group/*"
            ]
        },
        {
            "Sid": "EC2VpcNonresourceSpecificActions",
            "Effect": "Allow",
            "Action": [
                "ec2:DeleteNetworkAcl",
                "ec2:DeleteNetworkAclEntry",
                "ec2:DeleteRoute",
                "ec2:DeleteRouteTable",
                "ec2:AuthorizeSecurityGroupEgress",
                "ec2:AuthorizeSecurityGroupIngress",
                "ec2:RevokeSecurityGroupEgress",
                "ec2:RevokeSecurityGroupIngress",
                "ec2:DeleteSecurityGroup"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "ec2:vpc": "arn:aws:ec2:us-east-1:123456789012:vpc/vpc-7bcd371e"
                }
            }
        }
    ]
}

Through this policy, users should be able to:

  • Sign in to the AWS Management Console and go to the Amazon EC2 console.
  • Launch an EC2 instance as long as they:
    • Specify a subnet in the proper VPC.
    • Specify the allowed instance profiles.
  • Start/stop/reboot/terminate/attach volume/detach volume on an instance as long as they:
    • Specify an instance launched with the proper instance profiles.
  • Delete security groups, routes, route tables, network ACLs, and ACL entries as well as authorize and revoke security group ingress and egress rules, as long as they are in the proper VPC.

In this blog post, I have shown how you can lock down a user’s EC2 capabilities to a specific VPC. Using instance profiles, you can establish this level of access control while still allowing the user flexibility to take certain necessary actions.

If you have questions or comments, post them below or on the IAM forum.

- Chris

Comments