The cloud enables organizations to deploy their applications and support them at scale. To further support that scale, infrastructure as code (IaC) frameworks allow organizations to provision and manage infrastructure in a repeatable and standardized way.
One such framework is CloudFormation, AWS’s proprietary IaC tool that manages AWS resource stacks through YAML or JSON templates. CloudFormation templates allow for modularity and reusability, which makes it easier to build AWS applications at scale but also adds another level of complexity. And with that additional complexity comes the question of cloud security guidelines. The best and most comprehensive security strategy requires approaching every aspect of a project with a security-first mindset. This article will explore CloudFormation basic best practices you can use to build and maintain CloudFormation templates.
CloudFormation 101
Before we dive into the security aspects of CloudFormation, let’s start with some CloudFormation basics. First, let’s look at an example. Say we want a template that creates a new EC2 instance with an ElasticIP. We could write a CloudFormation template similar to the one shown below that describes these resources and specifies the relationships between each one using either JSON or YAML format.
AWSTemplateFormatVersion: 2010-09-09 Resources: SSHSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Enable SSH access via port 22 SecurityGroupIngress: - CidrIp: 0.0.0.0/0 FromPort: 22 IpProtocol: tcp ToPort: 22 ServerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: allow connections from specified CIDR ranges SecurityGroupIngress: - IpProtocol: tcp FromPort: 80 ToPort: 80 CidrIp: 0.0.0.0/0 - IpProtocol: tcp FromPort: 22 ToPort: 22 CidrIp: 192.168.1.1/32 MyElasticIP: Type: AWS::EC2::EIP Properties: InstanceId: !Ref MyEC2Instance MyEC2Instance: Type: AWS::EC2::Instance Properties: AvailabilityZone: us-east-1c InstanceType: t2.micro ImageId: ami-0853c5d673083066f SecurityGroups: - !Ref SSHSecurityGroup - !Ref ServerSecurityGroup
CloudFormation allows us to validate this template, create the EC2 instance, and assign it an ElasticIP with the required security groups. The Resources section is the only required section, but we can add additional elements to make the template easier to update, manage and scale.
Looking at the description for MyEC2Instance, we can see that the ImageId and InstanceType are hardcoded. We can add the following Parameters section to parameterize this template:
Parameters: InstanceType: Type: String Default: t2.micro AvailabilityZone: Type: String Default: us-east-1c ImageId: Description: EC2 image id Type: AWS::SSM::Parameter::Value Default: /dev/ec2/imageId Env: Type: String Default: TEST
The first two parameters allow us to specify the type of instance and Availability Zone (AZ) where we want to launch the instance. We can also add a default value that can be overridden when we deploy the resources with the template. The third parameter references a value stored in the AWS Systems Manager–Parameter store. We can set different parameters in each region where we deploy this CloudFormation template. The final parameter defines our target environment that we’ll be using when we talk about mappings.
In YAML templates, we use the !Ref keyword to reference a parameter. With the section above added to the template, we can update the MyEC2Instance as shown below. You’ll notice that we’re already referencing the security groups using a similar notation. By referencing the security groups, we’re also indicating that the CloudFormation service will need to create the security groups before creating the EC2 instance.
… MyEC2Instance: Type: AWS::EC2::Instance Properties: AvailabilityZone: !Ref AvailabilityZone InstanceType: !Ref InstanceType ImageId: !Ref ImageId SecurityGroups: - !Ref SSHSecurityGroup - !Ref ServerSecurityGroup
Parameters work well when we want to integrate user-specific values into our template. However, if we know the values ahead of time, we can use mappings to provide more control over what the template is able to do. Let’s redefine one of the parameters we used above in terms of a map. In this situation, we might know all the AMI IDs that the template will need to execute, depending on the region and environment we are using. We could set up a Mapping section within our CloudFormation template similar to the one shown below.
Mappings: EnvAMI: eu-west-1: TEST: ami-0ade00000f337d34 PROD: ami-1cdca00000e254b65 eu-west-2: TEST: ami-2cadc00000b257d74 PROD: ami-3bbef00000a265a73 us-east-1: TEST: ami-4ccde00000b347d33 PROD: ami-5acef00000a547e98 us-west-1: TEST: ami-6bbde00000e327a34 PROD: ami-7abed00000d597f83
Once we’ve defined the map, we can use a function to find the required value from within the map. The function accepts a series of arguments to find the required value within the map hierarchy. Let’s update the same section above to use the map to find the image ID instead of a parameter. We’ll use a reference to dynamically find the current region, assuming we have a parameter that specifies the environment (Env).
… MyEC2Instance: Type: AWS::EC2::Instance Properties: AvailabilityZone: !Ref AvailabilityZone InstanceType: !Ref InstanceType ImageId: !FindInMap [EnvAMI, !Ref "AWS::Region", !Ref Env] SecurityGroups: - !Ref SSHSecurityGroup - !Ref ServerSecurityGroup
You might encounter a situation where you also want to deploy slightly different resources depending on your environment. For example, your production environment may need additional resources to ensure uptime or support real-time backups. We can still use the same template in this case, and we can use the Conditions section to inform the template whether or not to deploy those additional resources. We’ll use the Equals function and use it to compare a parameter against a defined value.
Conditions: DeployProdResources: !Equals [ !Ref EnvType, prod ]
If we want, we can add a new EBS volume to our EC2 instance in our production environment to capture regular backups or log files using the additional Resource description below. We can also use the Conditions value to let CloudFormation know whether or not to deploy the resource. We’ll use the !GetAtt function to dynamically include the AZ of the EC2 instance.
MyNewVolume: Type: AWS::EC2::Volume Condition: DeployProdResources Properties: Size: 1 AvailabilityZone: !GetAtt EC2Instance.AvailabilityZone MountPoint: Type: AWS::EC2::VolumeAttachment Condition: DeployProdResources Properties: InstanceId: !Ref MyEC2Instance VolumeId: !Ref MyNewVolume Device: /dev/sdh
Once the CloudFormation stack is created, we can edit the template to change the resources it created and manages. If we want to increase the number of EC2 instances from one to three, we can edit the template and execute it to modify the stack. If we no longer need the application, we can delete the CloudFormation stack, which will delete the resources that it created.
In an enterprise environment, CloudFormation templates are likely to be more complex, although many templates will contain common components that can be simplified using nested stacks. Nested stacks are a basic best practice and allow you to reference common components from child stacks. Nested stacks can themselves be nested, and the beauty of this approach, in addition to the use of common code, is the ability to update a common component and have the update applied to all templates that use that component.
CloudFormation security basic best practices
1. Enforce least-privilege access with right-sized IAM policies
Managing access within your environment requires a careful balance between giving engineers enough access to do their jobs but not so much that they have unfettered access to all systems. This is known as the least privilege principle. AWS Identity and Access Management (IAM) is the service that controls access to AWS services and APIs, including CloudFormation. AWS has the concept of users and roles, which are used to define specific permissions associated with the account.
Roles are different from users in that they are designed to be assumed by users to provide specific permissions. A role may grant permission to create and delete security groups, and a user may assume this role temporarily if the user needs to delete a security group. Because CloudFormation is responsible for creating, modifying, and deleting infrastructure, it requires permissions through an IAM role to do its job.
By default, CloudFormation creates a temporary session using the credentials of the person executing the script; however, this may grant the template far more permissions than required and is not advisable. Another CloudFormation basic best practice is to create a service role and enable users with just enough access to create the CloudFormation template, upload templates to S3, and assign a user role to a service. A user account may have a policy (such as the one shown below) that grants them exactly that level of access.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "cloudformation:*", "s3:*", "iam:PassRole" ], "Resource": "*" } ] }
We can then create a service role for the CloudFormation service and attach managed policies to grant the appropriate permissions. Using the example referenced above, this role will grant access to the service to allow it to create resources such as EC2 and retrieve values from the SSM Parameter Store.
When a user uploads or creates a new template, they can assign the role using the ARN of the service role on the stack options screen.
2. Avoid exposing secrets
A huge benefit of using CloudFormation templates is that you can store them in a code repository alongside the code for the application or service they describe. However, if you need to specify credentials such as usernames, passwords, or other secret information as part of the CloudFormation script, you don’t want it checked in along with your code.
AWS has several secrets management services such as AWS Systems Manager Parameter Store and the AWS Secrets Manager that you can use to store these credentials and provide them to authorized services as needed. You can add dynamic references to your CloudFormation template that reference these secrets and use them securely when executing the template. This action is accomplished with the resolve keyword, followed by the service name and reference key to the required secret. It’s important to note that if you update the underlying secret, the change will not be automatically applied to the resources created by CloudFormation. You will need to update your template with the new reference key and update the stack to begin using the new secret.
'{{resolve:secretsmanager:MySecret:SecretString:password:EXAMPLE1-90ab-cdef-fedc-ba987EXAMPLE}}'
3. Enable logging
When you execute a CloudFormation template, the service initiates calls to service APIs within the AWS ecosystem to provision the described resources. Another CloudFormation security basic best practice is to log these interactions to help with security audits and anomaly detection. AWS CloudTrail is a built-in service that does this for you, but you need to enable the service and specify a location to store the log files.
CloudTrail logs include:
- User information
- Critical information about the event
- Parameters used to trigger the event
You can capture CloudTrail logs from CloudFormation and direct them to an AWS S3 bucket for storage. Ideally, you want to secure this bucket to prevent tampering with the logs. You can create the trail in the console, or better yet, use CloudFormation to manage that as well. Below is a sample resource definition to create a trail. Additional parameters provide options like encrypting the log files.
MyCloudTrailS3Bucket: Type: AWS::S3::Bucket DeletionPolicy: Retain MyCloudTrail: Type: AWS::CloudTrail::Trail Properties: S3BucketName: !Ref MyCloudTrailS3Bucket IsLogging: true
4. Scan for misconfigurations
In addition to the basic CloudFormation best practices outlined above, a secure CloudFormation strategy should follow all AWS security and compliance basic best practices. In addition to implementing secure access control, secrets management, and enabling logging, you also need to keep in mind AWS security basics. With hundreds of security basic best practices, keeping all resources secure—whether they’re managed in AWS or CloudFormation—is by no means an easy feat.
CloudFormation makes programmatically maintaining security basic best practices easier because of its machine-readability, which allows for automated scanning alongside other software tests in the build and test stages of the development lifecycle. Whether you use an AWS service such as GuardDuty, an open-source tool such as Checkov, or a commercial tool such as Bridgecrew, ensuring that your CloudFormation templates aren’t violating important security and compliance policies is a must.
···
Following these practices will set you up with a great security foundation as you tap into the power of CloudFormation. Stay tuned for our next post, which will explore more advanced and customized practices that you can implement to further protect your AWS environment from misuse and mistakes.