Scan Terraform Code for Vulnerabilities

Use Checkov to find and fix security misconfigs in Terraform IaC files.

Introduction

30 Second Summary

Imagine deploying a cloud server that accidentally exposes all its data to the internet. Most cloud security breaches trace back to a single misconfiguration that slipped through unnoticed.

In this project, you will write intentionally insecure Terraform code, scan it with Checkov to catch real-world security vulnerabilities, fix every finding, and automate the entire process in a CI/CD pipeline.

What You'll Build

A security scanning pipeline that uses Checkov to analyze Infrastructure as Code for misconfigurations before they ever reach production.

By the end of this project, you'll have:

  • A set of Terraform configurations for AWS S3, EC2, and Security Group resources with intentional security misconfigurations.
  • Checkov scan reports showing exactly which checks failed and why, plus remediated code that passes all checks.
  • A GitHub Actions workflow that automatically runs Checkov on every push, blocking insecure code from merging.
  • Secret Mission: A custom Checkov YAML policy that enforces your own tagging standard across all resources.

Are there any prerequisites?

You need basic comfort with the command line and Git (clone, commit, push). No Terraform or cloud experience is needed.

Install Terraform and Checkov

Before you can scan infrastructure code for security issues, you need the tools that write and analyze that code. Terraform is the industry-standard tool for defining cloud infrastructure as code. Checkov is a static analysis scanner that reads those Terraform files and flags security misconfigurations.

In this step, you will install both tools and create the GitHub repository where your project will live.

In this step, get ready to:
  • Install the Terraform CLI and verify it runs.
  • Install Checkov via pip and verify it runs.
  • Create and clone a GitHub repository for your project.
Install the Terraform CLI

Terraform is distributed as a single binary. The installation method differs by operating system.

macOS

On macOS, the official method uses Homebrew with HashiCorp's own tap.

  • Install Terraform by running these commands in your terminal:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform

What do these commands do?

The first command adds HashiCorp's official Homebrew repository (called a "tap") to your system. The second command installs the latest Terraform binary from that repository.

Windows

On Windows, you download the Terraform binary directly from HashiCorp and add it to your PATH.

  • Open https://developer.hashicorp.com/terraform/install in your browser.
  • Under the Windows section, click the AMD64 download link to download the .zip file.
  • Extract the .zip file. Inside you will find a single file called terraform.exe.
  • Move terraform.exe to a folder that is already on your system PATH (for example, C:\Windows\System32), or create a new folder like C:\terraform and add that folder to your PATH.

Adding a folder to PATH on Windows

Press the Windows key, search for Environment Variables, and open Edit the system environment variables.

Click Environment Variables, select Path under User variables, click Edit, then New, and paste the folder path where you placed terraform.exe.

  • Verify Terraform is available by running this command:
terraform version

✔️ I see a version number

You should see output starting with Terraform v1.15.3 (or a similar version number). Terraform is ready to go.

ⓧ Command not found

Your system cannot find the terraform binary.

  • On macOS, make sure Homebrew installed successfully. Try running brew doctor to check for issues, then re-run the install commands above.
  • On Windows, make sure the folder containing terraform.exe is in your PATH. Open a new terminal window after editing PATH (existing windows do not pick up changes).
Install Checkov via pip

Checkov is a Python package that you install with pip. It requires Python 3.9 or higher, which you already have.

  • Install Checkov by running this command:
pip3 install checkov

What does this command do?

This downloads and installs the Checkov package from PyPI along with its dependencies. Checkov includes over 1,000 built-in security policies for Terraform and other IaC frameworks.

  • Verify the installation by running this command:
checkov --version

✔️ I see a version number

You should see a version number like 3.2.528 or similar. Checkov is installed and ready.

ⓧ Command not found

The checkov command is not on your PATH. This usually means Python's script directory is not in your system PATH.

  • On macOS, try running python3 -m checkov to confirm it installed. If that works, add Python's bin directory to your shell profile.
  • On Windows, close and reopen your terminal. If it still fails, try pip3 show checkov to confirm the package is installed, then add Python's Scripts folder to your PATH.

Seeing a permission error during install?

