Security scanning Terraform code: lessons from production
We've all pushed infrastructure code that looked clean until it landed in production and exposed a database bucket to the internet. Security scanning Terraform code catches these misconfigurations before they become incidents—and once you've had to scramble to restrict access at 2 AM, you'll never skip this step again. Let's walk through the tools and practices that protect your cloud infrastructure from day one.
Why scanning matters: the cost of missing it
In our experience across e-commerce and SaaS platforms, the majority of cloud breaches stem from misconfigurations rather than zero-day exploits. Terraform code that provisions S3 buckets, security groups, IAM policies, and databases can embed security debt before a single resource launches. Checkov and Trivy exist to catch these problems during code review, not during an audit or breach notification.
The difference between scanning and not scanning is the difference between catching an open RDS port in a pull request versus discovering it in your AWS Config compliance report three months later. We've seen teams go from zero security tooling to comprehensive scanning in weeks, and the ROI is immediate: fewer late-night fixes, faster deployments because reviewers trust the code, and a measurable reduction in security findings.
Checkov and Trivy: scanning HCL at scale
Checkov is a static code analysis tool built specifically for infrastructure as code. It understands HCL, CloudFormation, Kubernetes manifests, and dozens of other formats. Trivy, developed by Aqua Security, started as a container vulnerability scanner but now includes filesystem and IaC scanning. For Terraform, both are excellent—Checkov tends to have more depth for HCL-specific checks, while Trivy is lighter and faster for quick scans.
Here's how we typically integrate Checkov into a pipeline:
#!/bin/bash
# Run Checkov on all Terraform files
checkov -d . \
--framework terraform \
--quiet \
--compact \
--exit-code 1
# Exit code 1 = failures found, fail the pipeline
# Exit code 0 = all passed
In practice, we run this in CI on every pull request. If Checkov detects a violation, the build fails and the developer sees immediate feedback. For example, if someone creates an S3 bucket without versioning or encryption, Checkov will flag it before the code review even starts.
Trivy offers a similar workflow but with a slightly different interface:
trivy config . \
--format sarif \
--output trivy-results.sarif \
--exit-code 1 \
--severity HIGH,CRITICAL
We prefer Checkov for Terraform-heavy organizations because its rule set is deeper and its false-positive rate is lower. But if you're scanning multiple infrastructure formats and container images in one pipeline, Trivy's unified approach saves operational overhead.
Catching unencrypted storage and open ports
The two most common misconfigurations we catch in scanning are unencrypted storage and overly permissive network rules. Let's walk through real examples.
Unencrypted storage
S3 buckets without encryption-at-rest, RDS databases without encryption, and EBS volumes without encryption are low-hanging security fruit. Checkov rule CKV_AWS_21 flags S3 buckets without encryption. Here's vulnerable code:
resource "aws_s3_bucket" "customer_data" {
bucket = "my-customer-data-${var.environment}"
}
# Checkov finds this and fails the check:
# CKV_AWS_21: Ensure S3 bucket has versioning enabled
# (Also missing encryption!)
The fix is straightforward:
resource "aws_s3_bucket" "customer_data" {
bucket = "my-customer-data-${var.environment}"
}
resource "aws_s3_bucket_versioning" "customer_data" {
bucket = aws_s3_bucket.customer_data.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "customer_data" {
bucket = aws_s3_bucket.customer_data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
Checkov catches this before deployment. Multiply this across 50 resources in a module, and the scanner saves hours of manual review.
Open ports and permissive security groups
Security groups that allow all traffic (0.0.0.0/0) on database ports are equally common and equally dangerous. Checkov rule CKV_AWS_24 flags this:
resource "aws_security_group" "database" {
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Open to the world!
}
}
# Checkov fails: CKV_AWS_24: Restrict ingress RDP access
The fix scopes access to your VPC or a specific bastion host:
resource "aws_security_group" "database" {
vpc_id = var.vpc_id
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.app_servers.id]
}
}
In a real platform, we'd use a database security group that only allows traffic from application servers. Scanning catches the overly permissive rule in CI, not when a customer's data is exposed.
Suppressing findings with justifications
Not every Checkov violation is a real problem. Sometimes you intentionally allow something for operational reasons—a public S3 bucket for static assets, for example. Suppressing with justification is how we keep scanning actionable rather than noisy.
Checkov supports suppression via comments in HCL:
resource "aws_s3_bucket" "public_assets" {
bucket = "my-public-assets-${var.environment}"
#checkov:skip=CKV_AWS_21:Assets are public CDN content, versioning not required
#checkov:skip=CKV_AWS_145:Encryption not needed for public static files
}
resource "aws_s3_bucket_public_access_block" "public_assets" {
bucket = aws_s3_bucket.public_assets.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
#checkov:skip=CKV_AWS_56:Intentionally public bucket for CDN distribution
}
The key is the justification. Anyone reading the code—your future self included—understands why the rule was suppressed. We also maintain a team policy: suppression requires a JIRA ticket or PR comment explaining the business reason. This prevents "skip everything" culture.
For organization-wide suppression (e.g., all environments must encrypt RDS), use a Checkov policy file:
apiVersion: v1
metadata:
name: Require RDS encryption
description: All RDS instances must use encryption at rest
specs:
- CKV_AWS_16
- CKV_AWS_162
enforcement: hard
Running scans on every pull request
Scanning is only effective if it runs automatically and blocks merges when violations are found. We implement this as a GitHub Actions workflow (or equivalent in GitLab CI, Jenkins, etc.):
name: Terraform Security Scan
on: [pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@master
with:
directory: infrastructure/
framework: terraform
quiet: true
compact: true
soft_fail: false # Hard fail on violations
output_format: sarif
output_file_path: reports/checkov.sarif
- name: Upload to GitHub Security Tab
if: always()
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: reports/checkov.sarif
This workflow runs on every pull request, uploads results to GitHub's Security tab so reviewers see violations inline, and fails the check if violations are found. Developers can't merge until Checkov passes (or violations are explicitly suppressed with justification).
We typically run scans in two modes: on pull requests (strict, blocks merge) and on main branch nightly (informational, sends report to Slack). The nightly scan catches any drift or new vulnerability patterns published since the last commit.
Common mistakes
- Suppressing too broadly. Using
soft_fail: truein CI means violations don't block merges, making scanning pointless. Keep it strict. - Never reviewing suppression comments. Suppressions accumulate like technical debt. Audit them quarterly and remove obsolete ones.
- Ignoring policy-as-code extensions. Checkov can enforce custom policies (e.g., "all resources must have a cost center tag"). Many teams miss this and resort to manual review instead.
- Running only on PRs, not production code. If your main branch has unscanned code, you're already exposed. Scan on merge and continuously monitor production infrastructure.
- Using outdated rule sets. Checkov and Trivy release new checks frequently. Run
checkov --updateor update your container image weekly to catch emerging patterns.
Key takeaways
- Security scanning Terraform code catches misconfigurations early, before they reach production or audit findings.
- Checkov excels at HCL scanning; Trivy offers lighter scanning for mixed infrastructure and container artifacts.
- Focus scanning on high-impact violations: unencrypted storage, open ports, overly permissive IAM, missing monitoring and logging.
- Suppress violations only with explicit justifications; review suppressions regularly to prevent accumulation.
- Integrate scanning into CI/CD to block merges; pair with nightly scans on main to catch drift and new vulnerabilities.
- Treat scanning as continuous: update rule sets weekly, audit findings weekly, and measure violation trends over time.






Comments
Loading comments…