Secure AWS Infrastructure with Checkov

Scan, fix, and harden Terraform configs using automated security checks.

Introduction

30 Second Summary

Every cloud security breach starts with a misconfiguration that nobody caught in time. What if a single command could flag those mistakes before your code ever reaches production?

In this project, you will write Terraform infrastructure code, scan it with Checkov to find security vulnerabilities, and fix each one until the scan comes back completely clean.

What You'll Build

You run a single scan command against your infrastructure code, see a clean bill of health for every resource, and know that your S3 bucket and security group meet every security best practice before they ever touch a cloud account.

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

  • A scan-fix-rescan workflow where you run one command to detect insecure infrastructure, fix the flagged issues, and confirm the fix passes in seconds.
  • A hardened S3 bucket with server-side encryption, versioning, and public access blocking that passes every Checkov check.
  • A locked-down security group that restricts SSH access to a specific network instead of the entire internet.
  • Secret Mission: A custom Checkov policy written in Python that enforces your team's naming convention across all Terraform resources.

Are there any prerequisites?

Familiarity with cloud security concepts like S3 policies, security groups, and encryption at rest is helpful. No prior Terraform or Python experience is required.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. This step is your moment to commit to the goal so every decision in the steps ahead has a clear purpose.

Set Up Your Toolchain

To scan infrastructure code for security issues, you need three tools working together. Terraform provides the language for writing Infrastructure as Code. Checkov (a Python tool) scans that Terraform code and flags security problems automatically.

In this step, you'll install all three tools and create the project folder where your Terraform files will live.

In this step, get ready to:
  • Install VS Code and create the secure-infra project folder.
  • Install Terraform for writing infrastructure code.
  • Install Python and Checkov for automated security scanning.
Install VS Code and create your project folder

VS Code is the code editor you'll use to write and edit Terraform configuration files throughout this project.

  • Check if you already have VS Code installed by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Visual Studio Code and press Enter.

✔️ VS Code opens

Great, VS Code is ready to go.

ⓧ Nothing found

  • Download VS Code from https://code.visualstudio.com.
  • Run the installer and follow the on-screen prompts.
  • Once installed, open VS Code using the search bar method above.

Now you need a folder to hold your Terraform files.

  • Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), typing Terminal (macOS) or PowerShell (Windows), and pressing Enter.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir secure-infra
cd secure-infra
  • Confirm the folder exists by running:
pwd

You should see a path ending in secure-infra (for example, /Users/yourname/Desktop/secure-infra).

  • Switch back to VS Code.
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the secure-infra folder, and click Open.
  • Open VS Code's built-in terminal by clicking Terminal in the top menu, then New Terminal.

Your terminal prompt should show that you are inside the secure-infra folder.

Terminal not showing secure-infra?

If the terminal opened to a different directory, run cd ~/Desktop/secure-infra to get there manually.

If VS Code asked you to trust the folder, click Yes, I trust the authors to proceed.

Help me troubleshoot this

Install and verify Terraform

Terraform is the tool that lets you define cloud infrastructure in code files. You will not deploy anything to the cloud in this project, but you need Terraform installed so it can validate your configuration syntax.

  • Check if Terraform is already installed by running this command in your VS Code terminal:
terraform version

✔️ I see version 1.0 or higher

You should see output like Terraform v1.15.8 (your exact version may differ). Any version starting with v1 or higher works for this project.

ⓧ I see version 0.x

Your Terraform version is outdated. This project requires Terraform 1.0 or higher. Uninstall your current version and follow the instructions in the Command not found tab to install the latest.

ⓧ Command not found

Terraform is not installed yet. Follow the instructions for your operating system below.

macOS

  • Install Terraform using Homebrew by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
Homebrew not installed?

If you see command not found: brew, install Homebrew first from https://brew.sh and then re-run the commands above.

Help me install Homebrew

Windows

  • Open a browser and go to https://developer.hashicorp.com/terraform/install.
  • Download the Windows AMD64 binary.
  • Extract the terraform.exe file to a folder such as C:\terraform.
  • Add that folder to your system PATH environment variable.
  • Close and reopen your terminal, then run terraform version to confirm the install.
