This chapter will walk you through the concept of policy-as-code and how it can be helpful in terms of security and compliance. You will also learn the stage of CI/CD at which infrastructure policies (which is not only limited to infrastructure resources but also network access control) can be checked. After that, we will learn how to apply some policies to an AWS CloudFormation template using CloudFormation Guard. We will also learn how to use AWS Service Catalog across multiple development teams to spin up compliant resources. Then, we will learn how to integrate Terraform Cloud with GitHub. Finally, we will write some HashiCorp Sentinel policies to apply to Terraform templates to enforce the rules before Terraform spins up any cloud resources.
In this chapter, we are going to cover the following main topics:
To get started, you will need an AWS account and a Terraform Cloud account (there is a 30-day free trial). The code examples can be found in this book's GitHub repository chapter-02:
https://github.com/PacktPublishing/Accelerating-DevSecOps-on-AWS/tree/main/chapter-02
In this section, we will learn what policy as code is and how it helps an organization to govern and enforce best practices when we spin up resources in a secure and compliant way. We will also learn where to place policy checks in the CI/CD pipeline.
A policy is a set of rules or a plan related to particular situations. It is a way to enforce certain rules and constraints that restrict unauthorized access to resources such as services and environments. There are three different types of policies:
Policy as code basically means expressing these rules or plans in a high-level language to automate and manage the policies. It is a similar concept to infrastructure as code (IaC), where we write infrastructure resources as code. Some of the well-known policy-as-code tools available in the market are HashiCorp Sentinel, Open Policy Agent (OPA), and CloudFormation Guard.
Policy as code is a extension of DevSecOps, which embraces the shift-left culture. DevSecOps is basically the practice of implementing security into each DevOps stage and activity. If we implement security at every stage, then we can identify and fix issues earlier, which helps save time, effort, and money. It's called shift-left because we discover issues earlier (to the left) in the timeline.
Policy as code provides the following benefits:
An important use case for policy as code is to integrate it in your CI/CD plan. There are multiple ways to include it, which we will discuss in the next section.
There are multiple ways to strategize policy as code as part of your CI/CD plan. Let's look at some of these in the following figures:
Example 1:
In the preceding figure, the following steps are illustrated:
This is one way of using policy as code in CI/CD, but there is one gotcha, and you must be wondering what that is? It is that the pre-approved catalog code will be locked and you won't be able to change the variables in the template. For example, if the developers want to use the template but want to change a configuration slightly (such as instance type), then it won't be permitted. Otherwise, the developers can use any higher VM instance type, which might not be allowed in the organization because of high cost. But it also restricts developers, with an organizational policy, to modify configurations based on their needs. To allow the developers to modify the configuration, we can implement the CI/CD plan in the following way.
Example 2:
In the preceding image, we are trying to give the developers the power to spin up infrastructure resources, which saves time by cutting out the ticketing process of infrastructure provisioning, for the operation stream. But the aforementioned CI/CD pipeline also makes sure that if developers pass any value in the infrastructure code that violates the policy, then the pipeline will fail. In the preceding diagram, the following steps are illustrated:
The policy as code not only applies to the infrastructure code, but it could also apply to Kubernetes definition or access control. There are some amazing policy engine tools available that support multiple platforms (such as AWS, Terraform, and Kubernetes) such as OPA, Checkov, and CloudFormation Guard. In the next section, we will explore CloudFormation Guard 2.0, which supports CloudFormation templates, Terraform, and Kubernetes.
In this section, we will learn about CloudFormation Guard, including how to install it, how to write rules for it, and how to validate it against CloudFormation templates.
CloudFormation Guard is an open source policy-as-code validation tool. It supports infrastructure configuration tools such as CloudFormation templates and Terraform JSON configuration files. It also has additional support to write rules for Kubernetes configurations. It enables developers to use a simple yet powerful domain-specific language (DSL) to write rules and validate JSON- or YAML-formatted structured data.
To install CloudFormation Guard 2.0 (the latest major version), please follow these steps:
$ curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh | sh
$ sudo cp ~/.guard/bin/cfn-guard /usr/local/bin
$ cfn-guard help
If you get a description of cfn-guard 2.0 and usage subcommands, that means you have installed cfn-guard.
To validate your CloudFormation template with Guard rulesets, you use the following command:
$ cfn-guard validate –d your-cloudformation-template –r guard-rule-file
The -d (or ––data) is for the CloudFormation template and the –r (or ––rules) is for the CloudFormation Guard rules.
Guard rules are used to check if resources' configurations comply with organizational policies or expectations. Guard rules are written in a DSL, and it's easy to understand. You don't need programming experience to understand or write the rules. The basic format of a Guard rule is illustrated as follows:
Let's explore this with an example. We have a CFT file, cfntemp.yaml, for an Amazon EBS volume, which can be seen as follows:
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Sample template
Resources:
SampleVolume:
Type: AWS::EC2::Volume
Properties:
AvailabilityZone: us-west-2b
Size: 10
VolumeType: gp2
Now, suppose we want to validate that the EBS volume resources in the preceding template meet the following criteria: the encryption property is set to true; the size of the volume is 50 or above; the AvailabilityZone property's value is ap-southeast-1a. To do this, you will create a ruleset, shown as follows in the cfntestpolicy file:
AWS::EC2::Volume {
Properties {
Encrypted == true
Size >= 50
VolumeType == 'gp2'
AvailabilityZone == 'ap-southeast-1a'
}
}
To check whether the CFT file for the EBS volume is compliant with the policy, we need to run this command:
$ #cfn-guard validate –d <CloudFormationTemplate> -r <cfn-guard-rulesetfile>
$ cfn-guard validate –d cfntemp.yaml -r cfntestpolicy
cfntest.yaml Status = FAIL
FAILED rules
cfntestrule/default FAIL
---
Evaluation of rules cfntestrule against data cfntest.yaml
--
Property traversed until [/Resources/SampleVolume/Properties] in data [cfntest.yaml] is not compliant with [cfntestrule/default] due to retrieval error. Error Message [Attempting to retrieve array index or key from map at path = /Resources/SampleVolume/Properties , Type was not an array/object map, Remaining Query = Encrypted]
Property [/Resources/SampleVolume/Properties/Size] in data [cfntest.yaml] is not compliant with [cfntestrule/default] because provided value [10] did not match expected value [50]. Error Message []
Property [/Resources/SampleVolume/Properties/AvailabilityZone] in data [cfntest.yaml] is not compliant with [cfntestrule/default] because provided value ["us-west-2b"] did not match expected value ["ap-southeast-1a"]. Error Message []
So, the result we can see in the preceding code block is FAIL, and the reason is also mentioned in the preceding code block. If the result is FAIL, that means the infrastructure code is not compliant. Now, to make it compliant, we need to fix the preceding CloudFormation template. We will enable the encryption, increase the volume size, and also change the availability zone. The changes are as follows:
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Sample template
Resources:
SampleVolume:
Type: AWS::EC2::Volume
Properties:
Encrypted: true
AvailabilityZone: ap-southeast-1a
Size: 50
VolumeType: gp2
Now, to check whether the preceding CFT file is compliant with the Guard rule, we again need to run the cfn-guard validate command:
$ cfn-guard validate -d cfntest.yaml -r cfntestrule
cfntest.yaml Status = PASS
PASS rules
cfntestrule/default PASS
---
Evaluation of rules cfntestrule against data cfntest.yaml
--
Rule [cfntestrule/default] is compliant for template [cfntest.yaml]
--
We can see the result is PASS, and that means the CFT file is compliant with the Guard rule. This one was a simple example. Let's see another example with a different scenario that covers additional rule formats. We will use a sample template comex.yaml file that describes an Amazon S3 bucket with server-side encryption and versioning enabled. It also describes an EBS volume with encryption set to true and AvailabilityZone set to ap-southeast-1, which is as follows:
---
AWSTemplateFormatVersion: '2010-09-09'
Description: Sample template
Resources:
SampleBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketName: !Sub 'sample-bucket-${AWS::Region}-${AWS::AccountId}'
VersioningConfiguration:
Status: Enabled
SampleVolume:
Type: AWS::EC2::Volume
Properties:
AvailabilityZone: ap-southeast-1a
Encrypted: true
Size: 10
VolumeType: gp2
Now, let's try to create a Guard rule file, comexrule.guard, for the preceding template:
let volumes = Resources.*[ Type == 'AWS::EC2::Volume' ] 1
rule sample_volume_check when %volumes !empty { 2
%volumes.Properties.Encrypted == true 3
%volumes.Properties.AvailabilityZone in ['ap-southeast-1a', 'ap-southeast-1b'] 4
}
let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]
rule sample_bucket_encryption when %buckets !empty {
%buckets.Properties {
BucketEncryption.ServerSideEncryptionConfiguration[*] {
ServerSideEncryptionByDefault.SSEAlgorithm == 'AES256'
}
}
}
You must be wondering why the rules written for this example are more complex than the first example. Let's try to understand this rule file based on the highlighted numbers in the preceding code example:
Resources:
SampleBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
BucketName: !Sub 'sample-bucket-1'
VersioningConfiguration:
Status: Enabled
SampleVolume:
Type: AWS::EC2::Volume
Properties:
AvailabilityZone: ap-southeast-1a
Encrypted: true
Size: 10
VolumeType: gp2
But, in our case, we are using the Resources.*[ Type == 'AWS::EC2::Volume' ] filter, so the query output will be as follows:
Type: AWS::EC2::Volume
Properties:
AvailabilityZone: ap-southeast-1a
Encrypted: true
Size: 10
VolumeType: gp2
Now, this output value will be assigned to a variable when we are using let volumes =. So, in this case, volumes is a variable that includes the properties of AWS::EC2::Volume.
rule <rule name> [when <condition>] {
Guard_rule_1
Guard_rule_2
...
}
We have created a rule named sample_volume_check, and this rule block will be executed only when the condition is met, meaning, when %volumes (the way to call the assigned variable) is !empty (NOT operator). In this case, the condition is met, because the value of volumes is not empty.
If we validate the CFT file against the Guard rule, we will get the status that it is compliant with the Guard rule:
$ cfn-guard validate -d comex.yaml -r comexrule
comex.yaml Status = PASS
PASS rules
comex/sample_volume_check PASS
comex/sample_bucket_encryption PASS
---
Evaluation of rules comex against data comex.yaml
--
Rule [comex/sample_bucket_encryption] is compliant for template [comex.yaml]
Rule [comex/sample_volume_check] is compliant for template [comex.yaml]
--
The previous two examples give you an overview of how to write a Guard rule. Now, let's try to write a Guard rule for another scenario.
We need to write a Guard rule for a CFT file, which enforces the following conditions in an Amazon EC2 instance:
The ruleset that covers all of the preceding conditions is as follows:
let volumes = Resources.*[ Type == 'AWS::EC2::Volume' ]
rule VOLUME_CHECK when %volumes !empty {
%volumes.Properties {
Encrypted == true
Size in [50,100,120]
VolumeType == 'gp2'
AvailabilityZone in ['ap-southeast-1a']
}
}
let sg = Resources.*[ Type == 'AWS::EC2::SecurityGroup' ]
rule SECURITYGROUP_CHECK when %sg !empty {
%sg.Properties {
SecurityGroupIngress[*]{
CidrIp != '0.0.0.0/0'
}
}
}
let ec2_instance = Resources.*[ Type == 'AWS::EC2::Instance' ]
let ec2_instance_dev = %ec2_instance [ Properties.Tags[1].Value == 'Dev' ]
let ec2_instance_prod = %ec2_instance [ Properties.Tags[1].Value == 'Prod' ]
rule EC2INSTANCE_DEV_CHECK {
when %ec2_instance_dev !empty {
%ec2_instance.Properties.InstanceType == 't2.micro'
%ec2_instance.Properties.AvailabilityZone == 'ap-southeast-1a'
}
}
rule EC2INSTANCE_PROD_CHECK {
when %ec2_instance_prod !empty {
%ec2_instance.Properties.InstanceType == 't2.xlarge'
%ec2_instance.Properties.AvailabilityZone == 'ap-southeast-1a'
}
}
Now, we will test this rule on the CFT file. The commands to validate the CFT file against the Guard rule are as follows:
$ git clone https://github.com/PacktPublishing/Modern-CI-CD-on-AWS.git
$ cd chapter-02
$ cfn-guard validate -d CFT-ec2instance.yaml -r cfn-ruleset
CFT-ec2instance.yaml Status = FAIL
SKIP rules
cfn-ruleset/EC2INSTANCE_DEV_CHECK SKIP
PASS rules
cfn-ruleset/VOLUME_CHECK PASS
cfn-ruleset/EC2INSTANCE_PROD_CHECK PASS
FAILED rules
cfn-ruleset/SECURITYGROUP_CHECK FAIL
---
Evaluation of rules cfn-ruleset against data CFT-ec2instance.yaml
--
Property [/Resources/InstanceSecurityGroup/Properties/SecurityGroupIngress/0/CidrIp] in data [CFT-ec2instance.yaml] is not compliant with [cfn-ruleset/SECURITYGROUP_CHECK] because provided value ["0.0.0.0/0"] did match expected value ["0.0.0.0/0"]. Error Message []
--
Rule [cfn-ruleset/EC2INSTANCE_PROD_CHECK] is compliant for template [CFT-ec2instance.yaml]
Rule [cfn-ruleset/VOLUME_CHECK] is compliant for template [CFT-ec2instance.yaml]
--
Rule [cfn-ruleset/EC2INSTANCE_DEV_CHECK] is not applicable for template [CFT-ec2instance.yaml]
We can see that EC2INSTANCE_DEV_CHECK has been skipped because the EC2 instance is tagged with Prod. Also, SECURITYGROYP_CHECK has failed, because inside the CFT template, the security group port 22 is open to 0.0.0.0/0.
The next question you might ask yourself is how will we use the cloudformation guard checkin automation? The answer is the exit status. If the Guard rule execution result is PASS, then the exit status is 0, and if it is FAIL, then the exit status is 5.
Note
You can read the blog at https://aws.amazon.com/blogs/devops/integrating-aws-cloudformation-guard/ to learn how to use Guard rules in the AWS Developers tools. Keep one thing in mind: the blog or the repository link mentioned in the blog is using CloudFormation Guard 1.0 instead of 2.0. You need to change the Guard rule from v1.0 to v2.0, as well as the Buildspec.yaml file used in CodeBuild.
So, in this section, we learned how to check a CloudFormation template compliance status using CloudFormation Guard rules. We can imbed this process in an automation pipeline very easily. If you want to provide a compliant CloudFormation template via user interface to developers for their own use, we can do that using AWS Service Catalog, which we will cover in the next section.
In this section, we will explore how to use AWS Service Catalog to give different teams access to resources. We will learn how to enforce rules using constraints, and we will find out how to manage access with access controls.
AWS Service Catalog is a service managed by AWS that allows organizations to provision and manage pre-approved catalogs of IT services. IT services include any AWS resources, such as servers, databases, software, and more. This service allows IT administrators/DevOps teams to create a service catalog and allow other teams to access it from a central portal. This way, IT administrators or DevOps teams ensure that other teams are provisioning compliant infrastructure resources. Some of the terminology used in Service Catalog is explained in the following subsections.
AWS Service Catalog supports two types of users:
A product is an IT service that is a set of AWS resources, such as Amazon EC2 instances, Amazon RDS instances, monitoring configurations, or networking components. For example, a CloudFormation template of LAMP stack can be considered a product.
A portfolio is a collection of products that contains configuration information. Portfolios help manage who can use specific products and how they can use them. With Service Catalog, you can create a customized portfolio for each type of user in your organization and selectively grant them access to the appropriate portfolio.
Constraints control the way the user deploys the product. They allow governance over the products. For example, if the user's environment is set to Dev, then the EC2 instance type must be t2.medium.
The workflow for a catalog administrator is illustrated in the following figure:
The workflow for an end user is illustrated in the following figure:
This is a basic overview of AWS Service Catalog. Now, we will explore a scenario to understand the AWS Service Catalog in action:
We will act as a catalog administrator, create a product and organize it in a portfolio, and then apply some constraints and access controls. After that, we will log in as an end user and try to access the product. We will enter noncompliant parameters in the service catalog page and check whether it stops us from provisioning the product. The steps to implement the scenario are as follows:
{
"Rules": {
"Rule1": {
"Assertions": [
{
"Assert": {
"Fn::Contains": [
[
"t2.medium",
"m3.medium"
],
{
"Ref": "InstanceType"
}
]
},
"AssertDescription": "Instance type should be either t2.micro or m3.medium"
}
]
}
}
}
This way, we can enforce the end user to only provision the compliant resources..
In the next section, we will learn about Terraform Cloud and HashiCorp Sentinel. There is no doubt that Terraform is heavily used by developers and DevOps. So, policy as code in Terraform will be covered in the next section.
In this section, we will dive deep into Terraform Cloud and how we can integrate it with GitHub.
Terraform is an IaC tool available from HashiCorp. Terraform lets you define infrastructure resources as human-readable and declarative configuration files. Terraform supports multiple cloud platforms and comes with lots of provider plugins. It also maintains a state file to track resource changes. Terraform comes in three different editions – Terraform OSS, Terraform Cloud, and Terraform Enterprise. Terraform OSS is free and comes with basic features. Terraform Cloud has free and paid versions. And Terraform Enterprise is a paid service with additional features.
A chart showing the differences between the Terraform versions can be seen in Table 2.1:
Terraform Cloud is a SaaS platform that manages Terraform executions in a reliable environment instead of a local machine. It basically stores all necessary information (such as secrets and shared state files) and connects to a VCS so that a team can collaborate. Terraform executions in Terraform Cloud can take place in the following three ways:
The steps to integrate Terraform Cloud with GitHub are as follows:
In this section, we saw how to integrate a VCS (GitHub) with Terraform Cloud. In the next section, we will write some Terraform configurations for an AWS EC2 instance and run the configuration via Terraform Cloud.
In this section, we will write a Terraform configuration to spin up an EC2 instance in AWS and push that configuration to the repository that we configured in the previous section. We will also learn how to store AWS credentials Terraform Cloud. Follow the next steps to get started:
Up until now, we have seen how to run Terraform configurations in Terraform Cloud via a VCS. In the next section, we will learn how to enforce policies on Terraform template using Sentinel.
In this section, we will learn about HashiCorp Sentinel and how we can enable it in Terraform Cloud. After that, we will write a Sentinel policy to enforce rules on Terraform templates.
Sentinel is a framework for policy and language, built in software to enforce fine-grained, logic-based policy decisions. It is an enterprise feature of Terraform, Vault, Nomad, and Consul. Sentinel is easy to learn and needs minimal programming experience. Sentinel policies are written in a text file using the Sentinel language with the .sentinel file extension. The Sentinel language has a main function, whose value decides whether a policy passes or fails. Here's an example:
main = 9 > 3
When you execute this policy using a Sentinel command, the result will be true. Sentinel handles the result of the execution in levels known as an enforcement level. Sentinel has three enforcement levels:
Enforcement levels are defined in the sentinel.hcl file. Enforcement levels are also tied to the name of the policy file. For example, if a policy file is named restrict-ec2-tag.sentinel, then the content of the sentinel.hcl file will be like the following JSON example:
policy "restrict-ec2-tag" {
enforcement_level = "hard-mandatory"
}
By default, Sentinel is not enabled in Terraform Cloud, as it is only available in the Team and Governance package. To enable Sentinel in Terraform Cloud, follow these steps:
We can also integrate Sentinel policy checks into our CI/CD toolchain, but in that case, you would need to execute the Terraform commands using a remote backend. You can read more about integrating Sentinel with Jenkins at the following link: https://www.hashicorp.com/resources/securing-infrastructure-in-application-pipelines.
In this chapter, we learned how we can implement policy and governance as code. We showed where we can fit our policy-as-code checks in the CI/CD pipeline. We also learned how to write AWS CloudFormation Guard rules. We implemented access controls in AWS Service Catalog to share Service Catalog products with constraints. We also subscribed to Terraform Cloud to execute Terraform configuration and we applied policies using HashiCorp Sentinel. Now, you can integrate policy checks in your CI/CD stages. In the next chapter, we will learn how to spin up application containers smoothly with the AWS Proton service, and we'll use AWS Code Guru to review the application code.
3.133.157.142