How to implement the principle of least privilege with CloudFormation StackSets
March 24, 2021: We’ve corrected errors in the policy statements in steps 2 and 3 of the section “To create the IAM policy document.”
AWS CloudFormation is a service that lets you create a collection of related Amazon Web Services and third-party resources and provision them in an orderly and predictable fashion. A typical access control pattern is to delegate permissions for users to interact with CloudFormation and remove or limit their permissions to provision resources directly. You can grant the AWS CloudFormation service permission to create resources by creating a role that the user passes to CloudFormation when a stack or stack set is created. This can be used to ensure that only pre-authorized services and resources are provisioned in your AWS account. In this post, I show you how to conform to the principle of least privilege while still allowing users to use CloudFormation to create the resources they need.
If a user has permission to create a stack or stack set using a template of their creation, what stops them from using CloudFormation to create an AWS Identity and Access Management (IAM) identity that has elevated permissions that they can then use? Or, what stops the user from creating resources of a certain size or within AWS services that they’re not otherwise permitted to use?
A basic approach would be to deny the user the ability to create IAM identities, but there are many legitimate reasons why a user would need to create IAM identities. For example, creating an AWS Lambda function requires the function to have an IAM role to run under. It’s therefore common to create a role at the same time you create a function.
Instead of denying these actions, let’s look at how you can limit the scope of what can be created via CloudFormation by enforcing the application of permissions boundaries. Permissions boundaries are an IAM feature that set the maximum permissions that an identity-based policy can grant to an IAM identity. For example, consider this managed policy—called
MyLambdaBoundaryPolicy—which allows the
s3:GetObject action on any resource:
And consider this policy—called
AdminPolicy—which allows all actions on all resources:
If you apply
AdminPolicy as an identity-based policy on an IAM role and the
MyLambdaBoundaryPolicy policy as the permissions boundary policy for the role, the effective permissions that the role has are the intersection of the two policies. In this example, the permission is limited to
As illustrated in Figure 1, a permissions boundary policy will scope the effective permissions for the role to something less than what the identity policy allows. This is the mechanism used in this post to limit the effective permissions of IAM identities created by CloudFormation on behalf of a user.
CloudFormation can initiate stack and stack set deployments by assuming an IAM role that the user passes to the service. You must ensure that this role has the necessary permissions to create, update, and delete the resources that are part of the stack or stack set. The role should also adhere to the principal of least privilege by not having more permissions than are needed to deploy the stack or stack set.
This post focuses on the stack set use case as it is slightly more involved due to its use of two roles compared to the use of a single role for a stack. The two roles a stack set uses are:
- Administration role – This role is passed directly to CloudFormation by the user when creating the stack set. CloudFormation uses this role to assume the execution role within the AWS accounts that are in-scope of the stack set.
- Execution role – This is a role within each of the AWS accounts that are in scope of the stack set. CloudFormation will assume this role in each account and use it to provision resources. There must be a trust policy on this role that allows the administration role from the AWS account containing the stack set to assume it.
Figure 2 shows the relationship between these roles.
The permissions policy on the administration role must allow the role to assume the execution role in any AWS accounts that are in scope for the stack set:
Note: As described in the CloudFormation documentation, the administration role permissions policy can limit which AWS accounts CloudFormation can operate in by specifying the account ID as part of the Amazon Resource Name (ARN) of the role and listing each role individually. This example uses a wildcard account ID (
*) to allow CloudFormation to assume the execution role in any account where the execution role trusts the administration account.
The permissions policy for the execution role is more complex as it must account for every action that CloudFormation must call as it creates, updates, and deletes resources. The next section covers how to build that policy.
In keeping with the practice of granting least privilege, the permissions policy for the CloudFormation execution role should be as detailed as possible while avoiding overly generous permissions. For example, instead of allowing
lambda:*—which allows all AWS Lambda actions—use specific actions such as
The question now becomes, what are the specific actions that CloudFormation needs permissions for?
This is best gleaned by exploring the actions that CloudFormation calls when launching, updating, and deleting the stack in a non-production or development environment. In a non-production setting, the user can launch their template as a CloudFormation stack using their own, privileged permissions without affecting the production environment. A list of the resulting actions taken can be generated by examining the event history captured by AWS CloudTrail. The user can use Amazon Athena to query CloudTrail logs and build a concise list of actions.
Note: Be sure to query the CloudTrail logs only after launching and deleting the stack in order to capture actions from both operations. Refer to the AWS CloudTrail documentation for information about how to enable and use CloudTrail.
As the user who is launching a template in a non-production setting, follow these steps:
- Follow the instructions for querying CloudTrail logs using Athena to create the required data catalog table and partitions.
- Follow the instructions for creating a stack to launch your CloudFormation template. Wait for it to reach the
CREATE_COMPLETEstate before continuing.
- Follow the instructions for deleting a stack to delete the CloudFormation stack you just launched. Wait for it to reach the
DELETE_COMPLETEstate before continuing.
- CloudTrail delivers log files to its target Amazon Simple Storage Service (Amazon S3) bucket approximately every 5 minutes. As a result, the query in Athena doesn’t show results in real time. Wait a few minutes before going to step 5.
- Run the following Athena query to yield a list of actions and the associated service principals:
Note: If you are following these steps in a Region other than us-east-1, be aware that some services, such as IAM, create CloudTrail logs in the us-east-1 Region regardless of which Region the event actually occurred in. Ensure that you create a partition in your data catalog for us-east-1 and that if you’re querying by Region, you include us-east-1 in the query.
The format of the
userIdentity.arn value will depend on how you’ve authenticated to AWS. If you logged in as an IAM user,
<USER-IDENTIFIER> is your IAM user name. If you’ve logged in via federation, it will be your federated ID such as
email@example.com. You can explore the fields and possible values found in the
userIdentity element in the CloudTrail documentation. This query can also be further refined to include only results from certain AWS Regions or within a specific time window. Refer to this section of the CloudTrail documentation for information on the fields contained in log records.
Assemble the permissions policy actions for CloudFormation
The results of the Athena query will resemble those shown in Figure 3.
The eventsource column shows the AWS service principal that is reporting that an event occurred and the eventname column shows the name of the event. You can use these two pieces of data to form an action that can be used in an IAM policy.
- Shorten the values in the
eventsourcecolumn to just the service name. For example,
- Use the values in the
eventnamecolumn as-is in most cases—for example, with
CreateRole. In some cases, you must modify the name to match a valid IAM action. For example,
CreateFunction20150331should be trimmed to just
CreateFunction. Based on the preceding query results, the actions become
lambda:CreateFunction, and so on.
- Review the resulting actions using the IAM visual policy editor in the AWS Management Console by looking for the transformed
eventnamein the visual editor and selecting the name as shown in Figure 4. If you cannot find the transformed name in the list, proceed to Step 5.
- Select the JSON tab and validate that the
Actionmatches the action that you assembled in Step 1. An example for the
lambda:CreationFunctionaction is shown in Figure 5.
- Use the AWS documentation of the services you will be using to validate the proper action for a given
eventname. For example, the Amazon S3 API documentation says that the
GetBucketEncryptionaction—which correlates to the
eventnamein the Athena query results—requires the
s3:GetEncryptionConfigurationIAM action. This can be further verified by going back to the IAM visual policy editor and confirming there is no
GetBucketEncryptionaction but there is a
Now that the actions are assembled, you can combine them with your knowledge of the resources that your CloudFormation template describes to build an IAM policy document.
- Build an IAM policy document using the assembled permissions policy actions along with your knowledge of the resources in your CloudFormation template. For example, the following policy is for a CloudFormation template that describes a Lambda function named
MyLambdaFunctionand an IAM role called
MyLambdaRole. The policy is built from the preceding Athena query results—Assemble the permissions policy actions for CloudFormation.
- Inspect the CloudFormation template for any resources that take an IAM role as a property. For example, a Lambda function takes an execution role as one of its properties. If the template has such a property, then the CloudFormation execution role needs permission to
iam:PassRoleon the role that’s being given as a property in the template.
PassRoleisn’t an API call, it’s a permission; it’s important to be aware when this is required because it won’t be included in the Athena query results.
- Enforce the assignment of a permissions boundary policy whenever CloudFormation creates an IAM identity. For example, break out the
iam:CreateRoleaction into its own statement and then add an IAM condition to that statement:
This statement says that in order to create the role named
MyLambdaRole, the call to
iam:CreateRole must include a permissions boundary policy that has an ARN of
arn:aws:iam::<AWS-ACCOUNT-ID>:policy/MyLambdaBoundaryPolicy. Attempts to create the role with a different or null permissions boundary policy will be denied.
Least privilege permissions for the CloudFormation user
As the cloud administrator, you need to define the permissions for the CloudFormation user. Since all the initial work of creating resources is being done under the CloudFormation execution role, the user needs few permissions in order to launch a stack set. For example, the permissions policy that you create for them could be as simple as:
This policy gives the user just enough permissions to:
- Launch a CloudFormation stack or stack set.
GETobjects to and from the S3 bucket used to store CloudFormation templates that are uploaded via the CloudFormation console. The policy also allows creation of the S3 bucket if it doesn’t exist.
- Pass the CloudFormation administration role to CloudFormation.
- List Amazon Simple Notification Service (Amazon SNS) topics and IAM roles in order to populate those respective fields in the CloudFormation console.
When you put the CloudFormation templates, IAM roles, permissions boundary policy, and least privilege policies together in the right sequence, here’s what it looks like.
- The CloudFormation user creates two CloudFormation templates for the workload:
- A permissions template that contains the CloudFormation administration role, execution role, permissions policies, and the permissions boundary policies for any IAM identities that the second template defines. The user gives this template to you, the cloud administrator, to launch.
- A resources template that contains all of the resources related to the workload. This template must apply the boundary policies specified in the first template to any IAM identities that it creates. The user launches this template themselves.
- As the cloud administrator, you review the policies in the permissions template to ensure compliance with the principle of least privilege and with the policies of your organization. You must ensure that there is a condition key in the CloudFormation execution role policy that requires the presence of the permissions boundary policy when creating IAM identities.The following is an example of a permissions template that the user could create for a Lambda-based workload:
- As the cloud administrator, launch the permissions template as a CloudFormation stack in a single AWS account or across multiple accounts using CloudFormation StackSets to support also deploying the resources template across multiple accounts. When launching as a stack set, the template should be launched in the AWS account that manages your AWS Organizations service. The AWS account ID specified in the
CfnAdminAccountIdparameter must be included in the stack set scope.The
CfnAdminAccountIdparameter must specify the AWS account ID where the user will launch the resources template. According to best practice when working with AWS Organizations, this AWS account should be a member account and not the organization management account.
- As the cloud administrator, apply a permissions policy to the user’s IAM identity in the
CfnAdminAccountIdaccount similar to the preceding example—Least privilege permissions for the CloudFormation user.
- After you launch the permissions template, the user can launch the resources template. They must do this in the same AWS account as was specified in the
CfnAdminAccountIdparameter of the permissions template because that’s where the CloudFormation administration role has been created.
- The CloudFormation user creates two CloudFormation templates for the workload:
The following is an example resources template that matches the example permissions template shown in step 2:
To deploy this template, the user will follow these steps:
- Navigate to CloudFormation in the console, expand the menu in the left-hand pane, and choose StackSets.
- On the StackSets page, select Create StackSet. Choose Upload a template file, choose the file for the resources template, and choose Next.
- Enter a StackSet name. Note that the name has relevance because of how the policy of the CloudFormation execution role has been written. The policy grants the role permissions to manage CloudFormation stacks with the name
StackSet-MyLambda-*. This requires that you enter
MyLambdaas the name of the stack set. Choose Next.
- Use the IAM admin role ARN drop-down to choose CloudFormationAdminRole. In the IAM execution role name box, enter CloudFormationExecRole as shown in Figure 6.
- Choose Next.
- Select the AWS account numbers and Regions to deploy to and choose Next.
- Review the stack set configuration, select the check box to agree that CloudFormation will create IAM resources, and choose Submit.
The Lambda resources are created in the AWS accounts that are specified in the stack set. The role that the Lambda function runs under has enough permissions for the function to do its work, but no more. Even if the resources template assigns a more permissive permissions policy to the role, the permissions boundary policy ensures that the effective permissions are scoped to only what is required.
By applying guardrails to user permissions, you can empower users to build solutions while at the same time following the principle of least privilege and other organizational policies. As shown in this post, this is achieved through a combination of:
- IAM permissions boundary policies.
- A process for building detailed, least-privilege permissions policies.
- A two-tier deployment system where a cloud administrator deploys the guardrails and a user deploys resources for a workload that aligns with those guardrails.
To learn more about the principle of least privilege in AWS, watch Separation of duties, least privilege, delegation, and CI/CD (SDD329), which was presented at AWS re:Inforce 2019.
Want more AWS Security how-to content, news, and feature announcements? Follow us on Twitter.