If you see a permissions error, try adding the --user flag: pip3 install --user checkov. This installs Checkov into your user directory instead of the system-wide location.

Help me fix this permission error

Create your project repository

You need a Git repository to store your Terraform files and eventually wire up automated scanning with GitHub Actions.

  • Open GitHub in your browser and sign in to your account.
  • Click the + icon in the top-right corner, then select New repository.
  • Enter terraform-security-scanning as the repository name.
  • Leave the repository set to Public.
  • Check the box for Add a README file.
  • Click Create repository.

Now clone the repository to your local machine.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone your new repository by running:
git clone [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/terraform-security-scanning.git"]]

Where do I find my repository URL?

On your new repository's page on GitHub, click the green Code button. Copy the HTTPS URL shown in the dropdown.

  • Move into the project folder by running:
cd terraform-security-scanning
  • Confirm the clone worked by running:
ls

You should see README.md in the output.

Finally, open the project in VS Code.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type VS Code and press Enter to open it.
  • In VS Code, click File in the top menu bar.
  • Click Open Folder.
  • Select the terraform-security-scanning folder on your Desktop, then click Open.
  • Confirm the folder is open by checking that you see README.md in the Explorer sidebar on the left.

Don't see the Explorer sidebar?

Press Cmd+Shift+E (macOS) or Ctrl+Shift+E (Windows) to toggle the Explorer panel. If the folder is empty, double-check you selected the right folder when opening.

Help me troubleshoot VS Code not showing my files

Your environment is ready. Next up, you will write intentionally insecure Terraform code to give Checkov something to find.

Write Intentionally Insecure Terraform

Your development environment is ready with Terraform and Checkov installed. Now it's time to write some Terraform code that is intentionally insecure.

Terraform uses HCL (HashiCorp Configuration Language) to declare cloud infrastructure as code. In this step, you'll create resources with common security misconfigurations that Checkov will catch in the next step. The best part: Checkov scans .tf files statically, so you don't need an AWS account or any cloud credentials.

In this step, get ready to:
  • Create the AWS provider configuration in providers.tf.
  • Write three insecure AWS resources in main.tf.
  • Understand what each missing security setting means.
Create the provider configuration

Every Terraform project starts with a provider block that tells Terraform which cloud platform and region to target. This file won't provision anything real. It just gives Checkov enough context to understand what kind of resources your code declares.

  • In VS Code's Explorer sidebar, right-click the empty area and select New File.
  • Name the file providers.tf and press Enter.
  • Paste the following code into providers.tf:
terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

What does this code do?

The terraform block sets the minimum Terraform version and declares which provider plugin to download.

The provider "aws" block tells Terraform you're targeting AWS in the us-east-1 region. Since Checkov performs static analysis only, it reads this file without ever connecting to AWS.

  • Save providers.tf.
  • Confirm the file appears in your Explorer sidebar under your terraform-security-scanning folder.

Don't see the file in Explorer?

Make sure you saved with Cmd+S (macOS) or Ctrl+S (Windows). Check that the file extension is .tf and not .tf.txt.

help me troubleshoot why my providers.tf file isn't appearing in VS Code

✔️ Awesome, I've got everything!

Great. Make sure you've saved providers.tf before moving on.

ⓧ I'd like to double check the full code

terraform {
  required_version = ">= 1.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}
Create an insecure S3 bucket

An S3 bucket in production should have encryption, versioning, and public access protections. You're going to skip all three on purpose so Checkov has something to flag.

  • Create a new file called main.tf in your project root by right-clicking in the Explorer sidebar and selecting New File.
  • Paste this S3 bucket resource into main.tf:
# S3 Bucket
resource "aws_s3_bucket" "app_data" {
  bucket = "my-secure-app-data-bucket"

  tags = {
    Name        = "app-data"
    Environment = "production"
  }
}

What's missing from this bucket?

This bucket has three critical security gaps that Checkov will detect:

  • No server-side encryption means data is stored unencrypted at rest.
  • No versioning means deleted or overwritten objects cannot be recovered.
  • No public access block means a misconfigured policy could accidentally expose data to the internet.
  • Save main.tf.
  • You should see both providers.tf and main.tf in your Explorer sidebar.

Seeing a syntax highlighting error?

If VS Code shows the file as plain text without colour highlighting, install the HashiCorp Terraform extension from the Extensions marketplace. This is optional and won't affect Checkov scanning.

help me fix syntax issues in my main.tf file

Add an insecure Security Group and EC2 instance

Now you'll add two more resources with dangerous misconfigurations. A Security Group controls which network traffic can reach your servers. An EC2 instance is a virtual machine running in AWS.

  • In main.tf, place your cursor after the closing } of the S3 bucket resource.
  • Add a blank line, then paste this Security Group resource:
# Security Group
resource "aws_security_group" "app_sg" {
  name        = "app-security-group"
  description = "Security group for application servers"

  ingress {
    description = "Allow all inbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "app-sg"
    Environment = "production"
  }
}

Why is this Security Group dangerous?

The cidr_blocks = ["0.0.0.0/0"] on the ingress rule means every IP address on the internet can connect to this server on every port. In production, you should restrict ingress to specific trusted IP ranges and only the ports your application needs.

  • You won't see a visible change yet. The next chunk completes main.tf with the EC2 instance.
  • After the Security Group's closing }, add a blank line and paste this EC2 instance resource:
# EC2 Instance
resource "aws_instance" "app_server" {
  ami                    = "ami-0c02fb55956c7d316"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.app_sg.id]

  root_block_device {
    encrypted = false
  }

  tags = {
    Name        = "app-server"
    Environment = "production"
  }
}