Use a package manager instead

If you have Chocolatey installed, you can run choco install terraform instead of downloading manually.

Why no AWS account?

This project uses mock credentials in the Terraform provider configuration. Terraform can validate syntax and structure without connecting to AWS.

Checkov scans .tf files statically, so no cloud access is needed at all.

Install Python and Checkov

Checkov is a Python-based security scanner. You need Python 3.9+ installed before you can install Checkov via pip3.

  • Check your Python version by running:
python3 --version

✔️ I see version 3.9 or higher

You should see output like Python 3.11.4 or similar. Any version from 3.9 to 3.13 works with Checkov.

ⓧ I see a version lower than 3.9

Checkov requires Python 3.9 or higher. Your current version is too old.

macOS

macOS sometimes ships with an older system Python. Install a newer version from https://www.python.org/downloads/.

After installing, run python3 --version again. If it still shows the old version, use the full path (for example /Library/Frameworks/Python.framework/Versions/3.13/bin/python3) or install via brew install python@3.13.

Windows

  • Download the latest Python from https://www.python.org/downloads/.
  • Run the installer and check Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python3 --version to confirm.

ⓧ Command not found

Python is not installed yet. Follow the instructions for your operating system below.

macOS

Download the latest Python installer from https://www.python.org/downloads/. Run the installer package and follow the prompts.

After installation completes, close and reopen your terminal, then run python3 --version to confirm.

Windows

  • Download the latest Python from https://www.python.org/downloads/.
  • Run the installer and check Add python.exe to PATH before clicking Install Now.
  • Close and reopen your terminal, then run python3 --version to confirm.

With Python confirmed, install Checkov.

  • Install Checkov by running:
pip3 install checkov

What is happening during the install?

Checkov has many dependencies, so pip3 will download and install several packages. This may take a minute or two. Wait until you see the Successfully installed line before continuing.

Seeing a permission error during pip install?

If pip3 install checkov fails with a permissions error, try running pip3 install --user checkov instead. This installs to your user directory without needing admin access.

Help me fix this permission error

Before you run the next command, what do you expect to see if Checkov installed correctly?

  • Verify Checkov is working by running:
checkov --version

You should see a version number like 3.3.8 (your exact number may be slightly different, but it should start with 3).

✔️ I see a version number

Checkov is installed and ready. Your security scanning toolchain is complete.

ⓧ Command not found

This usually means the pip3 install directory is not in your terminal's PATH.

  • Try running python3 -m checkov --version instead.
  • If that works, use python3 -m checkov in place of checkov for the rest of this project.
  • If both fail, close and reopen your terminal, then try checkov --version again.

Help me fix my PATH

Your toolchain is ready. You have Terraform for writing infrastructure code, and Checkov for scanning it. Next up, you'll write your first Terraform configuration and give Checkov something to scan.

Write Your First (Insecure) Terraform Configuration

Checkov needs Terraform code to scan. Before it can find security problems, there must be infrastructure code with problems to find.

In this step, you will write a deliberately insecure S3 bucket configuration. No encryption, no versioning, no public access block. This gives Checkov something meaningful to flag when you run your first scan in the next step.

In this step, get ready to:
  • Create main.tf with a Terraform provider and a bare S3 bucket.
  • Create variables.tf with project variables.
  • Initialize the project with terraform init.
Create the Terraform provider and S3 bucket

Every Terraform project needs at least two things: a provider block that says which cloud platform you are targeting, and a resource block that defines what you want to build.

  • In VS Code, hover over the Explorer sidebar on the left.
  • Click the New File icon at the top of the sidebar.
  • Name the file main.tf and press Enter.
  • Paste the following code into main.tf:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  required_version = ">= 1.0"
}

provider "aws" {
  region = var.region

  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  access_key = "mock"
  secret_key = "mock"
}

resource "aws_s3_bucket" "data_bucket" {
  bucket = var.bucket_name
}

What does this code do?

