Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Checkov: scanning infrastructure as code

By updated

Improve this page

Checkov is the open source infrastructure as code (IaC) scanner maintained by Prisma Cloud (Palo Alto Networks), under the Apache 2.0 licence. It reads your Terraform, Kubernetes, Dockerfile, Ansible or CloudFormation files and flags misconfigurations before they are deployed: public bucket, SSH open to the world, privileged container. This lesson is for a DevOps engineer who wants to plug it into CI without drowning in its results.

Everything is measured on Checkov 3.3.22: 25 failures on four Terraform resources, 22 on a single Deployment, and plain-text secrets that slip through without an alert in each of the four example files. Knowing what Checkov does not see matters as much as knowing how to read what it finds.

What you will learn

  • Install Checkov in a pinned version and run a first scan
  • Read a result and separate real risks from comfort rules
  • Spot the blind spots, secrets included, before relying on it
  • Make a CI job fail on the chosen rules, not on severity
  • Handle false positives and write a custom rule that loads
  • Produce the evidence for SOCLE-INT-VER-2 (in French), the IaC configuration analysis

What is Checkov?

Checkov is a static analyser (SAST) specialised in infrastructure as code: it reads the code, without deploying anything or querying a cloud. Each rule (a "check") carries a stable identifier, such as CKV_AWS_24, and verifies one precise good practice. It can also link resources together through graph analysis: rules prefixed CKV2_ check, for example, whether an S3 bucket has a public access block declared in another resource.

How many rules, and for which formats?

The checkov --list command of version 3.3.22 counts 1361 distinct rule identifiers. The "more than 800" figure still going around dates from version 2. The breakdown shows where the effort goes: Terraform dwarfs everything else, and a more recent framework such as GitHub Actions only has a handful of rules.

FrameworkRules (Checkov 3.3.22)
Terraform1144
ARM / Bicep (Azure)173 / 167
CloudFormation139
Kubernetes119
Ansible29
Dockerfile28
OpenAPI21
Secrets18
GitHub Actions8
GitLab CI3

The same rule can serve several formats, hence a sum greater than the total. For CI pipelines, these figures say it all: a GitHub Actions workflow is better checked with dedicated tools such as zizmor, seen in the lesson on implicit trust in pipelines.

What Checkov does not prove

A green Checkov report does not prove that an infrastructure is secure. It proves that the code contains none of the configurations its rules know about. Three limits come back throughout the lesson. It only sees the code, not the real state of the cloud nor what an operator changed by hand. It does not read the vulnerabilities of an image's packages, which is the job of Trivy. And its secret detection, measured below, lets ordinary passwords through.

Installing Checkov in a pinned version

Checkov is a Python package: you install it with pipx to isolate it from the rest of the system, pinning the version. The official Docker image avoids installing Python, and is pinned by digest: since the Trivy incident of 2026, a scanner is treated like any dependency, never as latest.

Terminal window
# Isolated installation, pinned version
pipx install "checkov==3.3.22"
checkov --version
# 3.3.22
# Or without Python, with the image pinned by digest
docker run --rm -v "$PWD":/src -w /src \
bridgecrew/checkov:3.3.22@sha256:617c76e3f9b1f7907ebca9abb6b9d746844edcb48e9bd775e4692c69c1c6ac47 \
-d . --compact --quiet

The output of checkov --version must show exactly the requested version. The Homebrew package also exists on macOS, but it follows the latest version published, which does not suit a reproducible CI.

First scan: a deliberately wrong Terraform file

The simplest way to understand Checkov is to give it a file whose mistakes you know. This one is valid for Terraform: terraform validate accepts it. It nevertheless contains four classic mistakes, marked in comments, and a plain-text password.