What's wrong with this EC2 instance?

Two things are missing:

  • encrypted = false on the root block device means the virtual machine's disk is stored unencrypted. Anyone who gains access to the underlying storage can read your data.
  • No metadata_options block means the instance uses the older IMDSv1 metadata service. IMDSv1 is vulnerable to server-side request forgery (SSRF) attacks that can steal your instance's credentials.
  • Save main.tf.
  • Your file should now contain three resource blocks: aws_s3_bucket, aws_security_group, and aws_instance.

Seeing red underlines or errors?

Make sure each resource block has matching opening and closing braces. Count that every { has a corresponding }. Check that string values are wrapped in double quotes.

help me fix syntax errors in my Terraform main.tf

✔️ Awesome, I've got everything!

Great. Double check you've saved main.tf with all three resources.

ⓧ I'd like to double check the full code

# S3 Bucket
resource "aws_s3_bucket" "app_data" {
  bucket = "my-secure-app-data-bucket"

  tags = {
    Name        = "app-data"
    Environment = "production"
  }
}

# Security Group
resource "aws_security_group" "app_sg" {
  name        = "app-security-group"
  description = "Security group for application servers"

  ingress {
    description = "Allow all inbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "app-sg"
    Environment = "production"
  }
}

# EC2 Instance
resource "aws_instance" "app_server" {
  ami                    = "ami-0c02fb55956c7d316"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.app_sg.id]

  root_block_device {
    encrypted = false
  }

  tags = {
    Name        = "app-server"
    Environment = "production"
  }
}

Your insecure Terraform is ready to be audited. Next up, you'll run Checkov to scan these files and see exactly which security checks fail.

Run Your First Checkov Scan

You have three Terraform resources written with intentional security gaps. The question is: how do you find out exactly what is wrong before any of this code reaches a real cloud environment?

In this step, you will run Checkov to scan your .tf files, read the results it produces, and export a structured JSON report for further analysis.

In this step, get ready to:
  • Run your first Checkov scan and review the terminal output.
  • Interpret the specific security check failures for each resource.
  • Export scan results to a JSON file for structured analysis.
Scan the Terraform directory

Checkov scans .tf files statically, meaning it reads the code and checks it against over 1,000 built-in security policies without connecting to AWS or any cloud provider.

  • Run your first security scan by running this command in your terminal:
checkov -d .

What does this command do?

The -d . flag tells Checkov to scan the current directory. Checkov finds every .tf file, parses the resource definitions, and runs all applicable security checks against them.

You should see a coloured output in your terminal listing passed and failed checks. Each entry in the output has four key components: a check ID (like CKV_AWS_145), the resource name, a PASSED or FAILED status, and a guideline link.