This file has three sections, each with a distinct job:

  • The terraform block declares that you need the AWS provider (version 5.x) and Terraform 1.0 or higher.
  • The provider "aws" block configures that provider with mock credentials so Terraform can initialize without a real AWS account.
  • The resource "aws_s3_bucket" block defines a single S3 bucket with nothing else attached. No encryption, no versioning, no public access block.

Why mock credentials?

You will never run terraform apply in this project. You only need Terraform to parse and validate the .tf files.

Checkov scans your configuration files statically. It reads the code and checks it against security policies without ever connecting to AWS.

  • Save main.tf with Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm that main.tf now appears in the Explorer sidebar on the left.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf before moving on.

ⓧ I'd like to double check the full code

Your main.tf should look exactly like this:

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

provider "aws" {
  region = var.region

  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  access_key = "mock"
  secret_key = "mock"
}

resource "aws_s3_bucket" "data_bucket" {
  bucket = var.bucket_name
}
Define your project variables

The main.tf file references var.region and var.bucket_name. Terraform needs a separate file to know what those variables are and what default values to use.

  • Click the New File icon in the Explorer sidebar again.
  • Name the file variables.tf and press Enter.
  • Paste the following code into variables.tf:
variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
  default     = "secure-data-bucket-2024"
}

variable "region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}

What does this code do?

  • bucket_name sets the S3 bucket's name to secure-data-bucket-2024 by default. Terraform plugs this value into var.bucket_name wherever it appears.
  • region tells the AWS provider which region to target. The default us-east-1 is used by the provider block in main.tf.
  • Save variables.tf with Cmd+S (macOS) or Ctrl+S (Windows).
  • Confirm that both main.tf and variables.tf appear in the Explorer sidebar.

✔️ Awesome, I've got everything!

Great. Double check you have saved variables.tf before moving on.

ⓧ I'd like to double check the full code

Your variables.tf should look exactly like this:

variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
  default     = "secure-data-bucket-2024"
}

variable "region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"
}
Initialize the Terraform project

Terraform needs to download the AWS provider plugin before it can parse your resource blocks. The terraform init command reads your required_providers block and fetches everything it needs.

Before you run this, what do you think will happen? Will Terraform complain about the mock credentials, or will it initialize successfully?

  • Initialize your project by running this command in the terminal:
terraform init

You should see Terraform download the AWS provider and print:

Terraform has been successfully initialized!

Terraform only validates syntax and downloads providers during init. It does not try to connect to AWS, so the mock credentials work perfectly.

Seeing an error during init?

  • Make sure your terminal is inside the secure-infra folder. Run pwd to check your current directory.
  • Check that main.tf is saved. Terraform reads from disk, not from unsaved editor buffers.
  • If you see a syntax error, compare your code to the double-check tab above and look for missing braces or quotes.

Still stuck? Help me debug a terraform init error in my secure-infra project.

Your configuration files are in place and Terraform confirms they are syntactically valid. Next up, you will point Checkov at this code and watch it light up with security failures.

Run Checkov and See It Light Up

Your Terraform project is initialized and your bare S3 bucket configuration is sitting in main.tf.

Now it's time to unleash Checkov on your code and see what it catches. Checkov scans your .tf files against hundreds of security policies and reports exactly which rules each resource violates. A manual security review could take hours. Checkov does it in seconds.

In this step, get ready to:
  • Run your first Checkov security scan against your Terraform project.
  • Read and interpret the scan output format.
  • Filter a scan to a single check ID.
Run your first Checkov scan

Your S3 bucket has no encryption, no versioning, and no public access protection. Before you run the scan, think about it: how many security issues do you think Checkov will catch on a completely bare bucket?

  • Run the Checkov scan against your project directory:
checkov -d .

You'll see a wall of output. Scroll through it and look for lines marked FAILED. You should see multiple failed checks including CKV_AWS_18 (no access logging), CKV_AWS_19 (no encryption at rest), and CKV_AWS_21 (no versioning).

What does this output mean?

Each failed check in the output follows a consistent format. Here is what each piece tells you:

  • Check ID and description (e.g. CKV_AWS_21: "Ensure all data stored in the S3 bucket have versioning enabled") identifies which security rule was violated.
  • FAILED for resource (e.g. aws_s3_bucket.data_bucket) tells you exactly which Terraform resource triggered the failure.
  • File and line range (e.g. /main.tf:23-25) points you to the exact location in your code.
  • Guide link provides a URL to documentation explaining why the check matters and how to fix it.

