1. Introduction

CloudFormation is a useful tool when working with AWS to define your infrastructure as code, or at least a YAML or JSON template. These templates allow us to make almost any change imaginable within the AWS ecosystem.

In this article we’ll take a look at how to practice the principle of least privilege with CloudFormation, with a working example making use of the CloudFormation service role. This will allow the CloudFormation stack to make changes by assuming that role, completely separately from the user or role who initiates the change in the first place.

2. Overview

When we use CloudFormation templates to deploy resources into AWS, we do that using stacks. These are self-contained groupings of resources that can be created, updated, and deleted. When you delete a stack, you delete all the resources contained within it.

CloudFormation stack info on the AWS Console

When a stack is created, updated, or deleted, it’s permissions are by default defined by the role of the user that updates the stack. For example, if I want to create a stack from a template that defines an S3 bucket, my user will have to have the s3:CreateBucket permission (as well as the cloudformation:CreateStack permission).

This isn’t always desirable, if perhaps you want a user to be able to update a stack even if he doesn’t have permissions to update the resources in the stack himself.

You’ll see in the screenshot above, that a stack also has an IAM role property. This allows CloudFormation to assume a service role that grants specific permissions during the stack operation.

service role is an AWS Identity and Access Management (IAM) role that allows AWS CloudFormation to make calls to resources in a stack on your behalf. 

The diagram below how this works, in the scenario where we want to deploy a CloudFormation template that creates an S3 bucket.

CloudFormation service role

AWS IAM Lingo Recap

IAM: stands for Identity and Access Management Permission: defines access to an AWS resource. Implemented using policies (see below). Role: defines a set of permissions to make AWS service requests. Users or services can assume roles to perform allowed actions. Policy: a document used to define permissions. Can be assigned to a role. Contains actions (e.g. s3:CreateBucket) and resources (e.g. specific bucket ARN or * for all buckets). ARN: Amazon resource name or unique identifier for a resource.

3. A working example

Here’s a working example that will show you how to create a stack with a service role. It will demonstrate how that service role is then used to limit or extend what permissions the stack has, beyond the permissions of the user creating the stack in the first place.

To follow along with this example, you’ll need:

  1. An AWS account (see guide)
  2. Access to an admin user to set up an initial user
  3. An access key and secret key for that user (see guide)
  4. AWS CLI installed (see guide)
  5. AWS CLI configured for your admin user (see guide)

3.1. Setting up a CloudFormation user

We’ll start by creating a user. In this example, we’ll use this user to create our CloudFormation stack:

aws iam create-user --user-name cloudformation-user

Then create an access key, so we can configure access as this user from the API:

aws iam create-access-key --user-name cloudformation-user

The response will contain the access key and secret key:

Use this information to run aws configure --profile cloudformation-user and configure a new profile.

Now we’ll be able to append --profile cloudformation-user to any AWS CLI command to run the command as this user. We’ll do that on any CloudFormation stack commands from this point onward.

3.2. Attaching a policy

Currently this user can’t do anything in AWS. Not very useful! Let’s create a policy for this user which will allow them to:

  • create, describe, update, and delete a CloudFormation stack
  • describe CloudFormation stack events, so we can see what happens if things go wrong
  • pass a role to an AWS service, in our case specifically to CloudFormation

Create a file cloudformation-user-policy.txt with the following contents:

{
     "Version": "2012-10-17",
     "Statement": [
         {
             "Effect": "Allow",
             "Action": [
                 "cloudformation:CreateStack",
                 "cloudformation:DescribeStacks",
                 "cloudformation:DeleteStack",
                 "cloudformation:DescribeStackEvents",
                 "cloudformation:UpdateStack",
                 "iam:PassRole"
             ],
             "Resource": "*"
         }
     ]
 }

Then run the following command to attach the above policy to the cloudformation-user:

aws iam put-user-policy --user-name cloudformation-user --policy-name cloudformation-user-policy --policy-document file://./cloudformation-user-policy.txt

Info: the command above attaches an inline policy to the user. This policy is only attached to a single entity, whereas a managed policy is standalone and can be attached to multiple entities.

3.3. Creating the initial stack

Create a file called stack.yml which should contain the CloudFormation template below which defines an S3 bucket resource.

Info: S3 is an AWS storage service, and an S3 bucket is just a repository where files can be stored.

AWSTemplateFormatVersion: 2010-09-09
Description: S3 bucket for testing
Resources:
  TestS3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Sub 'test-bucket-${AWS::StackName}-${AWS::Region}-${AWS::AccountId}'