Seeing 'command not found'?

Make sure Checkov was installed correctly by running checkov --version. If that also fails, confirm you installed it with pip install checkov in the same Python environment your terminal is using.

Help me troubleshoot my Checkov installation

Interpret the results

Your scan output contains several failed checks. Each failed check pinpoints a specific security best practice that your code violates.

  • Scroll through the terminal output and look for lines marked FAILED.
  • Identify the check IDs tied to each of your three resources (aws_s3_bucket.app_data, aws_security_group.app_sg, aws_instance.app_server).

What do these check IDs mean?

  • CKV_AWS_145 checks that your S3 bucket is encrypted with KMS by default. Your bucket has no encryption configured at all.
  • CKV_AWS_18 checks that your S3 bucket has access logging enabled. Without logging, you have no audit trail of who accessed the bucket.
  • CKV_AWS_260 checks that your security group does not allow unrestricted ingress. Your 0.0.0.0/0 CIDR allows traffic from the entire internet.
  • CKV_AWS_79 checks that your EC2 instance enforces IMDSv2. Without this, attackers who compromise the instance can steal its IAM credentials via the metadata service.

Why does Checkov not need AWS credentials?

Checkov performs static analysis. It reads the .tf files as text and checks them against known-good patterns. It never calls any cloud API or provisions real resources.

Export results to JSON

The CLI output is great for a quick glance, but a JSON report gives you structured data you can feed into other tools or review in detail.

  • Generate a machine-readable JSON report by running this command:
checkov -d . -o json > checkov-results.json

What does this command do?

The -o json flag switches the output format from CLI to JSON. The > checkov-results.json part redirects that output into a file instead of printing it to the terminal.

  • Confirm the file was created by running:
ls

You should see checkov-results.json listed alongside main.tf and providers.tf.

  • Open checkov-results.json in VS Code by clicking on it in the Explorer sidebar.

Inside the JSON you will see a results object containing passed_checks and failed_checks arrays. Each failed check entry includes the check_id, check_name, the resource it applies to, and a severity level.

File is empty or malformed?

Make sure you ran the command from the same directory that contains your main.tf and providers.tf files. If you are in a different folder, Checkov finds no Terraform files and produces an empty report.

Help me fix my empty checkov-results.json file

You now have a clear picture of every security gap in your Terraform code. Next up, you will fix every single finding so your code passes all of Checkov's checks.

Fix Every Security Finding

In the last step, you ran Checkov and got a list of specific security failures. Now it is time to fix them.

Each failed check maps directly to a missing security control in your Terraform code. You will address them resource by resource: harden the S3 bucket, lock down the Security Group, and secure the EC2 instance.

In this step, get ready to:
  • Add encryption, versioning, and public access controls to the S3 bucket.
  • Restrict the Security Group to only allow HTTPS from a trusted network.
  • Enforce IMDSv2 and enable disk encryption on the EC2 instance.
Harden the S3 bucket

Your S3 bucket is missing three critical security layers: server-side encryption with KMS, versioning to protect against accidental deletion, and a public access block to prevent unintended exposure.

  • In main.tf, find the closing brace of the aws_s3_bucket resource (the one with "app_data").
  • Add these three companion resources directly below it:
resource "aws_s3_bucket_server_side_encryption_configuration" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_versioning" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

What does this code do?

Each resource addresses a specific Checkov finding:

  • aws_s3_bucket_server_side_encryption_configuration fixes CKV_AWS_145 by encrypting all objects with AWS KMS. The bucket_key_enabled setting reduces KMS API costs.
  • aws_s3_bucket_versioning keeps a history of every object change. If someone accidentally deletes or overwrites a file, you can restore the previous version.
  • aws_s3_bucket_public_access_block sets four flags that collectively prevent anyone from making the bucket or its objects public, even by mistake.
  • Save main.tf.
  • You will not see a visible change yet. The next two substeps finish hardening the remaining resources, and then you will re-run Checkov to confirm everything passes.

Seeing a syntax error when you save?

  • Make sure each new resource block starts with resource on its own line, outside the closing brace of the previous block.
  • Check that the bucket argument in each new resource references aws_s3_bucket.app_data.id exactly.