terraform/main.tf
# ❌ S3 bucket with public access
resource "aws_s3_bucket" "bad_bucket" {
bucket = "factures-clients-prod"
acl = "public-read" # Problem: bucket publicly accessible
}
# ❌ EC2 instance without IMDSv2
resource "aws_instance" "bad_instance" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
# No metadata_options: IMDSv1 stays active, exploitable through SSRF
}
# ❌ Security Group with SSH open to the world
resource "aws_security_group" "bad_sg" {
name = "allow-all-ssh"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Problem: SSH open to the Internet
}
}
# ❌ Publicly accessible RDS with a plain-text password
resource "aws_db_instance" "bad_rds" {
identifier = "facturation"
engine = "mysql"
instance_class = "db.t3.micro"
username = "admin"
password = "SuperSecret123!" # Problem: secret in the code
publicly_accessible = true # Problem: database exposed to the Internet
}

Running the scan and reducing the output

Two options make the output readable in a terminal. --compact removes the code excerpt shown under each result, and --quiet hides the passed rules: without it, Checkov also lists every check that passed. The scan exits with code 1 as soon as a rule fails, which is enough to make a CI job fail.

Terminal window
$ checkov -d terraform --compact --quiet
terraform scan results:
Passed checks: 14, Failed checks: 25, Skipped checks: 0
Check: CKV_AWS_79: "Ensure Instance Metadata Service Version 1 is not enabled"
FAILED for resource: aws_instance.bad_instance
File: /main.tf:8-12
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.bad_sg
File: /main.tf:15-23
Check: CKV_AWS_17: "Ensure all data stored in RDS is not publicly accessible"
FAILED for resource: aws_db_instance.bad_rds
File: /main.tf:26-33
Check: CKV_AWS_20: "S3 Bucket has an ACL defined which allows public READ access."
FAILED for resource: aws_s3_bucket.bad_bucket
File: /main.tf:2-5
...
$ echo $?
1

The four targeted mistakes are found, each by a named rule. Each block also carries a Guide: line pointing to Prisma Cloud's remediation page, removed here for readability.

Reading a result: real risk or comfort rule?

Out of 25 failures, only four match the targeted mistakes. The other 21 are good practices that this file does not address: cross-region replication of the bucket (CKV_AWS_144), Multi-AZ for the database (CKV_AWS_157), logging, lifecycle. They are not equal: a public bucket exposes data today, a missing replication only costs you in a regional disaster. The reading work is to classify, not to fix everything.

RuleWhat it checksRisk if ignored
CKV_AWS_20public-read ACL on a bucketData readable by the whole Internet
CKV_AWS_24SSH open from 0.0.0.0/0Brute force, unauthorised access
CKV_AWS_17publicly_accessible = true on RDSDatabase directly exposed
CKV_AWS_79IMDSv1 active on EC2Theft of the instance credentials through SSRF
CKV_AWS_144Cross-region replicationLoss in case of a regional outage, no exposure

The database's plain-text password, however, triggers no rule. Secret detection looks for known formats and high-entropy strings: SuperSecret123! resembles neither. This is the first blind spot to remember.

Kubernetes, Dockerfile, Ansible: what the rules see

The other formats follow the same principle, with a common trap: a high number of passed rules says nothing about the security of the file. Checkov applies its whole rule set to every resource, including the Kubernetes control plane checks that make no sense for a Deployment.

A Deployment that gives access to the node

This manifest piles up the configurations that give a container full access to its node: shared host network and processes, privileged mode, root of the file system mounted. It also carries a secret in an environment variable.

k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: insecure-app
namespace: default # ❌ Uses the default namespace
spec:
replicas: 1
selector:
matchLabels:
app: insecure-app
template:
metadata:
labels:
app: insecure-app
spec:
hostNetwork: true # ❌ Shares the host network
hostPID: true # ❌ Sees the host processes
containers:
- name: app
image: nginx:latest # ❌ "latest" tag, not reproducible
securityContext:
privileged: true # ❌ Privileged container = root on the host
env:
- name: DB_PASSWORD
value: "secret123" # ❌ Secret in the env variables
volumeMounts:
- mountPath: /host
name: host-volume
volumes:
- name: host-volume
hostPath:
path: / # ❌ Mounts the host root

