Securing AWS Environments Using Preventive Controls

KangZheng Li
5 min readJul 23, 2023

In this article, I will discuss using Checkov, a Policy-as-Code tool, to help validate AWS deployments through Terraform.

What is Policy-as-Code?

Policy-as-Code (PaC) is a preventive control that can prevent non-compliant deployments into your environment. PaC helps to validate your Infrastructure-as-Code (IaC) templates, running checks against defined best practices and requirements before resources are deployed.

There are various PaC options, ranging from open-sourced options such as Checkov, AWS CloudFormation Guard and Open Policy Agent (OPA) to commercial versions such as Terraform Sentinel. This article will focus on the use of Checkov.

An important thing to note is since PaC is a form of preventive control, it is only effective for validating new or updates to your environment. Existing resources already in your environment will not be checked by PaC tools.

What is Checkov?

Checkov, as a PaC supports numerous Cloud providers and IaC options. In this article, we will be focusing on AWS using Terraform. Out of the box, Checkov provides more than 300 checks for you to start with. You can find the full list of checks here. This provided baseline is especially helpful for organisations looking into free PaC tools without needing a team of security engineers to establish the baseline.

Using Checkov on AWS Terraform templates

Do note that there are 2 methods of running Checkov checks against your AWS templates. The first is static code analysis, which scans existing Terraform templates in your directory. This other is a scan against a Terraform Plan JSON output.

I would not recommend the former method, especially in projects where your Terraform uses Terraform modules instead of resource blocks. Since Checkov only scans at your repository level, it does not have complete visibility of what resources are being created by the modules. There is no assurance that every resource part of your Terraform deployment has been validated.

I prefer the latter approach, where we first generate a Terraform Plan file and save it in a JSON format. A Terraform plan unpacks all the modules and generates a state file that lists out all the resources that are part of your Terraform deployment. To generate the state file and run it against Checkov, use the following command.

terraform init
terraform plan -out tfplan
terraform show -json tfplan | jq '.' > tfplan.json

Example

In this example, let’s look at the following sample code that creates an S3 bucket.

resource "aws_s3_bucket" "bucket" {
bucket = "likz_test_bucket"
}

If we run the following command given at the top, we will end up with a JSON file that looks like this.

{
"format_version": "1.2",
"terraform_version": "1.5.2",
"planned_values": {
"root_module": {
"resources": [
{
"address": "aws_s3_bucket.bucket",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "bucket",
"provider_name": "registry.terraform.io/hashicorp/aws",
"schema_version": 0,
"values": {
"bucket": "likz_test_bucket",
"force_destroy": false,
"tags": null,
"timeouts": null
},
"sensitive_values": {
"cors_rule": [],
"grant": [],
"lifecycle_rule": [],
"logging": [],
"object_lock_configuration": [],
"replication_configuration": [],
"server_side_encryption_configuration": [],
"tags_all": {},
"versioning": [],
"website": []
}
}
]
}
},
"resource_changes": [
{
"address": "aws_s3_bucket.bucket",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "bucket",
"provider_name": "registry.terraform.io/hashicorp/aws",
"change": {
"actions": [
"create"
],
"before": null,
"after": {
"bucket": "likz_test_bucket",
"force_destroy": false,
"tags": null,
"timeouts": null
},
"after_unknown": {
"acceleration_status": true,
"acl": true,
"arn": true,
"bucket_domain_name": true,
"bucket_prefix": true,
"bucket_regional_domain_name": true,
"cors_rule": true,
"grant": true,
"hosted_zone_id": true,
"id": true,
"lifecycle_rule": true,
"logging": true,
"object_lock_configuration": true,
"object_lock_enabled": true,
"policy": true,
"region": true,
"replication_configuration": true,
"request_payer": true,
"server_side_encryption_configuration": true,
"tags_all": true,
"versioning": true,
"website": true,
"website_domain": true,
"website_endpoint": true
},
"before_sensitive": false,
"after_sensitive": {
"cors_rule": [],
"grant": [],
"lifecycle_rule": [],
"logging": [],
"object_lock_configuration": [],
"replication_configuration": [],
"server_side_encryption_configuration": [],
"tags_all": {},
"versioning": [],
"website": []
}
}
}
],
"configuration": {
"provider_config": {
"aws": {
"name": "aws",
"full_name": "registry.terraform.io/hashicorp/aws",
"expressions": {
"region": {
"constant_value": "ap-southeast-1"
}
}
}
},
"root_module": {
"resources": [
{
"address": "aws_s3_bucket.bucket",
"mode": "managed",
"type": "aws_s3_bucket",
"name": "bucket",
"provider_config_key": "aws",
"expressions": {
"bucket": {
"constant_value": "likz_test_bucket"
}
},
"schema_version": 0
}
]
}
},
"timestamp": "2023-07-23T14:35:34Z"
}

If we do a scan now using checkov -f tfplan.json , we will get a results similar to the one below.

Checkov already has 11 defaultchecks against S3 resources based on the screenshot above. Of the 11 checks, only 4 passed, and 7 failed. Let’s look at one of the failed ones.

Here, the bucket failed the compliance check because it did not enable Versioning. Let’s fix that by updating our Terraform code.


resource "aws_s3_bucket" "bucket" {
bucket = "likz_test_bucket"

versioning {
enabled = true
}
}

Once it’s updated, we can see that we now have 5 passed checks.

For the rest of the failed checks, we can do a couple of things. By default, Checkov applies a hard-fail for failed checks, meaning the output of Checkov will throw an error if any tests fail. We can enable soft-fail instead if we do not want Checkov to fail but still want to know what tests are failing. Lastly, we can do a skip-check, which skips the checks altogether.

In this case, let’s run a skip-check against all the other failed checks.

checkov -f tfplan.json --skip-check CKV2_AWS_61,CKV2_AWS_62,CKV2_AWS_6,CKV_AWS_145,CKV_AWS_144,CKV_AWS_18

When we run Checkov again, we get the following result.

Conclusion

In this article, we discussed the benefits of Policy-as-Code, and explored some basic usage of Checkov to secure our Terraform templates. We made use of Checkov’s default policy to run checks against our templates and looked at the different actions that we could make depending on the results of the failure.

If you like this article, please give me a “clap”. I will be following up this article with another one talking about Checkov custom policies. Thank you!

--

--