Why is my S3 hardening code causing errors?

Lock down the Security Group

Your Security Group currently allows all traffic from any IP address on any port. This is the equivalent of leaving every door and window wide open. The fix applies the principle of least privilege: allow only the traffic you actually need.

  • In main.tf, scroll to the aws_security_group resource named "app_sg".
  • Find the ingress block that currently looks like this:
  ingress {
    description = "Allow all inbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  • Replace that entire ingress block with this locked-down version:
  ingress {
    description = "HTTPS from trusted network"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }

What changed and why?

  • protocol changed from "-1" (all protocols) to "tcp" (only TCP).
  • from_port and to_port changed from 0 (all ports) to 443 (HTTPS only).
  • cidr_blocks changed from "0.0.0.0/0" (the entire internet) to "10.0.0.0/16" (a private network range). Only machines inside this VPC CIDR can reach the server.

This is the principle of least privilege applied to networking. Only allow the minimum access required for the application to function.

  • Save main.tf.
  • You will not see a change yet. The next substep finishes the EC2 hardening, then you will verify everything.

Accidentally deleted the egress block?

Only replace the ingress block. The egress block (which allows all outbound traffic) should remain unchanged. If you deleted it, add it back below the new ingress block:

  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

Help me fix my Security Group after I accidentally removed part of it.

Harden the EC2 instance

Your EC2 instance has two problems. It does not enforce IMDSv2 (the secure version of the instance metadata service), and its root disk is unencrypted. Both are common production misconfigurations that attackers exploit.

  • In main.tf, find the aws_instance resource named "app_server".
  • Replace the entire aws_instance resource block with this hardened version:
# EC2 Instance
resource "aws_instance" "app_server" {
  ami                    = "ami-0c02fb55956c7d316"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.app_sg.id]

  # Enforce IMDSv2 to prevent SSRF-based credential theft
  metadata_options {
    http_tokens   = "required"
    http_endpoint = "enabled"
  }

  # Encrypt the root volume at rest
  root_block_device {
    encrypted = true
  }

  tags = {
    Name        = "app-server"
    Environment = "production"
  }
}

What does this code do?

  • metadata_options with http_tokens = "required" forces all metadata requests to use IMDSv2 (token-based). This fixes CKV_AWS_79. Without it, an attacker who finds an SSRF vulnerability can steal the instance's IAM credentials from the metadata endpoint.
  • encrypted = true on the root_block_device ensures the disk is encrypted at rest using the default AWS KMS key. If someone gains physical access to the underlying hardware, the data is unreadable.
  • Save main.tf.

Now confirm that all your fixes satisfy Checkov.

  • Re-run the Checkov scan by running this command:
checkov -d .
  • You should see all checks passing with zero failures. The output will show Passed checks: with a count and Failed checks: 0.

Still seeing failed checks?

  • If CKV_AWS_145 still fails, check that sse_algorithm is set to "aws:kms" (not "AES256"). Checkov requires KMS encryption specifically for this check.
  • If CKV_AWS_79 still fails, confirm your metadata_options block is inside the aws_instance resource (not outside it) and http_tokens is set to "required".
  • If you see a syntax error instead of scan results, run terraform validate to get a more specific error message pointing to the line number.

Why is Checkov still failing after my fixes?

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf and that your Checkov output shows zero failed checks.

ⓧ I'd like to double check the full code

Here is the complete main.tf after all hardening changes:

# S3 Bucket
resource "aws_s3_bucket" "app_data" {
  bucket = "my-secure-app-data-bucket"

  tags = {
    Name        = "app-data"
    Environment = "production"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
    bucket_key_enabled = true
  }
}

resource "aws_s3_bucket_versioning" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "app_data" {
  bucket = aws_s3_bucket.app_data.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# Security Group
resource "aws_security_group" "app_sg" {
  name        = "app-security-group"
  description = "Security group for application servers"

  ingress {
    description = "HTTPS from trusted network"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }

  egress {
    description = "Allow all outbound"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "app-sg"
    Environment = "production"
  }
}

# EC2 Instance
resource "aws_instance" "app_server" {
  ami                    = "ami-0c02fb55956c7d316"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.app_sg.id]

  metadata_options {
    http_tokens   = "required"
    http_endpoint = "enabled"
  }

  root_block_device {
    encrypted = true
  }

  tags = {
    Name        = "app-server"
    Environment = "production"
  }
}