The scan reports 22 failures for 67 passes, and the most serious ones are there. Watch the identifiers: hostPID triggers CKV_K8S_17, not CKV_K8S_1, which carries the same title but only targets the old PodSecurityPolicy, removed from Kubernetes.

Terminal window
$ checkov -f k8s/deployment.yaml --compact --quiet
kubernetes scan results:
Passed checks: 67, Failed checks: 22, Skipped checks: 0
Check: CKV_K8S_17: "Containers should not share the host process ID namespace"
Check: CKV_K8S_19: "Containers should not share the host network namespace"
Check: CKV_K8S_16: "Container should not be privileged"
Check: CKV_K8S_14: "Image Tag should be fixed - not latest or blank"
Check: CKV_K8S_43: "Image should use digest"
...

Two mistakes in the manifest nevertheless trigger no rule. The hostPath mount of the root / goes through: the only related rule, CKV_K8S_27, only targets the Docker socket. And DB_PASSWORD in clear text in env.value goes through too: CKV_K8S_35 only concerns Kubernetes secrets injected as variables. These two blind spots belong to an admission controller and to the Pod Security Standards (the three levels of restriction that Kubernetes can impose on pods), covered in the module on containers.

A badly built Dockerfile

Dockerfile rules cover build choices: floating base image, ADD instruction, no non-root user. This file combines several of them, and hides an API key in an ENV, readable by anyone who pulls the image.

docker/Dockerfile
# ❌ "latest" tag: non-reproducible version
FROM python:latest
# ❌ Deprecated MAINTAINER (use LABEL)
MAINTAINER dev@example.com
WORKDIR /app
# ❌ ADD can extract archives and download from the Internet
ADD https://example.com/config.tar.gz /app/
ADD . /app/
RUN pip install -r requirements.txt
# ❌ Exposes the SSH port
EXPOSE 22 80
# ❌ Secret in an environment variable (visible in the image)
ENV API_KEY=sk-secret-12345
# ❌ No USER and no HEALTHCHECK
CMD ["python", "app.py"]

The scan gives 7 failures. Absence rules, such as CKV_DOCKER_3 which requires a USER, target the whole file (Dockerfile. without a suffix), for lack of a line to point to.

Terminal window
$ checkov -f docker/Dockerfile --compact --quiet
dockerfile scan results:
Passed checks: 26, Failed checks: 7, Skipped checks: 0
Check: CKV_DOCKER_7: "Ensure the base image uses a non latest version tag"
Check: CKV_DOCKER_6: "Ensure that LABEL maintainer is used instead of MAINTAINER (deprecated)"
Check: CKV_DOCKER_4: "Ensure that COPY is used instead of ADD in Dockerfiles"
Check: CKV_DOCKER_4: "Ensure that COPY is used instead of ADD in Dockerfiles"
Check: CKV_DOCKER_1: "Ensure port 22 is not exposed"
Check: CKV_DOCKER_2: "Ensure that HEALTHCHECK instructions have been added to container images"
Check: CKV_DOCKER_3: "Ensure that a user for the container has been created"

The API_KEY=sk-secret-12345 key appears nowhere. CKV_DOCKER_7 only triggers on latest: a versioned tag without a digest, such as python:3.14, satisfies it. The lesson on Dockerfile best practices covers what these rules leave aside.

An Ansible playbook

The Ansible rule set is short, 29 rules, focused on the options that disable a verification. This playbook contains two of them, force and validate_certs, plus a mode: "0777" and two secrets in variables.

ansible/playbook.yml
---
- name: Configuration serveur web
hosts: webservers
become: true
vars:
# ❌ Plain-text password in the code
db_password: "SuperSecret123!"
api_key: "sk-1234567890abcdef"
tasks:
- name: Install packages with force
ansible.builtin.apt:
name: nginx
state: present
force: true # ❌ Bypasses signature verification
- name: Download file
ansible.builtin.get_url:
url: https://example.com/config.tar.gz
dest: /tmp/config.tar.gz
validate_certs: false # ❌ Disables TLS validation
- name: Set insecure permissions
ansible.builtin.file:
path: /etc/app/config
mode: "0777" # ❌ Overly permissive permissions

