CloudFormation permissions are surprisingly granular, allowing you to grant it just enough access to manage resources without over-provisioning.

Let’s say you’re deploying a new VPC with a couple of EC2 instances and an S3 bucket. Normally, you might let CloudFormation use the default AWSServiceRoleForCloudFormation role. This role has broad permissions, including AdministratorAccess, which is a huge security risk. If a malicious actor gains control of your AWS account and can trigger CloudFormation, they could potentially deploy anything.

Instead, we can create a dedicated service role specifically for this stack.

Here’s a CloudFormation template that creates an IAM role named MyVPCStackRole with the necessary permissions to create a VPC, EC2 instances, and an S3 bucket:

AWSTemplateFormatVersion: '2010-09-09'
Description: IAM Role for My VPC CloudFormation Stack

Resources:
  MyVPCStackRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: MyVPCStackRole
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: VPCResourceCreationPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:CreateVpc
                  - ec2:CreateSubnet
                  - ec2:CreateInternetGateway
                  - ec2:AttachInternetGateway
                  - ec2:CreateRouteTable
                  - ec2:CreateRoute
                  - ec2:AssociateRouteTable
                  - ec2:DescribeVpcs
                  - ec2:DescribeSubnets
                  - ec2:DescribeRouteTables
                  - ec2:DescribeInternetGateways
                  - ec2:DescribeSecurityGroups
                  - ec2:CreateSecurityGroup
                  - ec2:AuthorizeSecurityGroupIngress
                  - ec2:RevokeSecurityGroupIngress
                  - ec2:DeleteSecurityGroup
                  - ec2:DeleteRoute
                  - ec2:DeleteRouteTable
                  - ec2:DetachInternetGateway
                  - ec2:DeleteInternetGateway
                  - ec2:DeleteSubnet
                  - ec2:DeleteVpc
                Resource: '*' # For VPC resources, '*' is often necessary as resource IDs are not known at creation time.
              - Effect: Allow
                Action:
                  - ec2:CreateTags
                  - ec2:DeleteTags
                Resource: '*'
              - Effect: Allow
                Action:
                  - iam:PassRole # Required for EC2 instances to assume their own roles
                Resource: !GetAtt EC2InstanceProfile.Arn # Assuming you'll create an EC2 instance profile
        - PolicyName: S3ResourceCreationPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:CreateBucket
                  - s3:DeleteBucket
                  - s3:PutBucketPolicy # If you plan to attach policies to the bucket
                  - s3:GetBucketPolicy
                  - s3:DeleteBucketPolicy
                Resource: !Sub 'arn:aws:s3:::${MyS3BucketName}-*' # Using a pattern for bucket names
        # Add a policy for EC2 instances to be able to be launched
        - PolicyName: EC2LaunchPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:RunInstances
                  - ec2:TerminateInstances
                  - ec2:CreateNetworkInterface
                  - ec2:DeleteNetworkInterface
                  - ec2:AttachNetworkInterface
                  - ec2:DetachNetworkInterface
                  - ec2:DescribeInstances
                  - ec2:DescribeImages
                  - ec2:DescribeInstanceTypes
                  - ec2:DescribeKeyPairs
                  - ec2:DescribeVpcAttribute
                  - ec2:DescribeSubnetAttribute
                  - ec2:DescribeSecurityGroups # To associate security groups
                Resource: '*'

  # Example of an S3 bucket that the role will manage
  MyS3BucketName:
    Type: String
    Default: my-unique-app-bucket

  # Example of an EC2 Instance Profile that might be used by the EC2 instances
  EC2InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      InstanceProfileName: MyEC2InstanceProfile
      Path: /
      Roles:
        - !Ref MyVPCStackRole # This allows the EC2 instances to assume the role we created

When you deploy this template, CloudFormation creates the MyVPCStackRole. Then, when you deploy your VPC stack, you specify this MyVPCStackRole as the Stack’s service role. This tells CloudFormation to use these specific, limited permissions instead of its default broad ones.

The AssumeRolePolicyDocument is critical. It explicitly allows the cloudformation.amazonaws.com service principal to assume this role. Without it, CloudFormation can’t even pick up the role.

The Policies section contains the actual permissions. Notice we’re being specific: ec2:CreateVpc, ec2:CreateSubnet, s3:CreateBucket, etc. We’re not granting * for every action. For resources like VPCs and subnets, Resource: '*' is often a practical necessity because CloudFormation doesn’t know the exact ARNs of resources it’s about to create. However, for S3 buckets, we can be more precise by using a wildcard pattern like arn:aws:s3:::${MyS3BucketName}-* if we pre-define a naming convention for our buckets.

The iam:PassRole permission is crucial if your EC2 instances themselves need to assume an IAM role (e.g., for accessing other AWS services). You need to grant iam:PassRole on the instance profile’s role to the CloudFormation service role.

If you forget to include iam:PassRole in your service role’s policy when your stack attempts to launch EC2 instances that require an instance profile, the next error you’ll encounter is: The EC2 instance or spot instance request is not authorized to use the instance profile.

Want structured learning?

Take the full Cloudformation course →