Checkov scans infrastructure as code before it becomes infrastructure. Terraform, CloudFormation, Kubernetes manifests, Helm charts, Dockerfiles, GitHub Actions workflows. It parses them, runs a few thousand policies, and tells you which resources are misconfigured.
The tool is easy. Adopting it without your team learning to ignore it is the actual problem.
First run
pip install checkov
checkov -d .
On any real repository this prints hundreds of findings. Do not fix them. Do not turn it on in CI yet. This first run exists to tell you the size of the problem, nothing more.
Scope it to what you care about:
checkov -d . --framework terraform --compact --quiet
--quiet drops passed checks, --compact drops the code blocks. The output becomes readable.
Baseline first, then enforce
The thing that makes Checkov adoptable is the baseline file. It records current findings as accepted, so CI fails only on new problems:
checkov -d . --create-baseline
git add .checkov.baseline && git commit -m "Add Checkov baseline"
Then in CI:
checkov -d . --baseline .checkov.baseline
Existing debt is captured, new debt is blocked. You can burn down the baseline on your own schedule, and (importantly) a developer who has never run Checkov does not get a hundred failures on their first PR.
Regenerate the baseline deliberately, never automatically. A CI job that refreshes the baseline is a CI job that approves every new finding.
Configuration in a file, not flags
Put settings in .checkov.yaml at the repo root so local runs and CI agree:
framework:
- terraform
- kubernetes
- dockerfile
skip-path:
- examples/
- .terraform/
- test/fixtures/
skip-check:
- CKV_AWS_18 # S3 access logging - handled centrally by the log archive account
- CKV2_AWS_38 # Route53 DNSSEC - not supported by our registrar
soft-fail-on:
- LOW
compact: true
quiet: true
Two things worth copying from that example. First, every skip-check has a comment explaining why. A bare list of check IDs becomes unmaintainable within months. Nobody knows whether a skip is still valid, so nobody ever removes one.
Second, soft-fail-on: LOW reports low-severity findings without failing the build. Fail on what you will actually act on.
Suppressions belong next to the code
Repo-wide skips are blunt. For a specific resource, suppress inline:
resource "aws_s3_bucket" "public_assets" {
# checkov:skip=CKV_AWS_20:Public by design - static assets fronted by CloudFront
bucket = "example-public-assets"
}
The reason after the second colon is mandatory in practice even though the parser tolerates omitting it. It shows up in reports and in code review, which is exactly where someone can challenge it.
This is strictly better than a global skip: it applies to one resource, it is visible in the diff that introduced it, and it disappears when the resource does.
Wire it into CI
name: iac-scan
on: [pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: bridgecrewio/checkov-action@master
with:
directory: .
baseline: .checkov.baseline
output_format: cli,sarif
output_file_path: console,results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
SARIF output puts findings inline on the pull request diff rather than in a log nobody opens. if: always() matters. Without it, a failing scan skips the upload and the developer sees a red check with no detail.
Add the pre-commit hook too, so the feedback arrives before the push:
repos:
- repo: https://github.com/bridgecrewio/checkov.git
rev: 3.2.334
hooks:
- id: checkov
args: [--framework, terraform, --compact, --quiet]
Scan the plan, not just the source
This is the step most teams skip, and it is where Checkov gets substantially more accurate.
Scanning HCL means Checkov evaluates source files with variables unresolved. Scanning a Terraform plan means it sees resolved values: the actual CIDR, the actual bucket policy, including everything that came from modules and remote state.
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
checkov -f tfplan.json --repo-root-for-plan-enrichment . --framework terraform_plan
--repo-root-for-plan-enrichment maps findings back to source files and line numbers, so a plan finding still points at the code that caused it.
Run source scanning on every PR for fast feedback, and plan scanning in the pipeline that actually applies. The plan scan catches things the source scan structurally cannot.
Custom policies
The built-in checks are generic cloud hygiene. Your organisation’s rules (mandatory tags, approved regions, naming conventions) need custom policies. YAML is enough for most:
metadata:
name: "Resources must carry an owner tag"
id: "CKV_ORG_1"
category: "CONVENTION"
severity: "MEDIUM"
definition:
cond_type: attribute
resource_types:
- aws_s3_bucket
- aws_rds_cluster
- aws_instance
attribute: "tags.owner"
operator: exists
checkov -d . --external-checks-dir ./policies
An owner tag policy is usually worth more than any single built-in check. It is the one that makes cost allocation and incident routing work, and no generic ruleset can know your tag taxonomy.
Where it fits
Checkov is one layer. It reads configuration files, so it does not know about drift, resources created outside IaC, or anything running. Pair it with a runtime posture scan and image scanning. The Trivy post covers the container side.
Two habits decide whether it survives contact with your team: baseline before enforcing, and require a written reason for every suppression. Skip either and Checkov becomes a check people click through.