The scan produces two reports for a single file: ansible for the modules and secrets for the text. Secret detection runs on any file it can read, whatever the framework.

Terminal window
$ checkov -f ansible/playbook.yml --compact --quiet
secrets scan results:
Passed checks: 0, Failed checks: 1, Skipped checks: 0
Check: CKV_SECRET_6: "Base64 High Entropy String"
File: /ansible/playbook.yml:8-9
ansible scan results:
Passed checks: 2, Failed checks: 2, Skipped checks: 0
Check: CKV_ANSIBLE_6: "Ensure that the force parameter is not used, as it disables signature validation ..."
Check: CKV_ANSIBLE_2: "Ensure that certificate validation isn't disabled with get_url"

Only the api_key, a random string, is found. The db_password goes through, and so does the mode: "0777": none of the 29 Ansible rules reads the value of a permission. This check is left to code review, or to a custom rule like those at the end of this lesson.

Secret detection has blind spots

Checkov ships 18 secret rules: known formats (CKV_SECRET_2 for an AWS AKIA… key, CKV_SECRET_13 for a private key, CKV_SECRET_14 for Slack) and two entropy rules, in base64 (CKV_SECRET_6) and in hexadecimal (CKV_SECRET_19). This mechanism misses two categories, measured on version 3.3.22.

The first is the human password: SuperSecret123!, secret123 or sk-secret-12345 have neither a known format nor enough entropy, and none of the four previous files flagged them. The second is more surprising: by default, Checkov only reads secrets in certain file types. A real AWS key in a credentials.ini, or a GitHub token in a .env, goes through without an alert.

Terminal window
$ checkov -d sec --framework secrets --compact --quiet
# no result: .ini and .env are not read
$ checkov -d sec --framework secrets --compact --quiet --enable-secret-scan-all-files
Passed checks: 0, Failed checks: 3, Skipped checks: 0
Check: CKV_SECRET_6: "Base64 High Entropy String"
File: /.env:1-2
Check: CKV_SECRET_2: "AWS Access Key"
File: /credentials.ini:1-2
Check: CKV_SECRET_2: "AWS Access Key"
File: /credentials.ini:2-3

The same content renamed config.yaml is found without the option. The practical conclusion: Checkov does not replace a dedicated secret detector such as gitleaks, run in pre-commit and in CI, covered in the lesson on secrets in CI/CD.

Making CI fail on the right rules

Checkov exits with code 1 as soon as a rule fails and with code 0 otherwise: on an old project, the first job is therefore bound to break. Three options adjust this behaviour, and they were measured on the previous Dockerfile.

OptionEffectMeasured exit code
noneFails as soon as one rule is at fault1
--soft-failFull report, never fails0
--soft-fail-on CKV_DOCKER_2,CKV_DOCKER_3These two rules no longer block, the others do1 (5 other failures)
--soft-fail --hard-fail-on CKV_DOCKER_8Only the listed rule blocks0 (it passes)

The pair --soft-fail and --hard-fail-on is the most useful to get started: you block on a short list of serious rules, such as CKV_AWS_20, CKV_AWS_24 or CKV_K8S_16, and leave the rest as a report. You lengthen the list as the debt goes down.

A GitHub Actions example

The official bridgecrewio/checkov-action action runs Checkov in its official image. It is pinned to the SHA of its commit, like any action: its version v12.3128.0 runs Checkov 3.3.22. The SARIF report (a standard format for analysis results) goes to the repository's Code Scanning tab, even when the scan fails, thanks to if: always().

