IAM Roles in AWS (with CloudFormation)

By Jeff Romine | February 20, 2019

Introduction

This post revisits IAM Roles in AWS, which shows how to create EC2 instances with role-based rather than credential-based access. In that post, the AWS CLI was used to create all of the required AWS resources and dependencies between them were managed manually by copying values from the result of one command into other commands for building dependent resources. Here you will learn how to accomplish the same things with far less effort using AWS CloudFormation.

CloudFormation is a service which builds and configures sets of AWS resources based on a declarative specification called a template. The set of resources that is produced from a template is referred to as a "stack".

Stack Objectives

If all goes well you will soon have a stack which meets these objectives:

  1. An EC2 instance that you can ssh into
  2. An S3 Bucket that can only be accessed by the EC2 instance, the
  3. root user in your account, and your IAM user.

Objective 1 is a pretty standard thing to do but objective 2 involves some advanced techniques for securing S3 buckets from the AWS Security Blog article How to Restrict Amazon S3 Bucket Access to a Specific IAM Role.

Prerequisites

You will need 3 things to create the stack:

  1. Your UserId
  2. An EC2 Key Pair
  3. The CloudFormation template

Look up your UserId

Your IAM UserId is what is going to be used to grant access to the S3 bucket from your IAM account. In the example the bucket policy will check the userId, represented by the aws:userId condition key, against your IAM user and will allow access if there is a match. There doesn't seem to be a way to find the UserId in the console and it isn't available within CloudFormation itself but you can get it with the following command:

WARNING: If the CreatingUserId parameter is specified incorrectly you may need help from someone with access to the root login to delete the bucket.

1
aws iam get-user

This will produce a result like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
    "User": {
        "UserName": "Administrator",
        "PasswordLastUsed": "2019-01-10T00:51:51Z",
        "CreateDate": "2018-02-09T00:30:11Z",
        "UserId": "AIDAZZZZZZZZZZZZZZZZZ",
        "Path": "/",
        "Arn": "arn:aws:iam::999999999999:user/Administrator"
    }
}

And the value that you need to save is the UserId field, AIDAZZZZZZZZZZZZZZZZZ in this example. To make things easier, you can use jq to pick out the values you want and environment variables to keep track of them. The rest of the examples will use this approach. For example:

1
  export USER_ID=`aws iam get-user | jq --raw-output .User.UserId`

Select an existing EC2 Key Pair or Create a new one

If you have already created a keypair in the region where you want your stack to be and have access to the private key file (.pem file), you can use that for this example. Otherwise, you can create one using the AWS Console or using the CLI:

1
2
3
4
5
6
export KEYPAIR_NAME=example-kp
export KEY_FILE=$KEYPAIR_NAME.pem

aws ec2 create-key-pair --key-name $KEYPAIR_NAME | \
    jq .KeyMaterial --raw-output > $KEY_FILE
chmod 400 $KEY_FILE

If you are using an existing key pair but are following along with the CLI, you may want to set the KEYPAIR_NAME and KEY_FILE environment variables as they are used in the create-stack command example below.

CloudFormation

Download iam-roles-template.json. You can use to create the stack using the AWS Console and entering the UserId and KeyName as parameters or using the CLI command below.

WARNING: This will create AWS Resources that may incur charges. Be sure to follow the Clean Up instructions below and to verify that the resources are really gone.

1
2
3
4
5
6
7
export STACK_NAME=example

aws cloudformation create-stack \
    --stack-name $STACK_NAME \
    --template-body file://iam-roles-template.json \
    --parameters ParameterKey=KeyName,ParameterValue=$KEYPAIR_NAME ParameterKey=CreatingUserId,ParameterValue=$USER_ID \
    --capabilities CAPABILITY_IAM

Wait for Stack Creation to Complete

You can watch the progress in the Console or you can use the following CLI command to check status:

1
2
3
aws cloudformation list-stacks \
    --stack-status-filter CREATE_IN_PROGRESS CREATE_COMPLETE CREATE_FAILED | \
    jq ".StackSummaries[] | select(.StackName == \"$STACK_NAME\")"

At this point you should have the stack that meets all of the objectives. If you went through IAM Roles in AWS , you should remember doing much more work to get to this point. Plus, the template did a few things that weren't covered there, such as creating a Security Group and setting up ssh access to our EC2 instance.

Examining the Stack

See Viewing AWS CloudFormation Stack Data and Resources on the AWS Management Console for information on examining your stack using the AWS CloudFormation Console. This CLI command lists all of the resources in the stack:

1
aws cloudformation describe-stack-resources --stack-name $STACK_NAME

You can use it with some other commands to look at the S3 Bucket and associated access policies associated with the stack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export BUCKET_NAME=`aws cloudformation describe-stack-resources --stack-name $STACK_NAME | \
    jq --raw-output \
       '.StackResources[] | select(.LogicalResourceId == "NewS3Bucket") | .PhysicalResourceId'`