Seeing "command not found"?

  • Make sure you installed Checkov with pip3 install checkov in the previous step.
  • Verify the install worked by running checkov --version. If that also fails, your Python scripts directory may not be on your PATH.
  • Try running python3 -m checkov -d . as an alternative invocation.

Why is Checkov not running my scan?

Examine the output format

Scroll back up to the top of your terminal output. At the very top, Checkov prints a summary line showing how many checks passed, failed, and were skipped.

  • Find the summary line at the bottom of the output that reads something like Passed checks: X, Failed checks: Y, Skipped checks: 0.
  • Count the number of FAILED entries. Each one represents a security best practice your bucket does not follow.
  • Notice that for each failure, Checkov prints the offending Terraform code snippet with line numbers so you can jump straight to the problem.

Why so many failures for one resource?

A single aws_s3_bucket resource gets evaluated against every S3-related security policy in Checkov's library. Each policy checks for one specific best practice (encryption, versioning, logging, public access). Your bare bucket has none of these protections, so it fails every one.

This is exactly what a security reviewer would flag during a pull request review. Checkov just does it instantly and automatically.

Filter to a specific check

The full scan output is useful for a complete picture, but sometimes you want to focus on one check at a time. The --check flag lets you run only the checks you specify.

  • Run Checkov filtered to just the encryption-at-rest check by running:
checkov -d . --check CKV_AWS_19

You should see a single FAILED entry for CKV_AWS_19 targeting aws_s3_bucket.data_bucket. The output confirms your bucket has no server-side encryption configured.

Why filter to one check?

When you fix a specific issue, filtering to that check's ID lets you quickly confirm your fix worked without scrolling through the entire scan output. You'll use this pattern in the next step as you harden the bucket one policy at a time.

Seeing 0 failed checks?

  • Double check the check ID is spelled exactly as CKV_AWS_19 (all uppercase, underscores, no spaces).
  • Make sure you are running the command from inside the secure-infra folder where your main.tf lives.
  • Verify your main.tf still contains the bare aws_s3_bucket resource with no encryption configuration.

Why is my filtered Checkov scan not showing any failures?

You now have a clear picture of every security gap in your infrastructure code. The failures are your to-do list. Next up, you'll fix each one and watch the failures disappear.

Harden the S3 Bucket Until Checkov Passes

In the previous step, Checkov told you exactly what is wrong with your S3 bucket. Each failure came with a check ID and a description of the misconfiguration.

Now you fix each issue one at a time and re-scan after every change to confirm the failure disappears. This iterative scan-fix-rescan loop is the core DevSecOps workflow you will use on every project.

In this step, get ready to:
  • Add encryption and versioning to your S3 bucket.
  • Block all public access to the bucket.
  • Confirm a clean Checkov scan for all targeted S3 security checks.
Add server-side encryption

Checkov check CKV_AWS_19 requires that all data stored in the S3 bucket is encrypted at rest. You will add a separate encryption configuration resource that references your existing bucket.

  • In main.tf, find the closing brace of your aws_s3_bucket "data_bucket" resource.
  • Add the following encryption resource below it:
