Terraform IaC Security Pipeline
Scan Terraform code for misconfigurations before they reach the cloud.
Introduction
30 Second Summary
Teams write infrastructure code to spin up cloud servers, databases, and storage. One misconfiguration in that code can expose an entire company's data to the public internet.
In this project, you will write Terraform infrastructure code with intentional security flaws, run a scanner called Checkov to detect them, then fix every finding until your code passes with a clean report.
What You'll Build
You will run a single terminal command that instantly flags every security vulnerability hiding in your infrastructure code, fix each one, and re-scan to prove your cloud setup is locked down before it ever touches a real environment.
By the end of this project, you'll have:
- A working security scanning workflow where you run one command and instantly see every risk hiding in your Terraform code.
- Hands-on experience fixing real misconfigurations like unencrypted S3 buckets, overly permissive security groups, and wildcard IAM policies.
- Proof that syntactically valid Terraform is NOT the same as secure Terraform, giving you a mental model for shift-left security.
- Secret Mission: Automate the security scan with a GitHub Actions workflow that blocks insecure code from ever being merged.
Are there any prerequisites?
None. This project includes setup instructions for every tool you need. You do not need an AWS account because nothing is deployed to the cloud.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
Install Your Security Toolkit
To scan infrastructure code for security issues, you need two things: a tool to write the infrastructure and a tool to scan it.
In this step, you will install three tools. Terraform writes infrastructure definitions. Python powers the scanner. Checkov runs the security scans.
In this step, get ready to:
- Install Terraform via Homebrew.
- Install Python 3.12 via Homebrew.
- Install Checkov via pip.
Install Terraform via Homebrew
All three tools install through your terminal. Homebrew is the package manager that handles Terraform and Python on macOS.
- In VS Code, click Terminal in the top menu bar.
- Click New Terminal.
- Check that Homebrew is installed by running this command:
brew --version
✔️ I see a version number
Homebrew is ready. Continue to the Terraform install below.
ⓧ Command not found
Homebrew is not installed yet. Install it by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
The script will explain what it does and ask for your confirmation before proceeding. Follow the prompts, then run brew --version again to confirm it works.
Now check if Terraform is already on your machine by running:
terraform --version
✔️ I see version 1.x.x or higher
Terraform is ready. Move on to the next substep (Install Python 3 via Homebrew).
ⓧ I see a version lower than 1.0
Your Terraform version is too old for this project. Install the latest version via Homebrew by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
After the install finishes, run terraform --version again. You should now see v1.x.x.
ⓧ Command not found
Terraform is not installed. Install it via Homebrew by running these commands:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
What do these commands do?
brew tap hashicorp/tap adds HashiCorp's official package repository to Homebrew. brew install hashicorp/tap/terraform downloads and installs the Terraform CLI from that repository.
After the install finishes, run terraform --version again. You should now see v1.x.x.
Still seeing an error after installing?
Close your terminal and open a new one (click Terminal > New Terminal again). Some installs require a fresh terminal session to update your PATH.
Help me troubleshoot my Terraform installation on macOS.
Install Python 3 via Homebrew
Checkov is a Python tool, so it needs Python 3.12 to run. macOS may already have Python installed, but it could be an older version.
- Check your current Python version by running:
python3 --version
✔️ I see version 3.12.x
Python 3.12 is ready. Move on to the next substep (Install Checkov via pip).
ⓧ I see a version lower than 3.12
Your Python version is too old for Checkov. macOS often ships with an older Python that gets used by default. Install Python 3.12 alongside it by running:
brew install python@3.12
After the install completes, run python3 --version again. If it still shows the old version, macOS is pointing python3 at the system Python. Run python3.12 --version to confirm the new install is available. Use pip3.12 instead of pip3 in the next substep if this is the case.
ⓧ Command not found
Python is not installed. Install Python 3.12 by running:
brew install python@3.12
After the install finishes, run python3 --version again. You should see 3.12.x in the output.
Python installed but still showing the old version?
macOS ships with a system Python that shadows Homebrew's version. If python3 --version still shows an older version after installing, try running python3.12 --version directly. You can use python3.12 and pip3.12 throughout the project if needed.
Help me fix my Python version on macOS after installing via Homebrew.
Install Checkov via pip
Checkov is a static analysis tool that scans Terraform files for security misconfigurations. It installs through Python's package manager, pip3.
- Install Checkov by running:
pip3 install checkov
What is happening during this install?
Checkov has many dependencies, so the install may take a minute or two. You will see a stream of "Downloading" and "Installing" messages. Wait until you see "Successfully installed" at the end.
- Confirm Checkov installed correctly by running:
checkov --version
✔️ I see version 3.x.x
Checkov is ready. Your security toolkit is complete.
ⓧ Command not found or an error
If you see "command not found", the install may have placed checkov outside your PATH. Close your terminal and open a new one (click Terminal > New Terminal), then try checkov --version again.
If you used pip3.12 in the previous substep because your system Python was older, run pip3.12 install checkov instead.
Help me troubleshoot my Checkov installation.
Before you move on, do you think all three tools will respond to their version commands without error?
- Run each version command one more time to confirm your full toolkit:
terraform --version
python3 --version
checkov --version
All three commands print version numbers without errors. Your security toolkit is installed and ready.
Your toolkit is locked and loaded. Next up, you will write some intentionally insecure Terraform code and watch what happens when Terraform says it is perfectly valid.
Write Insecure AWS Infrastructure
Your security toolkit is installed and ready to go. Now you need something to scan.
In this step, you will write Terraform code that defines AWS infrastructure. The code will be syntactically valid but riddled with the exact misconfigurations that cause real-world cloud breaches. Public S3 buckets, SSH open to the internet, wildcard IAM permissions. This is your target practice before Checkov gets involved.
In this step, get ready to:
- Create a Terraform project directory and configuration file.
- Write three intentionally insecure AWS resource definitions.
- Prove that Terraform considers insecure code "valid" by running its built-in validator.
Create the project structure
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir terraform-security-lab
cd terraform-security-lab
- Confirm the folder exists by running:
ls ~/Desktop
You should see terraform-security-lab in the output.
- Open this folder in VS Code by running:
code .
VS Code didn't open?
If code . is not recognized, open VS Code manually. Press Cmd+Space (macOS) or the Windows key (Windows), type VS Code, and press Enter.
Then click File in the top menu bar, click Open Folder, and select the terraform-security-lab folder on your Desktop.
Help me set up the VS Code shell command
- Create a new file called main.tf by running:
touch main.tf
- Confirm the file was created by running:
ls
You should see main.tf listed in the output.
Write the Terraform configuration
Every Terraform project starts with a configuration block that tells Terraform which cloud provider to use. After that, you will add three AWS resources that are intentionally insecure.
- Open main.tf in the VS Code editor (click it in the Explorer sidebar) and paste this code:
# Tell Terraform which provider plugin to download
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.0"
}
# Set the AWS region for all resources
provider "aws" {
region = "us-east-1"
}
What does this code do?
- The terraform block tells Terraform to download the AWS provider plugin (version 5.x) from the HashiCorp registry.
- The provider "aws" block sets us-east-1 as the region where resources would be created.
- No AWS credentials are needed for this project. Terraform just downloads the plugin binary so it can parse your resource definitions.
- Save main.tf.
- Download the AWS provider plugin by running this command in your terminal:
terraform init
You should see output ending with Terraform has been successfully initialized! along with a message about the installed provider version.
Seeing an error during init?
- Make sure you saved main.tf before running terraform init. Unsaved changes are invisible to Terraform.
- Check that your terminal is inside the terraform-security-lab directory by running pwd. If not, run cd ~/Desktop/terraform-security-lab.
Help me troubleshoot this terraform init error
Now it is time to add the insecure resources. These are the exact misconfigurations that cause real-world cloud breaches.
- In main.tf, add the following code below your provider block (leave a blank line between them):
# S3 bucket with zero protections - no encryption, versioning, or access controls
resource "aws_s3_bucket" "data_store" {
bucket = "my-app-data-store-2024"
}
# Security group with SSH port 22 open to the entire internet
resource "aws_security_group" "web_server" {
name = "web-server-sg"
description = "Security group for web server"
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
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 makes these resources insecure?
- The S3 bucket has no encryption, no versioning, and no public access block. Anyone who finds the bucket name could potentially access its contents.
- The security group opens SSH (port 22) to 0.0.0.0/0. That means every IP address on the internet can attempt to connect via SSH.
- The HTTPS rule (port 443) open to 0.0.0.0/0 is intentional for a public web server. The danger is SSH being open the same way.
- Save main.tf.
- Now add the final insecure resource below the security group. Paste this at the bottom of main.tf:
# IAM policy granting full access to everything in the AWS account
resource "aws_iam_policy" "app_policy" {
name = "app-limited-access"
description = "Application access policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "*"
Resource = "*"
}
]
})
}
Why is Action = "*" dangerous?
The IAM policy uses Action = "*" and Resource = "*". This grants full permission to do anything to any resource in the entire AWS account.
If an attacker compromises a service using this policy, they can delete databases, spin up crypto miners, or exfiltrate data. This violates the principle of least-privilege: only granting exactly the permissions a service needs.
- Save main.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf and move on to the validation step below.
ⓧ I'd like to double check the full code
Your main.tf should contain these three sections in order. Scroll up and confirm each is present:
- The terraform block and provider "aws" block at the top.
- The aws_s3_bucket resource and aws_security_group resource in the middle.
- The aws_iam_policy resource at the bottom.
Validate the syntax (not security)
Terraform has a built-in validation command that checks whether your code is syntactically correct. Before you run it, think about this question.
Do you think terraform validate will catch the security problems in your code? Will it flag the open SSH port, the unencrypted bucket, or the wildcard IAM policy?
- Run the validation command:
terraform validate
You will see this output:
Success! The configuration is valid.
Terraform says your code is valid. It has no idea the code is dangerously insecure.
Why didn't Terraform catch the security issues?
The terraform validate command only checks syntax and internal consistency. It confirms your HCL is well-formed, your references resolve, and your types are correct.
It does not evaluate whether your infrastructure is safe, encrypted, or follows best practices. That gap is exactly what a security scanner fills.
Seeing a validation error instead?
- Check for missing closing braces } or unclosed quotes in main.tf. Each opening brace needs a matching close.
- Make sure you ran terraform init first. Validation needs the downloaded provider to resolve resource types.
- Confirm your terminal is in the terraform-security-lab directory (run pwd to check).
Help me find the syntax issue in my main.tf
Your infrastructure code is syntactically perfect. Terraform gave it a thumbs up. Next up, you will run Checkov and watch it tear apart every security flaw Terraform ignored.
Run Checkov and See the Failures
In the last step, Terraform told you your configuration is valid. It passed syntax checks without a single warning.
Terraform's validation only checks whether your code can be parsed and is internally consistent. It has no opinion on whether your S3 bucket is publicly accessible or your IAM policy grants full admin access. Checkov fills that blind spot by scanning your .tf files against over 1,000 security policies.
In this step, get ready to:
- Run Checkov to scan your Terraform code for security misconfigurations.
- Read and interpret the scan report to identify what needs fixing.
Run the security scan
Terraform said your code is valid. Before you run Checkov, take a moment to predict: do you think this code will pass a security scan? How many issues might be hiding in those three resources?
- In your terminal, run the Checkov scan against the current directory:
checkov -d .
Your terminal fills with red. Scroll through the output and look at the summary line near the bottom showing how many checks passed and failed.
What does this command do?
The -d . flag tells Checkov to scan all .tf files in the current directory.
Checkov reads the HCL code statically. It never connects to AWS and never deploys anything. No cloud credentials are needed.
Seeing command not found?
If your terminal says checkov: command not found, the pip3 install checkov from Step 1 may not have added Checkov to your PATH. Try running python3 -m checkov.main -d . instead.
help me fix Checkov not being found on my PATH after pip install
Read the scan report
Each finding in the Checkov output follows the same structure. Here is how to read a single finding:
- A check ID like CKV_AWS_24 that uniquely identifies the security rule.
- A human-readable description of what the rule checks for.
- The FAILED for resource: line naming the specific Terraform resource that violated the rule.
- A File: line showing the file path and line numbers where the problem lives.
Look through your output and find these critical failures:
- CKV_AWS_21 flags aws_s3_bucket.data_store for missing versioning.
- CKV_AWS_145 flags the same bucket for missing server-side encryption.
- CKV2_AWS_6 flags the bucket for missing a public access block.
- CKV_AWS_24 flags aws_security_group.web_server for allowing SSH from 0.0.0.0/0 (the entire internet).
You should see 6 or more FAILED checks in total across the S3, security group, and IAM categories.
Why did Terraform say this was fine?
Terraform's terraform validate command checks syntax and internal consistency. It confirms that your HCL can be parsed and that resource references resolve correctly.
It does not check whether your configuration is secure. An S3 bucket with no encryption is perfectly valid HCL. A security group open to the world is syntactically correct. Checkov fills the gap that terraform validate leaves wide open.
What is static security analysis?
Static analysis means examining code without running it. Checkov reads your .tf files as text and matches resource configurations against known-bad patterns. Nothing is deployed, no API calls are made, and no cloud account is touched.
This is the core of shift-left security: catching issues in the blueprint before anything gets built. Fixing a misconfiguration in code costs minutes. Fixing it after a breach costs hours or worse.
The scan told you exactly what is wrong and where to find it. Next up, you will fix every misconfiguration to harden your infrastructure and re-scan to prove the fixes work.
Fix Every Finding and Get a Clean Scan
Checkov just told you exactly where your infrastructure is vulnerable. The scan report is not a vague warning. It names the check ID, the resource, and the line number for every finding.
In this step, you will fix each misconfiguration one category at a time: harden S3, lock down the security group, and apply least-privilege IAM. After each fix, you will re-scan to prove it worked.
In this step, get ready to:
- Harden the S3 bucket with versioning, encryption, and a public access block.
- Lock down the security group to restrict SSH to a private network.
- Apply least-privilege IAM and confirm a clean scan.
Harden the S3 bucket
The scan flagged three problems with your S3 bucket: no versioning (CKV_AWS_21), no encryption (CKV_AWS_145), and no public access block (CKV2_AWS_6). Each fix is a separate Terraform resource that attaches to the existing bucket.
- In main.tf, find the existing aws_s3_bucket resource.
- Add this comment on the line directly above it:
# --- S3 Bucket (Secured) ---
- Below the closing } of the aws_s3_bucket resource, add these three new resources:
resource "aws_s3_bucket_versioning" "data_store" {
bucket = aws_s3_bucket.data_store.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data_store" {
bucket = aws_s3_bucket.data_store.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_public_access_block" "data_store" {
bucket = aws_s3_bucket.data_store.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
What do these resources do?
- aws_s3_bucket_versioning keeps a history of every object change. If someone overwrites or deletes a file, you can recover the previous version.
- aws_s3_bucket_server_side_encryption_configuration encrypts every object at rest using KMS. Even if an attacker accesses the underlying storage, the data is unreadable.
- aws_s3_bucket_public_access_block prevents any policy or ACL from accidentally making the bucket public. All four settings are belt-and-suspenders coverage.
- Save main.tf.
- Verify the S3 fixes by running this command in your terminal:
checkov -d . --check CKV_AWS_21,CKV_AWS_145,CKV2_AWS_6 --compact
You should see all three checks show PASSED in green.
Still seeing FAILED on an S3 check?
- Make sure each new resource references aws_s3_bucket.data_store.id in its bucket field. A typo here means Checkov does not associate the fix with the bucket.
- Confirm all four public access block settings (block_public_acls, block_public_policy, ignore_public_acls, restrict_public_buckets) are set to true.
- help me debug a failing Checkov S3 check in my Terraform code
Lock down the security group
Check CKV_AWS_24 flagged SSH (port 22) open to the entire internet. You will restrict it to a private network range and add description fields to every rule so future readers know the intent.
- In main.tf, find the existing aws_security_group "web_server" resource block.
- Replace the entire resource (from resource "aws_security_group" to its closing }) with this updated version:
# --- EC2 Security Group (Locked Down) ---
resource "aws_security_group" "web_server" {
name = "web-server-sg"
description = "Security group for web server"
ingress {
description = "SSH from trusted private network only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
ingress {
description = "HTTPS from anywhere"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow all outbound traffic"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
What changed in the security group?
- The SSH ingress cidr_blocks changed from 0.0.0.0/0 (the entire internet) to 10.0.0.0/8 (a private network range). Only machines inside that network can reach port 22.
- Every rule now has a description field. This is a Checkov best practice and helps anyone reading the code understand the intent of each rule at a glance.
- Save main.tf.
- Verify the security group fix by running:
checkov -d . --check CKV_AWS_24 --compact
You should see CKV_AWS_24 show PASSED in green.
CKV_AWS_24 still failing?
- Double-check that the SSH ingress block uses cidr_blocks = ["10.0.0.0/8"] and not 0.0.0.0/0. The HTTPS ingress should remain 0.0.0.0/0 since public web traffic is expected.
- help me fix a failing CKV_AWS_24 check for SSH access in my Terraform security group
Apply least-privilege IAM
Your IAM policy currently grants Action = "*" and Resource = "*". That is god-mode access to every AWS service. You will scope it down to only the S3 actions this application actually needs.
- In main.tf, find the existing aws_iam_policy "app_policy" resource block.
- Replace the entire resource (from resource "aws_iam_policy" to its closing }) with this least-privilege version:
# --- IAM Policy (Least Privilege) ---
resource "aws_iam_policy" "app_policy" {
name = "app-limited-access"
description = "Application access policy with least privilege"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket"
]
Resource = [
"arn:aws:s3:::my-app-data-store-2024",
"arn:aws:s3:::my-app-data-store-2024/*"
]
}
]
})
}
What is least privilege?
Least privilege means granting only the permissions an application actually needs to function. Instead of * (every action on every resource), this policy allows only three specific S3 operations on one specific bucket.
The two Resource ARNs target the bucket itself (for ListBucket) and the objects inside it (the /* suffix, for GetObject and PutObject). If this application is compromised, the blast radius is limited to one bucket.
- Save main.tf.
Before you run the final scan, what do you think the result will be? Do you expect all four critical checks to pass now?
- Run the final verification scan targeting all four critical checks:
checkov -d . --check CKV_AWS_145,CKV_AWS_21,CKV2_AWS_6,CKV_AWS_24 --compact
All four checks show PASSED in green. Your infrastructure code went from dangerously misconfigured to hardened, and you proved it with a single command.
Seeing a FAILED check in the final scan?
- Run checkov -d . without the --check flag to see the full report. The detailed output shows the exact file and line number of the failure.
- Confirm that terraform validate still passes. A syntax error (missing brace or comma) can cause Checkov to misparse the file.
- help me figure out why my Checkov scan still shows a FAILED check after applying all the security fixes
✔️ Awesome, I've got everything!
Great. Make sure you have saved main.tf and all four checks are green.
ⓧ I'd like to double check the full code
Your main.tf should now contain these sections in order. Scroll up and confirm each one is present:
- The terraform block and provider "aws" block (unchanged from the previous step).
- The # --- S3 Bucket (Secured) --- comment, followed by the original aws_s3_bucket resource, then the three new resources: aws_s3_bucket_versioning, aws_s3_bucket_server_side_encryption_configuration, and aws_s3_bucket_public_access_block.
- The # --- EC2 Security Group (Locked Down) --- comment, followed by aws_security_group "web_server" with SSH restricted to 10.0.0.0/8 and descriptions on all rules.
- The # --- IAM Policy (Least Privilege) --- comment, followed by aws_iam_policy "app_policy" with three specific S3 actions and two specific resource ARNs.
Your infrastructure code is now hardened. You went from 6+ failed security checks to a clean scan by applying three categories of fixes: encryption and access controls for storage, network restriction for compute, and least-privilege for identity. Next up, you will automate this scan so it runs on every code change without relying on humans to remember.
Secret mission
Automate with GitHub Actions
Running Checkov locally works, but humans forget. In this secret mission, you will create a GitHub Actions workflow that runs Checkov automatically on every pull request, blocking insecure Terraform code from ever being merged. This is the full shift-left pipeline.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally with static code scanning. No cloud resources were provisioned, no AWS account was used, and nothing is running that costs money.
Decide whether to keep your resources on your machine for future use, or delete them entirely.
Resources you used:
- terraform-security-lab directory on your Desktop (includes the .terraform folder with ~200MB AWS provider plugin).
- Checkov Python package (installed via pip3).
- Terraform CLI (installed via Homebrew).
- Python 3.12 (installed via Homebrew).
- GitHub repository (if you completed the Secret Mission).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with Terraform security scanning or want to reuse the project as a template.
- Your terraform-security-lab folder stays on your Desktop.
- Terraform, Python, and Checkov remain installed as general-purpose tools with no ongoing cost.
- Your GitHub repository (if created) remains available for future workflow experiments.
Pause - I'll come back to this later
Since nothing is running in this project, there is nothing to pause. Your files and tools stay on your machine until you decide to delete them.
- No processes to stop. No servers to shut down. No costs accumulating.
- When you are ready to return, open the terraform-security-lab folder in VS Code and pick up where you left off.
Delete - I don't want to use this again
Remove all project files and tools to free up disk space and start fresh.
- Delete the project directory by running this command in your terminal:
rm -rf ~/Desktop/terraform-security-lab
- Uninstall Checkov by running:
pip3 uninstall checkov
- Uninstall Terraform by running:
brew uninstall hashicorp/tap/terraform
- Uninstall Python 3.12 by running:
brew uninstall python@3.12
- Delete your GitHub repository (if you completed the Secret Mission):
- Open your repository page on GitHub.
- Click Settings in the repository navigation bar.
- Scroll down to the Danger Zone section and click Delete this repository.
- Click I want to delete this repository, read the warnings, then type your repository name to confirm.
That's a Wrap!
That's a Wrap!
Nice work! You just built a Terraform project with hardened AWS infrastructure and a local security scanning workflow that catches misconfigurations before deployment.
You've learned how to:
- Write Terraform HCL defining AWS infrastructure from scratch, prove it passes terraform validate, and then reveal with Checkov that syntactically valid code is not the same as secure code.
- Detect real-world cloud misconfigurations (unencrypted S3 buckets, SSH open to the internet, wildcard IAM permissions) and fix every critical finding by applying security best practices like encryption, versioning, public access blocks, CIDR restriction, and least-privilege IAM.
- Complete the scan-fix-verify loop that proves your fixes work, building the muscle memory for shift-left security where catching issues in code costs minutes instead of hours in production.
- Secret Mission: Automated the security scan with a GitHub Actions workflow that blocks insecure code from ever being merged at the PR level.
Ready to quiz yourself?