Skip to main content
Cloud Security · lesson 1 of 8
Cloud Security intermediate Lesson 1 of 8

Also part of: DevSecOps (AppSec in the pipeline) Terraform

Scanning Terraform for Misconfiguration with Checkov

Catch a public S3 bucket and SSH open to the internet in Terraform before anything is deployed: run Checkov, gate on the checks that matter, fix, re-scan.

By SkillByExample · Published

cloud-security terraform checkov iac

Infrastructure code can be wrong in the same way application code can, and the cheapest place to find out is before terraform apply. Checkov reads .tf files and flags settings that would be insecure once deployed. It needs no cloud account, no credentials and no plan.

Tested with Checkov 3.3.29 in Docker (bridgecrew/checkov:3.3.29) on Docker Engine 29.8.2. To type checkov like the commands below, define an alias first. On Git Bash for Windows, also put MSYS_NO_PATHCONV=1 in front of docker so the /src path isn’t rewritten:

alias checkov='docker run --rm -v "$PWD:/src" -w /src bridgecrew/checkov:3.3.29'

pip install checkov==3.3.29 gives the same command without Docker.

Scan a file with two real problems

This Terraform has a bucket whose public access block is switched off, and a security group that allows SSH from anywhere:

# main.tf: a public S3 bucket and SSH open to the internet
resource "aws_s3_bucket" "reports" {
  bucket = "acme-reports"
}