Apply the stack using the AWS CLI. We’ll be using the cloudformation-user to run these stack related commands:

aws cloudformation create-stack –template-body file://./stack.yml –stack-name my-test-stack –profile cloudformation-user

This will create the stack in AWS, which will automatically try to create the S3 bucket. To query the stack status, just run the following command:

aws cloudformation describe-stacks --stack-name my-test-stack --profile cloudformation-user

Unfortunately you’ll see that eventually there was a problem, and the stack is in the ROLLBACK_COMPLETE status. To see what happened we need to look at the individual events that happened to the stack:

aws cloudformation describe-stack-events --stack-name my-test-stack --profile cloudformation-user

You can see here that we have a resource status CREATE_FAILED, with the reason that access was denied to s3:CreateBucket. This is fair enough, considering our cloudformation-user doesn’t have this permission. At this point we have 2 options:

  1. Add s3:CreateBucket permission to our cloudformation-user
  2. Create a new service role with the s3:CreateBucket permission, and assign it to our CloudFormation stack

Since this article is about CloudFormation stack roles, we’ll go with option 2. We don’t want to allow our cloudformation-user to directly create S3 buckets, but we’re happy for S3 buckets to be created via CloudFormation stacks.

3.4. Creating the CloudFormation service role

We’ll need to create a role for the CloudFormation service to assume. That role will need a policy with the s3:CreateBucket permission. It also will need something called an assume role policy document which defines the trust relationship so that the CloudFormation service can assume this role.

First off let’s create the file assume-role.txt containing the assume role policy document:

{
   "Version": "2012-10-17",
   "Statement": [
     {
       "Effect": "Allow",
       "Principal": {
         "Service": "cloudformation.amazonaws.com"
       },
       "Action": "sts:AssumeRole"
     }
   ]
 }

Info: the document above is just saying that the cloudformation.amazonaws.com service can assume the role to which this policy is attached.

Next up, create a role passing the assume role policy document we just made:

aws iam create-role --role-name cloudformation-role --assume-role-policy-document file://./assume-role.txt

Now all that’s left is to attach a policy to the role which gives the role the s3:CreateBucket permission. We’ll also add s3:DeleteBucket, so that you can clean up the stack later on. Let’s put that in a file called cloudformation-role-policy.txt:

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

And attach it to the role like so:

aws iam put-role-policy --role-name cloudformation-role --policy-name cloudformation-policy --policy-document file://./cloudformation-role-policy.txt

3.5. Running the CloudFormation changes with the service role

Now it’s time to pull it all together, and create the stack again but this time with a role which the CloudFormation service will assume to create the S3 bucket resource. Let’s first delete the previous stack:

aws cloudformation delete-stack --stack-name my-test-stack --profile cloudformation-user

And now run the same create-stack command, but this time with the role-arn parameter specified:

aws cloudformation create-stack --template-body file://./stack.yml --stack-name my-test-stack --profile cloudformation-user --role-arn

Tip: if you didn’t note down the cloudformation-role role ARN, you can use this command to get it again: aws iam get-role --role-name cloudformation-role

Check the stack status again, and this time after a while it should be CREATE_COMPLETE:

aws cloudformation describe-stacks --stack-name my-test-stack --profile cloudformation-user

If you run aws s3 ls you can see your new bucket has been created!

3.6. Cleanup

If you like to clean up after yourself like me, you can issue the following commands to delete all the resources we’ve just created:

aws cloudformation delete-stack --stack-name my-test-stack --profile cloudformation-user

aws iam delete-role-policy --role-name cloudformation-role --policy-name cloudformation-policy

aws iam delete-role --role-name cloudformation-role

aws iam delete-user-policy --user-name cloudformation-user --policy-name cloudformation-user-policy

aws iam list-access-keys --user-name cloudformation-user

aws iam delete-access-key --user-name cloudformation-user --access-key-id <access-key-id-from-above-command>

aws iam delete-user --user-name cloudformation-user

You’ll also need to manually delete the entries for cloudformation-user from ~/.aws/config and ~/.aws/credentials.

4. Conclusion

You’ve now seen how you can use a CloudFormation service role to provide permissions to a CloudFormation stack beyond those of the user who has created it.

This can be particularly handy if, for example, you have a continuous integration (CI) server that is creating or updating CloudFormation stacks. You can limit the permissions of the CI server to create or update a stack. The stack itself will use a different role, giving it the permissions to create whatever resources it needs to.