Every vulnerability from your initial scan is now resolved. Next up, you will wire this entire process into a GitHub Actions workflow so that insecure code can never slip past code review.

Automate Scanning with GitHub Actions

Your Terraform code is now fully hardened. Every Checkov check passes locally, which is great for you today.

But what happens when a teammate pushes insecure code next week? You need automated enforcement that catches misconfigurations before they ever reach the main branch. In this step, you'll create a GitHub Actions workflow that runs Checkov on every push.

In this step, get ready to:
  • Create a GitHub Actions workflow file that runs Checkov automatically.
  • Push your code and verify the workflow passes.
  • Test the security gate by intentionally breaking it.
Create the workflow file

GitHub Actions workflows live in a special .github/workflows/ directory at the root of your repository. Each .yml file in that folder defines an automated pipeline.

  • Create the workflow directory by running this command in your terminal:
mkdir -p .github/workflows
  • Confirm the folder exists by running:
ls .github/

You should see workflows listed in the output.

  • In VS Code, expand the .github folder in the Explorer sidebar.
  • Right-click the workflows folder and select New File.
  • Name the file checkov.yml and paste the following workflow configuration:
name: Checkov Security Scan

on:
  push:
    branches: ["**"]
  pull_request:
    branches: ["**"]

jobs:
  scan:
    name: Checkov IaC Scan
    runs-on: ubuntu-latest

    permissions:
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          framework: terraform
          output_format: cli,sarif
          output_file_path: console,results.sarif
          soft_fail: false

What does this workflow do?

  • on: push and pull_request triggers the workflow every time code is pushed or a pull request is opened against any branch.
  • actions/checkout@v4 pulls your repository code into the runner so Checkov can access the files.
  • bridgecrewio/checkov-action@v12 is the official Checkov GitHub Action. It runs the scan inside a container with Checkov pre-installed.
  • framework: terraform tells Checkov to only scan Terraform files, skipping other frameworks.
  • soft_fail: false means the workflow will fail (exit non-zero) if any check fails. This is what makes it a real security gate.
  • Save .github/workflows/checkov.yml.
  • Confirm the file exists by running:
ls .github/workflows/

You should see checkov.yml listed in the output.

Don't see the file?

Make sure you created the file inside .github/workflows/ and not at the project root. The leading dot in .github makes it a hidden folder.

Help me find my workflow file

Why GitHub Actions instead of running Checkov manually?

Running checkov -d . locally relies on every developer remembering to do it. An automated CI/CD pipeline runs on every push regardless of who wrote the code or whether they have Checkov installed.

This is the core of shift-left security. You catch problems at code review time, not after deployment.

✔️ Awesome, I've got everything!

Great. Double check you've saved your file before moving on.

ⓧ I'd like to double check the full code

name: Checkov Security Scan

on:
  push:
    branches: ["**"]
  pull_request:
    branches: ["**"]

jobs:
  scan:
    name: Checkov IaC Scan
    runs-on: ubuntu-latest

    permissions:
      contents: read

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: .
          framework: terraform
          output_format: cli,sarif
          output_file_path: console,results.sarif
          soft_fail: false
Push and verify the pipeline

Your workflow file only runs on GitHub's servers. You need to commit and push all your project files so GitHub Actions can pick up the workflow.

  • Stage all files, commit, and push to GitHub by running these commands:
git add .
git commit -m "Add hardened Terraform and Checkov CI workflow"
git push origin main

What does this push include?

This commit sends your hardened providers.tf, main.tf, checkov-results.json, and the new checkov.yml workflow to GitHub. As soon as the push arrives, GitHub detects the workflow file and starts running it.

  • Open your browser and navigate to your terraform-security-scanning repository on GitHub.
  • Click the Actions tab at the top of the repository page.
  • You should see a workflow run in progress or already completed with a green checkmark.
  • Click on the workflow run to see the detailed logs.
  • Expand the Run Checkov step to confirm all checks passed.

