Secure the PipelineIaCStep 15 of 31

Catch AWS Misconfigurations Before Apply (Checkov)

difficulty intermediatehands-on 25 min hands-on

48% complete

prereqs · terraform-basics

concepts · IaC scanning · policy as code · CIS benchmark mapping · soft-fail gating

Every finding a posture scanner reports about your AWS account started as a line of Terraform somebody merged. Prowler tells you the bucket is public; Checkov tells you before it exists, in the pull request, while it is still one line to change.

The economics are the entire argument. Fixing a public bucket after apply means a change request, a deploy window, and an explanation. Fixing it before means a review comment.

Verified against Checkov 3.2.533, scanning the Range Terraform. Every number below is copied from a real terminal.

Step 1 — Install and scan

bash
pipx install checkov          # or: brew install checkov
checkov --version
bash
git clone https://github.com/jaybilgaye/aiopsone-range
cd aiopsone-range

checkov -d terraform/ --compact --quiet
text
Passed checks: 13, Failed checks: 26, Skipped checks: 0

26 failures, no AWS account, no credentials, nothing applied. The scan reads the HCL and evaluates policy against it. That is why it belongs in a pre-merge gate rather than a nightly job.

--compact drops the code blocks from the output; --quiet hides passed checks. Without both you get several hundred lines and stop reading.

Step 2 — Read the findings in groups, not as a list

26 findings is already past the point where people skim. They cluster into four causes.

The public bucket — four checks on one resource:

text
CKV_AWS_53: "Ensure S3 bucket has block public ACLS enabled"
CKV_AWS_54: "Ensure S3 bucket has block public policy enabled"
CKV_AWS_55: "Ensure S3 bucket has ignore public ACLs enabled"
CKV_AWS_56: "Ensure S3 bucket has 'restrict_public_buckets' enabled"
     FAILED for resource: aws_s3_bucket_public_access_block.public_data
CKV_AWS_70: "Ensure S3 bucket does not allow an action with any Principal"
     FAILED for resource: aws_s3_bucket_policy.public_read

Those first four are one decision — the Range sets all four Block Public Access flags to false — reported as four findings because the CIS benchmark treats them as four controls. Fixing the block fixes all four.

The security group:

text
CKV_AWS_24:  ingress from 0.0.0.0/0 to port 22
CKV_AWS_25:  ingress from 0.0.0.0/0 to port 3389
CKV_AWS_382: egress to 0.0.0.0/0
CKV_AWS_23:  every security group and rule has a description

The IAM role:

text
CKV_AWS_62:  IAM policies that allow full "*-*" administrative privileges
CKV_AWS_355: "*" as a statement's resource for restrictable actions
CKV_AWS_288: IAM policies does not allow data exfiltration

CKV_AWS_288 is the one worth pausing on. It is not complaining about the wildcard as such — it is saying this policy permits actions that move data out, which is the difference between a badly-scoped role and an exfiltration path. That distinction is what makes an IAM finding worth escalating.

13 checks passed, and that number is not reassurance. Checkov only evaluates resources you declared. The Range has no KMS key, so no KMS check ran; no RDS instance, so no RDS check ran. A high pass rate on a small configuration means very little.

Terminal comparing Checkov's 26 failures against trivy config's 11 on the same Terraform

Step 3 — The findings Checkov cannot give you

The Range has no aws_cloudtrail resource at all. CloudTrail is absent, which is a serious finding in any real account — and Checkov says nothing, because there is no resource to attach a policy to.

This is structural, not a gap in the rule set. IaC scanning evaluates what you wrote. It cannot evaluate what you did not write, and it cannot see anything created by hand in the console, by another team's stack, or by a module you did not scan.

That is the whole reason the RUN chapter exists. Checkov and a posture scanner like Prowler are not competing tools that overlap — they answer different questions:

Sees Blind to
Checkov what your Terraform declares anything absent, anything created outside IaC
Prowler what actually exists in the account nothing — but only after it exists

A team running only IaC scanning has no idea what is in its account. A team running only posture scanning finds everything too late. You want both, and they catch different things.

Step 4 — Gate it, without failing the build on day one

The naive gate fails any pull request with any finding. On an existing codebase that means a red build on day one, and the gate removed by Friday.

yaml
# .github/workflows/iac.yml
name: iac
on: [pull_request]

permissions:
  contents: read

jobs:
  checkov:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: bridgecrewio/checkov-action@master
        with:
          directory: terraform/
          soft_fail_on: LOW,MEDIUM      # report, do not block
          framework: terraform

soft_fail_on reports lower-severity findings without failing the run, so HIGH and CRITICAL block the merge and the rest become a backlog. That gets you an enforced gate immediately rather than a perfect one eventually.

Step 5 — Skip a check honestly

Sometimes a finding is genuinely wrong for your context — a bucket that is public because it serves a static website:

text
resource "aws_s3_bucket" "public_site" {
  # checkov:skip=CKV_AWS_20:Static site bucket, public by design. Reviewed 2026-08-14.
  bucket = "my-public-site"
}

Inline, scoped to one check on one resource, with a reason and a date. Never a repository-wide skip list — that is how a scanner quietly stops covering the thing it was bought for, and it is precisely what an auditor will ask to see.

Stretch: compare against the same Terraform

Run trivy config over terraform/ and diff the findings. Different tools, same input, different results — and the reasons why are more instructive than either list on its own.

What you learned

  • 26 failures with no AWS account and nothing applied. IaC scanning is cheap enough to gate every pull request.
  • Findings cluster by cause, not by count. Four Block Public Access failures are one decision; fix the cause, not the list.
  • Checkov cannot report a missing CloudTrail, because there is no resource to attach a rule to. IaC scanning is blind to absence and to anything created outside your code — which is why posture scanning exists.
  • A passing check only means a declared resource passed. High pass rates on small configurations mean nothing.
  • soft_fail_on: LOW,MEDIUM gets you an enforced gate today instead of a perfect gate never.
  • Skip inline, by check id, with a reason and a date.