aws s3api get-bucket-policy --bucket $BUCKET_NAME | jq '.Policy' --raw-output | jq .

export EC2_ROLE_NAME=`aws cloudformation describe-stack-resources --stack-name $STACK_NAME | \
    jq --raw-output \
       '.StackResources[] | select(.LogicalResourceId == "RootRole") | .PhysicalResourceId'`

aws iam get-role-policy --role-name $EC2_ROLE_NAME --policy-name root

The bucket policy and root policy should look very much like the policies described in How to Restrict Amazon S3 Bucket Access to a Specific IAM Role and IAM Roles in AWS. The role policy is a little different because it is an inline policy attached directly to the role rather than a managed user policy as it is in those other articles.

Validation

Now that you have created a stack, you can verify that it works as expected with these steps:

  1. Get the bucket name
  2. Check your IAM user's access to the bucket
  3. Check access to the bucket from the EC2 instance

Get the Bucket Name

To get the bucket name, you can use the resources section of the CloudFormation Console or the CLI:

1
2
3
4
export BUCKET_NAME=`aws cloudformation describe-stack-resources --stack-name $STACK_NAME | \
    jq --raw-output \
       '.StackResources[] | select(.LogicalResourceId == "NewS3Bucket") | .PhysicalResourceId'`
echo $BUCKET_NAME

Check your access to the bucket

To access the created bucket use the S3 Console or the CLI:

1
2
3
4
echo $BUCKET_NAME
echo "Hello $BUCKET_NAME" > hello.txt
aws s3 cp hello.txt s3://$BUCKET_NAME
aws s3 ls s3://$BUCKET_NAME

Check access to the bucket from the EC2 instance

Get Connection command in the console

You can get the command to connect to the EC2 instance by going to the EC2 instances page in the console, right clicking on the instance you want to connect to and selecting "Connect".

Getting the connection command with the CLI

From the CLI, this series of commands should give you the correct ssh command:

1
2
3
4
5
6
export EC2_INSTANCE_ID=`aws cloudformation describe-stack-resources --stack-name $STACK_NAME \
      | jq '.StackResources[] | select(.LogicalResourceId == "Ec2Instance") | .PhysicalResourceId' \
      --raw-output`
export PUBLIC_IP=`aws ec2 describe-instances --instance-ids $EC2_INSTANCE_ID | jq '.Reservations[0].Instances[0].PublicIpAddress' --raw-output`

echo "ssh -i $KEY_FILE ec2-user@$PUBLIC_IP"

Checking Access from the EC2 instance

You may want to open up a new terminal to ssh into the instance. Once connected you can verify access to the stack bucket and also verify that none of the other account buckets can be accessed:

1
2
3
4
5
6
# This should show all of the buckets in the account
aws s3 ls
# you will need to get the bucket name here either from the output of
# the ls command or the console
export BUCKET_NAME=<copy-pasted bucket name>
aws s3 ls s3://$BUCKET_NAME

Attempting to access other non-public buckets should fail with:

1
An error occurred (AccessDenied) when calling the ListObjects operation: Access Denied

For extra credit you can verify that other IAM users, even those with the AmazonS3FullAccess policy are unable to access the newly created stack bucket.

Clean Up

The S3 Bucket

You are about to delete the stack but this will fail unless the bucket is empty. You can either use to console to empty the bucket or the CLI:

1
aws s3 rm s3://$BUCKET_NAME --recursive

The Stack

You can use the console to initiate stack deletion or from the CLI:

1
aws cloudformation delete-stack --stack-name $STACK_NAME

You can check progress in the Console or in the CLI:

1
2
3
aws cloudformation list-stacks \
    --stack-status-filter DELETE_IN_PROGRESS DELETE_FAILED| \
    jq ".StackSummaries[] | select(.StackName == \"$STACK_NAME\")"

If, for some reason, the stack fails to delete, your best bet is to look through the events in the console for clues. About the only thing that is likely to cause a failure with deletion is the bucket not being empty. In that case, you can just empty the bucket and re-run the delete.

The Key Pair and Key File

If you created a new key pair that you want to get rid of you can remove it from the console or with:

1
2
aws ec2 delete-key-pair --key-name $KEYPAIR_NAME
rm -f $KEY_FILE

Wrap Up

Assuming that you didn't already have an EC2 key pair to use, creating this stack only really required 3 steps:

  1. Look up your UserId
  2. Create an EC2 key pair
  3. Run the create-stack command with the template

If you worked through IAM Roles in AWS, you should have found this approach to be quite a bit easier. The details of the CloudFormation template and CloudFormation operation were intentionally avoided in order to more clearly make this point. Future articles will go into more depth and will explore more of what you can do with CloudFormation.

comments powered by Disqus