resource "aws_s3_bucket_server_side_encryption_configuration" "data_bucket_encryption" {
  bucket = aws_s3_bucket.data_bucket.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

What does this code do?

This resource tells AWS to automatically encrypt every object stored in the bucket using the AES256 algorithm.

The bucket = aws_s3_bucket.data_bucket.id line links this encryption config to your existing bucket. Terraform uses separate resources for bucket settings because the older inline blocks are deprecated.

  • Save main.tf.
  • Confirm the encryption check passes by running this command:
checkov -d . --check CKV_AWS_19

You should see PASSED for CKV_AWS_19 on your aws_s3_bucket_server_side_encryption_configuration resource.

Still seeing CKV_AWS_19 FAILED?

  • Make sure you saved main.tf before running the scan.
  • Check that your sse_algorithm value is exactly "AES256" (all caps, inside quotes).
  • Verify the bucket field references aws_s3_bucket.data_bucket.id so Checkov can link the config to the bucket.

Help me debug why CKV_AWS_19 is still failing after I added the encryption resource.

Add bucket versioning

Checkov check CKV_AWS_21 requires that versioning is enabled so deleted or overwritten objects can be recovered.

  • In main.tf, add the following versioning resource below the encryption resource you just created:
resource "aws_s3_bucket_versioning" "data_bucket_versioning" {
  bucket = aws_s3_bucket.data_bucket.id

  versioning_configuration {
    status = "Enabled"
  }
}

What does this code do?

Versioning keeps a history of every object version in the bucket. If someone accidentally deletes or overwrites a file, you can restore a previous version.

The status = "Enabled" value turns versioning on. The only other valid values are Suspended and Disabled.

  • Save main.tf.
  • Confirm both encryption and versioning checks now pass by running:
checkov -d . --check CKV_AWS_19,CKV_AWS_21

You should see PASSED for both CKV_AWS_19 and CKV_AWS_21.

CKV_AWS_21 still failing?

  • Make sure status is set to "Enabled" with a capital E. Lowercase "enabled" will not satisfy the check.
  • Confirm the resource type is aws_s3_bucket_versioning (not an inline versioning block inside the bucket resource).

Help me figure out why CKV_AWS_21 is still failing after I added versioning.

Block public access to the bucket

Four Checkov checks (CKV_AWS_53, CKV_AWS_54, CKV_AWS_55, CKV_AWS_56) each enforce one aspect of the S3 public access block. You will satisfy all four with a single resource that sets every flag to true.

  • In main.tf, add the following public access block resource below the versioning resource:
resource "aws_s3_bucket_public_access_block" "data_bucket_public_access" {
  bucket = aws_s3_bucket.data_bucket.id

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

What do these four flags do?

  • block_public_acls prevents anyone from applying a public ACL to the bucket or its objects.
  • block_public_policy rejects any bucket policy that grants public access.
  • ignore_public_acls overrides existing public ACLs so they have no effect.
  • restrict_public_buckets limits access to the bucket to only AWS-authenticated principals, even if a public policy exists.

Together these four flags form a complete shield against accidental public exposure.

  • Save main.tf.
  • Confirm all four public access checks now pass by running:
checkov -d . --check CKV_AWS_53,CKV_AWS_54,CKV_AWS_55,CKV_AWS_56

You should see PASSED for all four checks on your aws_s3_bucket_public_access_block resource.

One or more public access checks still failing?

  • Double check that all four flags (block_public_acls, block_public_policy, ignore_public_acls, restrict_public_buckets) are set to true (not "true" in quotes).
  • Make sure the resource type is aws_s3_bucket_public_access_block and the bucket field points to aws_s3_bucket.data_bucket.id.

Help me debug why one of the public access checks is still failing.

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf and all three new resources are in place below your original bucket resource.

ⓧ I'd like to double check the full code

Scroll up through your work and confirm these resources appear in main.tf in this order:

  • The terraform block and provider "aws" block at the top.
  • aws_s3_bucket "data_bucket" with bucket = var.bucket_name.
  • aws_s3_bucket_server_side_encryption_configuration "data_bucket_encryption" with sse_algorithm = "AES256".
  • aws_s3_bucket_versioning "data_bucket_versioning" with status = "Enabled".
  • aws_s3_bucket_public_access_block "data_bucket_public_access" with all four flags set to true.
Confirm all S3 checks pass

You have added encryption, versioning, and a public access block. Time to confirm the two headline checks that started this step are fully resolved.

Before you run the command below, what do you expect the result to be for CKV_AWS_19 and CKV_AWS_21? Will they pass or fail?

  • Run the following command to verify both checks pass together:
checkov -d . --check CKV_AWS_19,CKV_AWS_21

Both checks show PASSED for your S3 resources. Every security gap Checkov identified at the start of this step is now closed.

Why filter with --check instead of scanning everything?

The --check flag lets you focus on specific check IDs. This is useful when you are iterating on fixes and do not want to scroll through unrelated results.

A full checkov -d . scan may still show other checks (like CKV_AWS_18 for access logging) that are outside the scope of this step. Filtering keeps the feedback loop tight.

Your S3 bucket is now hardened with encryption, versioning, and a full public access block. Next up, you will add a security group with an intentionally permissive rule and watch Checkov catch that too.

Add and Harden a Security Group

Your S3 bucket is fully locked down. Every Checkov storage check comes back clean.

Storage security is only half the picture. Security groups control who can reach your infrastructure over the network. A security group that allows SSH from anywhere is one of the most common cloud misconfigurations. In this step, you will add one with intentionally weak rules, watch Checkov catch the problems, and then fix them.

In this step, get ready to:
  • Add a security group with intentionally insecure SSH rules.
  • Observe Checkov flag the network security violations.
  • Fix the security group until the entire project passes with 0 failures.
Add an insecure security group

You are going to write a security group that allows SSH (port 22) from any IP address on the internet, and omit all descriptions. This is deliberately insecure. You want to see what Checkov catches before you fix anything.

  • In VS Code, open main.tf.
  • Scroll to the bottom of the file, below the aws_s3_bucket_public_access_block resource.
  • Add the following insecure security group resource:
resource "aws_security_group" "app_sg" {
  name   = "app-security-group"
  vpc_id = "vpc-placeholder"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

What does this code do?

This creates a virtual firewall (security group) for an application server. The ingress block allows inbound SSH traffic on port 22 from 0.0.0.0/0, which means any IP address in the world can connect.

The egress block allows all outbound traffic. Notice there are no description fields anywhere. Both of these are real-world security anti-patterns that Checkov is built to catch.

  • Save main.tf.

Before you run Checkov, think about what you expect it to flag. You have SSH open to the entire internet and no descriptions on any rule. What check IDs do you think will fail?

  • Run Checkov against your project by running this command:
checkov -d .

You will see new FAILED checks appear for your security group resource. Look for these two in the output:

  • CKV_AWS_24: "Ensure no security groups allow ingress from 0.0.0.0:0 to port 22" - your SSH rule is wide open.
  • CKV_AWS_23: "Ensure every security group and rule has a description" - you omitted all descriptions.

This is exactly what you wanted to see. Checkov caught both problems in seconds.

Why start with insecure code on purpose?

This is the same scan-fix-rescan workflow you used for S3. Starting with a known-bad configuration proves that Checkov actually detects the issue. If you started with the correct code, you would never know whether the scanner was really watching.

Not seeing CKV_AWS_24 in the output?

Make sure the cidr_blocks in your ingress block is exactly ["0.0.0.0/0"]. A typo in the CIDR notation (like 0.0.0/0) can prevent Checkov from matching the pattern.

Also confirm from_port and to_port are both set to 22. If the port range does not include 22, CKV_AWS_24 will not trigger.

help me figure out why Checkov is not detecting my insecure security group

Fix the security group

Now you know exactly what is wrong. You will restrict the SSH CIDR to an internal network range, and add descriptions to the security group itself and to every rule.

  • In main.tf, find the aws_security_group resource you just added.
  • Replace the entire aws_security_group block with this hardened version:
resource "aws_security_group" "app_sg" {
  name        = "app-security-group"
  description = "Security group for application servers with restricted SSH access"
  vpc_id      = "vpc-placeholder"

  ingress {
    description = "SSH access from internal network"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/8"]
  }

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

What changed?

  • Added a description to the security group itself explaining its purpose.
  • Changed cidr_blocks from 0.0.0.0/0 (the entire internet) to 10.0.0.0/8 (a private internal network range). Only machines on the internal network can SSH in.
  • Added a description to both the ingress and egress rules, so anyone reading this config understands each rule's purpose.
  • Save main.tf.

Seeing a Terraform syntax error?

Make sure you replaced the entire old aws_security_group block, including its closing }. A leftover brace or duplicate resource declaration causes Terraform to reject the file.

help me fix a Terraform syntax error in my security group

Confirm a fully clean scan

Time for the moment of truth. You are about to scan the entire project, covering both your S3 bucket and security group. Do you think all checks will pass now?

  • Run the full Checkov scan by running this command:
checkov -d .

You should see 0 failed checks in the output. Every S3 check and every security group check reports PASSED.

What does a clean scan prove?

A fully clean Checkov scan means your infrastructure code meets baseline security standards across storage and networking. In a real CI/CD pipeline, this scan would run automatically on every pull request, blocking insecure code from reaching production.

Still seeing failed checks?

If you see CKV_AWS_23 still failing, check that every ingress and egress block has a description field. Checkov checks both the security group level and each individual rule.

If CKV_AWS_24 still fails, confirm the ingress cidr_blocks is ["10.0.0.0/8"] and not still ["0.0.0.0/0"].

help me figure out why Checkov still shows failed security group checks

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf and your scan is clean.

ⓧ I'd like to double check the full code

Your main.tf should contain these sections in order. Scroll through your file and confirm each one is present:

  • The terraform block with required_providers (hashicorp/aws ~> 5.0) and required_version.
  • The provider "aws" block with mock credentials and skip flags.
  • The aws_s3_bucket resource named data_bucket.
  • The aws_s3_bucket_server_side_encryption_configuration resource named data_bucket_encryption with AES256.
  • The aws_s3_bucket_versioning resource named data_bucket_versioning with status Enabled.
  • The aws_s3_bucket_public_access_block resource named data_bucket_public_access with all four flags set to true.
  • The aws_security_group resource named app_sg with a description, restricted ingress (10.0.0.0/8), and descriptions on all rules.

Your variables.tf should be unchanged from the previous step, containing bucket_name and region variables.

You have proven that the scan-fix-rescan workflow applies to network security just as well as it does to storage. Your project now passes every Checkov check across both resource types. Next up, you will write your own custom Checkov policy to enforce rules that are unique to your team.

Secret mission

Write a Custom Checkov Policy

Built-in checks cover common best practices, but every team has unique rules. Can you write your own Checkov policy in Python that enforces a naming convention across all your S3 buckets?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally. No cloud resources were created and there are no ongoing costs. Decide whether to keep your project files for future reference or delete them entirely.

Resources you used:

  • secure-infra project folder (contains main.tf, variables.tf, custom_checks/, and the .terraform/ directory).
  • Terraform CLI (installed via Homebrew or binary download).
  • Checkov (installed via pip3).

Keep everything running

No action needed. Choose this if you plan to keep experimenting with Terraform and Checkov, or want to reference your hardened configurations later.

  • Your secure-infra folder stays on your Desktop with all files intact.
  • Terraform and Checkov remain installed and available from any terminal session.

Pause - I'll come back to this later

Since everything is local files with no running processes, there is nothing to pause. Your project folder will be waiting whenever you return.

  • No servers, containers, or background processes are running from this project.
  • You can close VS Code and your terminal. The files persist on disk.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the tools you installed for this project.

  • Delete the project folder by running this command in your terminal:

macOS

rm -rf ~/Desktop/secure-infra

Windows

Remove-Item -Recurse -Force ~\Desktop\secure-infra
  • Uninstall Checkov by running:
pip3 uninstall checkov

Type y and press Enter when prompted to confirm.

  • Uninstall Terraform (macOS with Homebrew) by running:
brew uninstall hashicorp/tap/terraform

What about Windows or manual installs?

If you installed Terraform by downloading the binary directly, delete the terraform executable from wherever you placed it (check with which terraform on macOS/Linux or where terraform on Windows).

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully hardened Terraform project with automated security scanning that catches misconfigurations before they ever reach production.

You've learned how to:

  • Write Terraform configurations for an S3 bucket and security group, then iteratively harden them using the scan-fix-rescan workflow that DevSecOps teams rely on daily.
  • Use Checkov to automatically detect security misconfigurations like missing encryption, disabled versioning, open public access, and unrestricted SSH.
  • Implement the shift-left security pattern by catching cloud misconfigurations in code instead of waiting until after deployment.
  • Write a custom Checkov policy in Python that enforces a team-specific naming convention across all your Terraform resources.

Ready to quiz yourself?