resource "aws_s3_bucket_public_access_block" "reports" {
  bucket                  = aws_s3_bucket.reports.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

resource "aws_security_group" "admin" {
  name        = "admin"
  description = "Admin access"

  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}
$ checkov -f main.tf --compact --quiet | grep -E "^(Passed|Check)"
Passed checks: 9, Failed checks: 13, Skipped checks: 0
Check: CKV_AWS_56: "Ensure S3 bucket has 'restrict_public_buckets' enabled"
Check: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
Check: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLS enabled"
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
Check: CKV2_AWS_61: "Ensure that an S3 bucket has a lifecycle configuration"
Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
Check: CKV2_AWS_62: "Ensure S3 buckets should have event notifications enabled"
Check: CKV2_AWS_5: "Ensure that Security Groups are attached to another resource"
Check: CKV_AWS_144: "Ensure that S3 bucket has cross-region replication enabled"
Check: CKV2_AWS_6: "Ensure that S3 bucket has a Public Access block"
Check: CKV_AWS_145: "Ensure that S3 buckets are encrypted with KMS by default"

Thirteen failures from 25 lines, and only some of them are exposure. The four CKV_AWS_53 to CKV_AWS_56 checks are the four switched-off flags, CKV_AWS_24 is the SSH rule, and CKV2_AWS_6 fails even though a public access block is attached, because a block with every flag set to false blocks nothing. The rest (versioning, logging, replication, KMS) are hardening advice, not holes. The order of the list can change between runs, so compare by check id, not by line.

Gate on the checks that matter

--check limits the scan to a list of ids. Failing the build on these six, and reviewing the rest, keeps the gate meaningful:

$ checkov -f main.tf --compact --quiet --check CKV_AWS_24,CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56,CKV2_AWS_6
terraform scan results:

Passed checks: 0, Failed checks: 6, Skipped checks: 0

Check: CKV_AWS_56: "Ensure S3 bucket has 'restrict_public_buckets' enabled"
	FAILED for resource: aws_s3_bucket_public_access_block.reports
	File: /main.tf:6-12
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-22
Check: CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
	FAILED for resource: aws_s3_bucket_public_access_block.reports
	File: /main.tf:6-12
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-21
Check: CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
	FAILED for resource: aws_s3_bucket_public_access_block.reports
	File: /main.tf:6-12
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-20
Check: CKV_AWS_53: "Ensure S3 bucket has block public ACLS enabled"
	FAILED for resource: aws_s3_bucket_public_access_block.reports
	File: /main.tf:6-12
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/s3-policies/bc-aws-s3-19
Check: CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22"
	FAILED for resource: aws_security_group.admin
	File: /main.tf:14-25
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/networking-1-port-security
Check: CKV2_AWS_6: "Ensure that S3 bucket has a Public Access block"
	FAILED for resource: aws_s3_bucket.reports
	File: /main.tf:2-4
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-networking-policies/s3-bucket-should-have-public-access-blocks-defaults-to-false-if-the-public-access-block-is-not-attached

$ echo $?
1

Each finding names the resource and the line range in main.tf, so 6-12 is the access block and 14-25 is the security group. The exit code is 1, which is what a CI step needs to fail the pipeline.

Fix and re-scan

Turn the four flags on and narrow SSH to the VPN range:

# main.tf: public access blocked, SSH only from the internal network
resource "aws_s3_bucket" "reports" {
  bucket = "acme-reports"
}

resource "aws_s3_bucket_public_access_block" "reports" {
  bucket                  = aws_s3_bucket.reports.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_security_group" "admin" {
  name        = "admin"
  description = "Admin access"

  ingress {
    description = "SSH from the VPN range"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.8.0.0/16"]
  }
}
$ checkov -f main.tf --compact --quiet --check CKV_AWS_24,CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56,CKV2_AWS_6
terraform scan results:

Passed checks: 6, Failed checks: 0, Skipped checks: 0


$ echo $?
0

All six pass and the exit code is 0. Nothing was deployed and no credentials were used: the bucket and the security group never existed outside this file.

The full scan still has opinions:

$ checkov -f main.tf --compact --quiet | grep -E "^(Passed|Check)"
Passed checks: 15, Failed checks: 7, Skipped checks: 0
Check: CKV_AWS_18: "Ensure the S3 bucket has access logging enabled"
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"
Check: CKV2_AWS_61: "Ensure that an S3 bucket has a lifecycle configuration"
Check: CKV2_AWS_62: "Ensure S3 buckets should have event notifications enabled"
Check: CKV2_AWS_5: "Ensure that Security Groups are attached to another resource"
Check: CKV_AWS_144: "Ensure that S3 bucket has cross-region replication enabled"
Check: CKV_AWS_145: "Ensure that S3 buckets are encrypted with KMS by default"

Fixing the exposure took the count from 13 to 7 and the passes from 9 to 15. The remaining seven are decisions for the team, not emergencies. CKV2_AWS_5 only fails because this file never attaches the security group to an instance.

Accept a finding on purpose

When a check doesn’t apply, say so in the resource, with a reason a reviewer can read:

# main.tf: one finding accepted on purpose, with the reason in the code
resource "aws_s3_bucket" "reports" {
  #checkov:skip=CKV_AWS_144:Reports are rebuilt nightly, so no second region
  bucket = "acme-reports"
}

resource "aws_s3_bucket_public_access_block" "reports" {
  bucket                  = aws_s3_bucket.reports.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_security_group" "admin" {
  name        = "admin"
  description = "Admin access"

  ingress {
    description = "SSH from the VPN range"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.8.0.0/16"]
  }
}
$ checkov -f main.tf --compact --check CKV_AWS_144 | sed -n '/scan results/,$p'
terraform scan results:

Passed checks: 0, Failed checks: 0, Skipped checks: 1

Check: CKV_AWS_144: "Ensure that S3 bucket has cross-region replication enabled"
	SKIPPED for resource: aws_s3_bucket.reports
	Suppress comment: Reports are rebuilt nightly, so no second region
	File: /main.tf:2-5
	Guide: https://docs.prismacloud.io/en/enterprise-edition/policy-reference/aws-policies/aws-general-policies/ensure-that-s3-bucket-has-cross-region-replication-enabled

The check moves from failed to skipped, and the reason travels with it into every report. That beats a global --skip-check, which hides the check everywhere and records no reason. The sed only trims Checkov’s ASCII banner, which --quiet would hide along with the skipped check.

Practice

  1. In the fixed file, change the ingress rule to port 3389 from 0.0.0.0/0. Which check id fails now?
Solution

CKV_AWS_25, the RDP version of the SSH check:

$ checkov -f main.tf --compact --quiet | grep -E "^(Passed|Check: CKV_AWS_2)"
Passed checks: 14, Failed checks: 8, Skipped checks: 0
Check: CKV_AWS_25: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 3389"
Check: CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled"

Checkov has separate checks per well-known admin port, so a rule open to the internet on 22 or 3389 is caught by id, not by guessing.

  1. On the fixed file, drop CKV2_AWS_5 from the full scan without editing the Terraform. What does the summary line become?
Solution
$ checkov -f main.tf --compact --quiet --skip-check CKV2_AWS_5 | grep -E "^Passed"
Passed checks: 15, Failed checks: 6, Skipped checks: 0

--skip-check removes the check from the run entirely, so it isn’t even counted as skipped. That is why an inline skip with a reason is the better tool for one-off exceptions.

  1. Run the original main.tf with --check CKV_AWS_24 --soft-fail. What exit code do you get, and when is that useful?
Solution
$ checkov -f main.tf --compact --quiet --check CKV_AWS_24 --soft-fail | grep -E "^Passed"
Passed checks: 0, Failed checks: 1, Skipped checks: 0
$ echo ${PIPESTATUS[0]}
0

The failure is still reported, but Checkov exits 0. Use it while introducing a scanner to an existing codebase: report first, then switch to failing builds once the backlog is fixed.

  1. Write a custom YAML policy CKV2_ACME_1 that requires an owner tag on every aws_s3_bucket, and make the fixed file pass it.
Solution
metadata:
  id: "CKV2_ACME_1"
  name: "S3 buckets must have an owner tag"
  category: "GENERAL_SECURITY"
definition:
  cond_type: "attribute"
  resource_types:
    - "aws_s3_bucket"
  attribute: "tags.owner"
  operator: "exists"

Save it as policies/owner_tag.yaml. Without a tag the bucket fails:

$ checkov -f main.tf --compact --quiet --external-checks-dir policies --check CKV2_ACME_1
terraform scan results:

Passed checks: 0, Failed checks: 1, Skipped checks: 0

Check: CKV2_ACME_1: "S3 buckets must have an owner tag"
	FAILED for resource: aws_s3_bucket.reports
	File: /main.tf:2-4

Add tags = { owner = "data-team" } under bucket = "acme-reports" and it passes:

$ checkov -f main.tf --compact --quiet --external-checks-dir policies --check CKV2_ACME_1
terraform scan results:

Passed checks: 1, Failed checks: 0, Skipped checks: 0

Frequently Asked Questions

What is IaC misconfiguration scanning?
It is static analysis of infrastructure-as-code files such as Terraform, CloudFormation or Kubernetes YAML. A scanner like Checkov reads the resource definitions and flags settings that would be insecure once deployed, such as a storage bucket readable by anyone or a firewall rule open to the whole internet. It needs no cloud account and runs before terraform apply.
Does Checkov need cloud credentials or a terraform plan?
No. Checkov parses the .tf files directly, so it runs without credentials, providers or network access to the cloud. It can also scan the JSON output of terraform show -json when you want it to see values that are only known after planning, such as variables resolved from tfvars files.
Why does Checkov report so many failed checks on a simple file?
Its default policy set mixes real exposure checks with hardening and operational best practices, such as versioning, access logging and cross-region replication on every S3 bucket. Treat the full report as a review list, and fail the build only on the checks your team agrees are blocking, using --check, --skip-check or inline skip comments with a reason.
How do I suppress a Checkov finding?
Add a comment inside the resource in the form #checkov:skip=CHECK_ID:reason. Checkov then reports the check as SKIPPED and prints the reason, so the decision is visible in code review. The --skip-check flag suppresses a check across the whole scan instead, which is better suited to checks the team never wants.
Can I write my own Checkov policy?
Yes. A YAML policy names the resource types, the attribute to test and an operator such as exists or equals. Put it in a directory and pass --external-checks-dir. Checkov runs it alongside the built-in checks and reports it with the id you gave it, for example to require an owner tag on every bucket.