One Cloud Please

CloudFormation Static Analysis

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.

Pre-deploy vs. post-deploy analysis

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.

Validating templates before deployment

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.

Diving in

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.

CloudFormation Resource Specification

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.

Thinking maliciously

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.