17 October 2018
AWS CloudFormation is a great tool for developers to provision AWS resources in a structured, repeatable way that also has the added benefit of making updates and teardowns far more reliable than if you were to do it with the individual resource APIs. It is the recommended approach for teams to utilise these stacks for the majority of their workloads and easily integrates into CI/CD pipelines.
Being so powerful, the complexity of CloudFormation templates can quickly become overwhelming and shortcuts or mistakes can occur. One common solution to this problem is to define a set of rules for the stack resources to be evaluated against. These rules can be generically defined, or specific to a particular teams needs. For example, a common issue that teams face is S3 buckets being incorrectly exposed. A rule may be defined that prevents this from occuring or notifies security teams.
There are two distinct approaches to performing CloudFormation analysis; pre-deploy and post-deploy. Pre-deploy analysis reviews the content of the templates before they are created or updated whereas post-deploy analysis will look at the resultant state of the resources created/updated by the CloudFormation action.
Pre-deploy analysis will catch problems before they have a chance to manifest themselves in the environment. It is a more security-conscious approach but has the drawback of being significantly more difficult to predict or simulate the result.
Post-deploy has a much clearer picture of the state of resources and the result of the stacks actions, however some damage may already have been done the moment the resources are placed in this state. Amazon GuardDuty is a service which will alert on an Amazon-managed pre-defined set of rules on all resources within your account and is an example of a post-deploy analysis and alerting tool.
Let’s discuss how a pre-deploy tool might work. The following example is written in Python 3:
def processItem(item):
if isinstance(v, dict):
for k, v in item.items():
processItem(v)
elif isinstance(v, list):
for listitem in v:
processItem(listitem)
else:
evaluate_ruleset(item)
processItem(template_as_object)
This is a very rudimentary evaluator that looks for all primitive types (strings, integers, booleans) within the template and evaluates against the ruleset.
Consider the following template:
AWSTemplateFormatVersion: '2010-09-09'
Description: A public bucket
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
A rule that prevents S3 buckets from being publicly exposed may choose to interrogate the AccessControl
property of any AWS::S3::Bucket
resource for a public ACL and alert or deny based on that. This is how the majority of pre-deployment analysis pipelines work. Things can get tricky though when you involve the CloudFormation intrinsic functions, like Ref
. Now consider the following template:
AWSTemplateFormatVersion: '2010-09-09'
Description: A public bucket
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl:
Fn::Join:
- ''
- - Publi
- cRead
You’ll quickly notice that even if a tool were to iterate through all properties in every Map and List, they would never find the “PublicRead” keyword intact. It’s very common to join strings, or reference mappings in templates so a string-matching approach would be fairly ineffective.
AWS produces a JSON-formatted file called the AWS CloudFormation Resource Specification. This file is a formal definition of all the possible resource types that CloudFormation can process. It includes all resources, their properties and information about those fields such as whether or not they need recreation when they are modified.
We can use this file to evaluate the properties for each resource and apply rulesets directly to individual properties, rather than the template as a whole. With logic around the processing of the intrinsic functions, we have created an open-source CloudFormation template simulator that is easily deployable in any environment. This simulator is able to evaluate most intrinsic functions like the example above and properly evaluate with our ruleset.
The template simulator can be found at https://github.com/iann0036/cfn-analyse.
Now consider the following template:
AWSTemplateFormatVersion: '2010-09-09'
Description: A public bucket
Resources:
TopicLower:
Type: AWS::SNS::Topic
Properties:
TopicName: a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-0-1-2-3-4-5-6-7-8-9
TopicUpper:
Type: AWS::SNS::Topic
Properties:
TopicName: A-B-C-D-E-F-G-H-I-J-K-L-M-N-O-P-Q-R-S-T-U-V-W-X-Y-Z
S3Bucket:
Type: AWS::S3::Bucket
Properties:
AccessControl:
Fn::Join:
- ''
- - Fn::Select:
- 15
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicUpper
- TopicName
- Fn::Select:
- 20
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 1
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 11
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 8
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 2
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 17
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicUpper
- TopicName
- Fn::Select:
- 4
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 0
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
- Fn::Select:
- 3
- Fn::Split:
- "-"
- Fn::GetAtt:
- TopicLower
- TopicName
The above template will actually evaluate to produce a public S3 bucket. This is because the S3 buckets AccessControl
property uses characters from the TopicName
attribute of the SNS topics. The format of these attributes is not formally documented in the CloudFormation resource specification, nor anywhere else. This means that there is currently no effective way to truly verify that resources with the intrinsic functions Ref
or Fn::GetAtt
are truly valid against the defined ruleset.
By the nature of CloudFormation, there isn’t a perfect solution for static analysis which is why a multi-layered strategy is the most effective defense.