The following is a guest post by Hubert Cheung, Solutions Architect.
AWS CloudFormation makes it easy for developers and systems administrators to create and manage a collection of related AWS resources by provisioning and updating them in an orderly and predictable way. Many of our customers use CloudFormation to control all of the resources in their AWS environments so that they can succinctly capture changes, perform version control, and manage costs in their infrastructure, among other activities.
Customers often ask us how to control permissions for CloudFormation stacks. In this post, we share some of the best security practices for CloudFormation, which include using AWS Identity and Access Management (IAM) policies, CloudFormation-specific IAM conditions, and CloudFormation stack policies. Because most CloudFormation deployments are executed from the AWS command line interface (CLI) and SDK, we focus on using the AWS CLI and SDK to show you how to implement the best practices.
Limiting Access to CloudFormation Stacks with IAM
With IAM, you can securely control access to AWS services and resources by using policies and users or roles. CloudFormation leverages IAM to provide fine-grained access control.
As a best practice, we recommend that you limit service and resource access through IAM policies by applying the principle of least privilege. The simplest way to do this is to limit specific API calls to CloudFormation. For example, you may not want specific IAM users or roles to update or delete CloudFormation stacks. The following sample policy allows all CloudFormation APIs access, but denies UpdateStack and DeleteStack APIs access on your production stack:
{ "Version":"2012-10-17", "Statement":[{ "Effect":"Allow", "Action":[ "cloudformation:*" ], "Resource":"*" }, { "Effect":"Deny", "Action":[ "cloudformation:UpdateStack", "cloudformation:DeleteStack" ], "Resource":"arn:aws:cloudformation:us-east-1:123456789012:stack/MyProductionStack/*" }] } |
We know that IAM policies often need to allow the creation of particular resources, but you may not want them to be created as part of CloudFormation. This is where CloudFormation’s support for IAM conditions comes in.
IAM Conditions for CloudFormation
There are three CloudFormation-specific IAM conditions that you can add to your IAM policies:
- cloudformation:TemplateURL
- cloudformation:ResourceTypes
- cloudformation:StackPolicyURL
With these three conditions, you can ensure that API calls for stack actions, such as create or update, use a specific template or are limited to specific resources, and that your stacks use a stack policy, which prevents stack resources from unintentionally being updated or deleted during stack updates.
Condition: TemplateURL
The first condition, cloudformation:TemplateURL, lets you specify where the CloudFormation template for a stack action, such as create or update, resides and enforce that it be used. In an IAM policy, it would look like this:
{ "Version":"2012-10-17", "Statement":[{ "Effect": "Deny", "Action": [ "cloudformation:CreateStack", “cloudformation:UpdateStack” ], "Resource": "*", "Condition": { "StringNotEquals": { "cloudformation:TemplateURL": [ "https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template" ] } } }, { "Effect": "Deny", "Action": [ "cloudformation:CreateStack", "cloudformation:UpdateStack" ], "Resource": "*", "Condition": { "Null": { "cloudformation:TemplateURL": "true" } } }] } |
The first statement ensures that for all CreateStack or UpdateStack API calls, users must use the specified template. The second ensures that all CreateStack or UpdateStack API calls must include the TemplateURL parameter. From the CLI, your calls need to include the –template-url parameter:
aws cloudformation create-stack --stack-name cloudformation-demo --template-url https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template
Condition: ResourceTypes
CloudFormation also allows you to control the types of resources that are created or updated in templates with an IAM policy. The CloudFormation API accepts a ResourceTypes parameter. In your API call, you specify which types of resources can be created or updated. However, to use the new ResourceTypes parameter, you need to modify your IAM policies to enforce the use of this particular parameter by adding in conditions like this:
{ "Version":"2012-10-17", "Statement":[{ "Effect": "Deny", "Action": [ "cloudformation:CreateStack", "cloudformation:UpdateStack" ], "Resource": "*", "Condition": { "ForAllValues:StringLike": { "cloudformation:ResourceTypes": [ "AWS::IAM::*" ] } } }, { "Effect": "Deny", "Action": [ "cloudformation:CreateStack", "cloudformation:UpdateStack" ], "Resource": "*", "Condition": { "Null": { "cloudformation:ResourceTypes": "true" } } }] } |
From the CLI, your calls need to include a --resource-types parameter. A call to update your stack will look like this:
aws cloudformation create-stack --stack-name cloudformation-demo --template-url https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template --resource-types=”[AWS::IAM::Group, AWS::IAM::User]”
Depending on the shell, the command might need to be enclosed in quotation marks as follow; otherwise, you’ll get a “No JSON object could be decoded” error:
aws cloudformation create-stack --stack-name cloudformation-demo --template-url https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template --resource-types=’[“AWS::IAM::Group”, “AWS::IAM::User”]’
The ResourceTypes conditions ensure that CloudFormation creates or updates the right resource types and templates with your CLI or API calls. In the first example, our IAM policy would have blocked the API calls because the example included AWS::IAM resources. If our template included only AWS::EC2::Instance resources, the CLI command would look like this and would succeed:
aws cloudformation create-stack --stack-name cloudformation-demo --template-url https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template --resource-types=’[“AWS::EC2::Instance”]’
The third condition is the StackPolicyURL condition. Before we explain how that works, we need to provide some additional context about stack policies.
Stack Policies
Often, the worst disruptions are caused by unintentional changes to resources. To help in mitigating this risk, CloudFormation provides stack policies, which prevent stack resources from unintentionally being updated or deleted during stack updates. When used in conjunction with IAM, stack policies provide a second layer of defense against both unintentional and malicious changes to your stack resources.
The CloudFormation stack policy is a JSON document that defines what can be updated as part of a stack update operation. To set or update the policy, your IAM users or roles must first have the ability to call the cloudformation:SetStackPolicy action.
You apply the stack policy directly to the stack. Note that this is not an IAM policy. By default, setting a stack policy protects all stack resources with a Deny to deny any updates unless you specify an explicit Allow. This means that if you want to restrict only a few resources, you must explicitly allow all updates by including an Allow on the resource "*" and a Deny for specific resources.
For example, stack policies are often used to protect a production database because it contains data that will go live. Depending on the field that's changing, there are times when the entire database could be replaced during an update. In the following example, the stack policy explicitly denies attempts to update your production database:
{ "Statement" : [ { "Effect" : "Deny", "Action" : "Update:*", "Principal": "*", "Resource" : "LogicalResourceId/ProductionDB_logical_ID" }, { "Effect" : "Allow", "Action" : "Update:*", "Principal": "*", "Resource" : "*" } ] } |
You can generalize your stack policy to include all RDS DB instances or any given ResourceType. To achieve this, you use conditions. However, note that because we used a wildcard in our example, the condition must use the "StringLike" condition and not "StringEquals":
{ "Statement" : [ { "Effect" : "Deny", "Action" : "Update:*", "Principal": "*", "Resource" : "*", "Condition" : { "StringLike" : { "ResourceType" : ["AWS::RDS::DBInstance", "AWS::AutoScaling::*"] } } }, { "Effect" : "Allow", "Action" : "Update:*", "Principal": "*", "Resource" : "*" } ] } |
For more information about stack policies, see Prevent Updates to Stack Resources.
Finally, let’s ensure that all of your stacks have an appropriate pre-defined stack policy. To address this, we return to IAM policies.
Condition:StackPolicyURL
From within your IAM policy, you can ensure that every CloudFormation stack has a stack policy associated with it upon creation with the StackPolicyURL condition:
{ "Version":"2012-10-17", "Statement":[ { "Effect": "Deny", "Action": [ "cloudformation:SetStackPolicy" ], "Resource": "*", "Condition": { "ForAnyValue:StringNotEquals": { "cloudformation:StackPolicyUrl": [ "https://s3.amazonaws.com/samplebucket/sampleallowpolicy.json" ] } } }, { "Effect": "Deny", "Action": [ "cloudformation:CreateStack", "cloudformation:UpdateStack" ], "Resource": "*", "Condition": { "ForAnyValue:StringNotEquals": { "cloudformation:StackPolicyUrl": [ “https://s3.amazonaws.com/samplebucket/sampledenypolicy.json” ] } } }, { "Effect": "Deny", "Action": [ "cloudformation:CreateStack", "cloudformation:UpdateStack", “cloudformation:SetStackPolicy” ], "Resource": "*", "Condition": { "Null": { "cloudformation:StackPolicyUrl": "true" } } }] } |
This policy ensures that there must be a specific stack policy URL any time SetStackPolicy is called. In this case, the URL is https://s3.amazonaws.com/samplebucket/stackpolicyallow.json. Similarly, for any create and update stack operation, this policy ensures that the StackPolicyURL is set to the sampledenypolicy.json document in S3 and that a StackPolicyURL is always specified. From the CLI, a create-stack command would look like this:
aws cloudformation create-stack --stack-name cloudformation-demo --parameters ParameterKey=Password,ParameterValue=CloudFormationDemo --capabilities CAPABILITY_IAM --template-url https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template --stack-policy-url https://s3-us-east-1.amazonaws.com/samplebucket/stackpolicydeny.json
Note that if you specify a new stack policy on a stack update, CloudFormation uses the existing stack policy: it uses the new policy only for subsequent updates. For example, if your current policy is set to deny all updates, you must run a SetStackPolicy command to change the stack policy to the one that allows updates. Then you can run an update command against the stack. To update the stack we just created, you can run this:
aws cloudformation set-stack-policy --stack-name cloudformation-demo --stack-policy-url https://s3-us-east-1.amazonaws.com/samplebucket/stackpolicyallow.json
Then you can run the update:
aws cloudformation update-stack --stack-name cloudformation-demo --parameters ParameterKey=Password,ParameterValue=NewPassword --capabilities CAPABILITY_IAM --template-url https://s3.amazonaws.com/cloudformation-templates-us-east-1/IAM_Users_Groups_and_Policies.template --stack-policy-url https://s3-us-west-2.amazonaws.com/awshubfiles/stackpolicydeny.json
The IAM policy that we used ensures that a specific stack policy is applied to the stack any time a stack is updated or created.
Conclusion
CloudFormation provides a repeatable way to create and manage related AWS resources. By using a combination of IAM policies, users, and roles, CloudFormation-specific IAM conditions, and stack policies, you can ensure that your CloudFormation stacks are used as intended and minimize accidental resource updates or deletions.
You can learn more about this topic and other CloudFormation best practices in the recording of our re:Invent 2015 session, (DVO304) AWS CloudFormation Best Practices, and in our documentation.