.github/workflows/checkov.yml
name: checkov
on:
pull_request:
paths:
- "infra/**"
permissions: {}
jobs:
checkov:
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Scan the IaC
uses: bridgecrewio/checkov-action@cc0e17d456ef1713bbfd963ebac19dc776775cb2 # v12.3128.0
with:
directory: infra/
framework: terraform
output_format: cli,sarif
output_file_path: console,results.sarif
- name: Publish to Code Scanning
if: always()
uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
with:
sarif_file: results.sarif

This workflow passes actionlint, zizmor 1.30.0 and poutine 1.1.6 with no finding at all. One limit remains to know: the action designates its image by tag (ghcr.io/bridgecrewio/checkov:3.3.22), not by digest. For a complete guarantee, run the image pinned by digest in a run step.

Managing false positives

A rule can be wrong in your context: a bucket that hosts a public website must be public. Checkov offers three mechanisms, from the most targeted to the broadest. The first, inline suppression, is the only one that stays attached to the code and goes through review with it.

Suppressing a rule in the code

Each format has its own comment syntax, always followed by a justification. The three forms below were verified: each scan counts the rule as Skipped checks: 1 or 2 instead of a failure.

resource "aws_s3_bucket" "site" {
# checkov:skip=CKV_AWS_20: static website, public by design
bucket = "site-vitrine"
acl = "public-read"
}
apiVersion: v1
kind: Pod
metadata:
name: test-pod
namespace: recette
annotations:
checkov.io/skip1: CKV_K8S_40=UID imposed by the test image
spec:
containers:
- name: app
image: nginx:1.29.1@sha256:8adbdcb969e2676478ee2c7ad333956f0c8e0e4c5a7463f4611d7a2e7a7ff5dc
# checkov:skip=CKV_DOCKER_2: the health probe is carried by the Kubernetes manifest
FROM ubuntu:24.04@sha256:008173c23f95b170204355c12626cb5a965d779a7e1283b09e9cffbb1bf33ca3
# checkov:skip=CKV_DOCKER_4: the archive must be extracted on copy
ADD app.tar.gz /opt/app/
RUN useradd --uid 1000 --create-home appuser
USER appuser

On Kubernetes, the annotation is numbered (skip1, skip2…) to set several of them. On the command line, --skip-check CKV_AWS_20,CKV_AWS_21 removes rules for the whole scan, without a trace in the code: keep it for the rules that the team considers irrelevant everywhere.

Starting with a baseline

On an existing project, --create-baseline records the current failures in a .checkov.baseline file, placed in the scanned folder. Subsequent scans with --baseline only report the new failures. Measured: after adding an open RDP Security Group to the starting Terraform file, the scan with the baseline only reports its 3 failures, instead of 28.

Terminal window
checkov -d terraform --create-baseline
checkov -d terraform --baseline terraform/.checkov.baseline --compact --quiet

The baseline freezes a debt, it does not pay it off. You version it, and regenerate it only when fixes have reduced it.

A configuration file for the team

Rather than repeating the options, a .checkov.yaml file placed in the current folder is loaded automatically, without any option. Measured: with skip-check: [CKV_DOCKER_2, CKV_DOCKER_3] in this file, a plain checkov -d . drops from 7 to 5 failures. It is both handy and treacherous, like the trivy.yaml of the previous lesson: a file forgotten in a repository silently filters the report.

.checkov.yaml
directory:
- infra
framework:
- terraform
- kubernetes
- dockerfile
skip-check:
- CKV_AWS_144 # cross-region replication: out of budget, decision of 2026-10-01
compact: true
quiet: true

Each key takes a command-line option without the double dash. The command checkov -d . --skip-check CKV_DOCKER_7 --create-config .checkov.yaml writes this file from the current options, and --show-config displays the configuration actually applied. Avoid the check: key: when set, it limits the scan to the listed rules only.

Exporting the results

Checkov writes several formats in a single pass: cli, json, sarif, junitxml, csv and cyclonedx, among others. The behaviour of --output-file-path changes depending on what you give it, and it was measured: a list of paths, one per format, writes files; a single path for several formats is treated as a folder, filled with results_json.json and results_sarif.sarif.