Workflow not appearing in the Actions tab?

Make sure your push succeeded. Check your terminal for any authentication errors. If you see a prompt about credentials, you may need to set up a personal access token or SSH key for GitHub.

Also verify the workflow file is at the exact path .github/workflows/checkov.yml. GitHub only detects workflows in that specific location.

Help me troubleshoot my workflow

Test the security gate

A security gate is only useful if it actually blocks insecure code. Let's prove yours works by intentionally reintroducing a vulnerability and watching the workflow reject it.

  • Create a new branch for your test by running:
git checkout -b test-security-gate
  • In main.tf, scroll down to the aws_instance resource and find the root_block_device block.
  • Find this line:
    encrypted = true
  • Change true to false, so the line now looks like this:
    encrypted = false
  • Save main.tf.
  • Commit and push the insecure change by running these commands:
git add .
git commit -m "Test: reintroduce insecure config"
git push origin test-security-gate
  • Go back to the Actions tab on GitHub.
  • You should see a new workflow run with a red X. This means Checkov detected the unencrypted root volume and blocked the push.
  • Click on the failed run and expand the Run Checkov step to see which check failed.

This is your security gate in action. Any code that fails Checkov's checks will be flagged immediately, preventing insecure infrastructure from reaching production.

Workflow passed when it should have failed?

Double check that you changed encrypted = true to encrypted = false and saved the file before committing. Run git diff to verify the change was included in your commit.

Also confirm that soft_fail: false is set in your checkov.yml file. If this is set to true, Checkov will report failures but won't actually block the workflow.

Help me debug my security gate

  • Switch back to your main branch by running:
git checkout main

Your hardened code is still safe on the main branch. The insecure change only lives on the test branch.

Secret mission

Write a Custom Checkov Policy

You have mastered built-in Checkov checks. Now write your own custom YAML policy that enforces a tagging standard across all AWS resources. This is how platform teams codify their organization's rules into automated guardrails.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses no paid cloud services, so there are no ongoing costs. Decide whether to keep your resources for future reference, pause them, or delete them entirely.

Resources you used:

  • Local project folder (terraform-security-scanning).
  • GitHub repository (terraform-security-scanning).
  • Checkov Python package.
  • Terraform CLI binary.

Keep everything running

No action needed. Choose this if you plan to keep building on this project or want to reference the code later.

  • Your local project files stay on your Desktop.
  • Your GitHub repository remains public and shows your DevSecOps skills to anyone who visits your profile.
  • Checkov and Terraform stay installed and ready for future projects.

Pause - I'll come back to this later

Shut down any running processes and free up space while keeping your files intact. Since this project has no running services or background processes, pausing is the same as keeping everything.

  • Your local files and GitHub repository stay as they are.
  • When you come back, open the project folder in VS Code and pick up where you left off.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to fully clean up.

  • Delete your local project folder by running this command in your terminal:
  • On Windows, use this command instead:
  • Delete the checkov-results.json file if you saved it anywhere outside the project folder.
  • Uninstall Checkov by running:
  • Delete the Terraform binary from wherever you placed it in your PATH. On macOS/Linux, this is typically in /usr/local/bin/. On Windows, delete the terraform.exe file from the folder you added to your PATH.
  • Delete your GitHub repository by navigating to your terraform-security-scanning repository on GitHub.
  • Click Settings.
  • Scroll down to the Danger Zone section and click Delete this repository.
  • Follow the confirmation prompts to complete the deletion.

That's a Wrap!

That's a Wrap!

Nice work! You just built a complete Infrastructure as Code security scanning pipeline from scratch.

You've learned how to:

  • Run Checkov static analysis on Terraform configurations and interpret real security findings like missing encryption, unrestricted network access, and instance metadata vulnerabilities.
  • Remediate every vulnerability following AWS security best practices including KMS encryption, public access blocks, least-privilege security groups, and IMDSv2 enforcement.
  • Automate security scanning with GitHub Actions so every push is checked automatically and insecure code is blocked from merging.
  • Write a custom Checkov YAML policy enforcing your own tagging standard across all AWS resources.

Ready to quiz yourself?