Terminal window
# One file per format: console for cli, checkov.xml for JUnit
checkov -d infra -o cli -o junitxml --output-file-path console,checkov.xml
# One folder for several formats
checkov -d infra -o sarif -o json --output-file-path rapports

The summary block of the JSON is enough to follow a project over time. Its parsing_errors field deserves attention: a file that Checkov could not read is not analysed, and does not count as a failure.

{
"passed": 26,
"failed": 7,
"skipped": 0,
"parsing_errors": 0,
"resource_count": 1,
"checkov_version": "3.3.22"
}

Writing a custom rule

The built-in rules ignore your internal conventions: mandatory cost centre tag, allowed instance types. Checkov accepts rules in YAML for an attribute to check, and in Python for richer logic. Both are loaded with --external-checks-dir.

In YAML: a mandatory tag

The YAML format describes a condition on an attribute. This one requires a cost-center tag on every S3 bucket; on the starting Terraform file, it fails on aws_s3_bucket.bad_bucket, which has none.

policies/s3_cost_center.yaml
metadata:
id: "CKV2_ACME_1"
name: "Ensure S3 buckets carry a cost-center tag"
category: "GENERAL_SECURITY"
definition:
cond_type: "attribute"
resource_types:
- "aws_s3_bucket"
attribute: "tags.cost-center"
operator: "exists"
Terminal window
checkov -d terraform --external-checks-dir policies --check CKV2_ACME_1

In Python, and the __init__.py trap

A Python rule inherits from a base class and returns PASSED or FAILED. This one refuses instance types outside a list. One detail decides everything, measured on version 3.3.22: without an __init__.py file in the folder, the rule is not loaded, and Checkov does not say so. The scan then returns zero results, which is easily mistaken for a success.

checks/ec2_instance_type.py
from checkov.common.models.enums import CheckCategories, CheckResult
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
class EC2InstanceTypeCheck(BaseResourceCheck):
def __init__(self):
super().__init__(
name="Ensure EC2 instances use approved instance types",
id="CKV_ACME_1",
categories=[CheckCategories.GENERAL_SECURITY],
supported_resources=["aws_instance"],
)
def scan_resource_conf(self, conf):
# Types allowed in your organisation
autorises = ["t3.micro", "t3.small", "t3.medium"]
instance_type = conf.get("instance_type", [None])[0]
if instance_type in autorises:
return CheckResult.PASSED
return CheckResult.FAILED
check = EC2InstanceTypeCheck()
Terminal window
touch checks/__init__.py # without it, the rule is silently ignored
checkov -d terraform --external-checks-dir checks --check CKV_ACME_1
# Passed checks: 1 (t3.micro); an m5.4xlarge instance gives FAILED

Prefix the identifiers with the name of the organisation (CKV_ACME_) to avoid any collision with a future official rule. Test each rule on a compliant file and a faulty file: a rule that never sees a failure may never have been loaded.

Key points

  • Checkov reads infrastructure code without deploying anything: 1361 rules in 3.3.22, of which 1144 for Terraform.
  • You install it pinned: pipx install "checkov==3.3.22" or the image pinned by digest.
  • --compact removes the code, --quiet removes the passes; the scan exits with code 1 at the first failure.
  • A failure is not a risk: you classify the results before fixing.
  • Ordinary secrets go through, and .ini or .env files are only read with --enable-secret-scan-all-files.
  • Without an API key, no severity: you block with --soft-fail and --hard-fail-on on named rules.
  • A .checkov.yaml is loaded automatically, and a Python rule without __init__.py is silently ignored.
  • The evidence expected by SOCLE-INT-VER-2 is an IaC configuration analysis report: the SARIF or the JSON of the CI job.

Knowledge check

Check that you have the essentials of this lesson. The quiz only covers what has just been explained here, from Checkov results to custom rules.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

FAQ: frequently asked questions

The questions below cover the most common doubts about Checkov: comparison with Trivy, integration into CI, false positives and custom rules, with the same pinned version as the lesson.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn