{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreigxqrtpky2ukilegcrw76aftomoblpuzhdrvo6lzmuchu4ilzzyca",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpubj55qwn72"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreifjbmbziw6t5rg4kpb42ufzuqhpjjmy4ed7qtlfxwtedbsupg7nt4"
},
"mimeType": "image/webp",
"size": 77000
},
"path": "/dayan_elvisjahuirapilco/your-infrastructure-has-bugs-too-scanning-terraform-with-checkov-iac-sast-5fdk",
"publishedAt": "2026-07-04T23:24:24.000Z",
"site": "https://dev.to",
"tags": [
"security",
"terraform",
"devsecops",
"aws",
"Checkov",
"Source Code Analysis Tools",
"GitHub repo →",
"previous article",
"github.com/Dayan-18/checkov-demo",
"OWASP Source Code Analysis Tools",
"Checkov documentation",
"Terraform AWS provider docs"
],
"textContent": "> **TL;DR:** Application code isn't the only thing that ships vulnerabilities — your Terraform does too. I wrote an intentionally insecure AWS configuration, scanned it with Checkov (a SAST tool for Infrastructure as Code, listed on the OWASP Source Code Analysis Tools page), went from **35 failed checks to 0** , and wired the scan into GitHub Actions. Full code: **GitHub repo →**\n\n## SAST for infrastructure? Yes, that's a thing\n\nIn my previous article I used Bandit to find security bugs in Python code. But modern applications are deployed with **Infrastructure as Code** (Terraform, Pulumi, OpenTofu, CloudFormation) — and a misconfigured S3 bucket has caused more real-world data breaches than most code vulnerabilities.\n\nThe good news: since infrastructure is now _code_ , it can be statically analyzed like code. That's exactly what **Checkov** does: an open-source tool (Python, maintained by Prisma Cloud) with 1,000+ built-in policies that scans Terraform, CloudFormation, Kubernetes manifests, Dockerfiles, and more — no cloud credentials needed, it never touches your AWS account.\n\n## The target: a deliberately insecure AWS stack\n\nI wrote a `main.tf` that concentrates the misconfigurations behind many famous breaches:\n\n\n\n # Issue 1: S3 bucket - public, unencrypted, no versioning, no logging\n resource \"aws_s3_bucket\" \"data\" {\n bucket = \"company-customer-data-bucket\"\n acl = \"public-read\"\n }\n\n # Issue 2: security group open to the entire internet, including SSH\n resource \"aws_security_group\" \"web\" {\n name = \"web-sg\"\n\n ingress {\n from_port = 22\n to_port = 22\n protocol = \"tcp\"\n cidr_blocks = [\"0.0.0.0/0\"]\n }\n\n ingress {\n from_port = 0\n to_port = 65535\n protocol = \"tcp\"\n cidr_blocks = [\"0.0.0.0/0\"]\n }\n }\n\n # Issue 3: database publicly accessible, unencrypted, hardcoded password\n resource \"aws_db_instance\" \"db\" {\n identifier = \"app-db\"\n engine = \"mysql\"\n instance_class = \"db.t3.micro\"\n allocated_storage = 20\n username = \"admin\"\n password = \"SuperSecret123!\"\n publicly_accessible = true\n storage_encrypted = false\n skip_final_snapshot = true\n }\n\n # Issue 4: EC2 instance without IMDSv2 and with unencrypted root volume\n resource \"aws_instance\" \"app\" {\n ami = \"ami-0c55b159cbfafe1f0\"\n instance_type = \"t3.micro\"\n\n vpc_security_group_ids = [aws_security_group.web.id]\n\n root_block_device {\n encrypted = false\n }\n }\n\n # Issue 5: IAM policy with full admin wildcard\n resource \"aws_iam_policy\" \"admin\" {\n name = \"app-policy\"\n\n policy = jsonencode({\n Version = \"2012-10-17\"\n Statement = [{\n Effect = \"Allow\"\n Action = \"*\"\n Resource = \"*\"\n }]\n })\n }\n\n\nFive resources. Looks harmless, right? Let's see.\n\n## Running Checkov\n\n\n pip install checkov\n checkov -f main.tf\n\n\nThe verdict, in a few seconds and without touching AWS:\n\n\n\n Passed checks: 14, Failed checks: 35, Skipped checks: 0\n\n\n**35 failed checks** in 70 lines of Terraform. Some highlights:\n\n### 🔴 The public S3 bucket (the classic breach)\n\n\n Check: CKV_AWS_20: \"S3 Bucket has an ACL defined which allows public READ access.\"\n FAILED for resource: aws_s3_bucket.data\n Check: CKV2_AWS_6: \"Ensure that S3 bucket has a Public Access block\"\n FAILED for resource: aws_s3_bucket.data\n Check: CKV_AWS_145: \"Ensure that S3 buckets are encrypted with KMS by default\"\n FAILED for resource: aws_s3_bucket.data\n\n\nA bucket named `company-customer-data-bucket` with `acl = \"public-read\"` — this exact pattern exposed hundreds of millions of records in real incidents (Capital One, Accenture, the US voter records leak...).\n\n### 🔴 The god-mode IAM policy\n\n\n Check: CKV_AWS_62: \"Ensure IAM policies that allow full \"*-*\" administrative\n privileges are not created\"\n FAILED for resource: aws_iam_policy.admin\n Check: CKV_AWS_286: \"Ensure IAM policies does not allow privilege escalation\"\n FAILED for resource: aws_iam_policy.admin\n Check: CKV_AWS_288: \"Ensure IAM policies does not allow data exfiltration\"\n FAILED for resource: aws_iam_policy.admin\n\n\n`Action = \"*\"` on `Resource = \"*\"` earned **eight** failed checks by itself: privilege escalation, credentials exposure, data exfiltration... one wildcard, every attack path.\n\n### 🔴 SSH open to the world + public database\n\nCheckov also flagged the security group allowing `0.0.0.0/0` on port 22 (CKV_AWS_24), the RDS instance with `publicly_accessible = true` (CKV_AWS_17), no storage encryption (CKV_AWS_16), and the **hardcoded database password** sitting in version control.\n\n## Fixing it\n\nThe remediated `main_fixed.tf` applies standard hardening:\n\nMisconfiguration | Fix\n---|---\nPublic S3 bucket | `aws_s3_bucket_public_access_block` + private ACL\nUnencrypted bucket | KMS key with rotation + SSE configuration\nNo versioning/logging | `aws_s3_bucket_versioning` + access logging to a second bucket\nSSH open to 0.0.0.0/0 | Ingress restricted to a trusted CIDR variable, HTTPS only\nHardcoded DB password | `variable \"db_password\" { sensitive = true }` via `TF_VAR_db_password`\nPublic, unencrypted RDS | `publicly_accessible = false`, `storage_encrypted = true`, multi-AZ\nEC2 metadata v1 | `metadata_options { http_tokens = \"required\" }` (IMDSv2)\nAdmin wildcard IAM | Least privilege: `s3:GetObject` on one bucket ARN only\n\nTwo checks didn't apply to this workload (cross-region replication, S3 event notifications), so instead of silencing the scanner I documented the decision inline — Checkov's equivalent of Bandit's `# nosec`:\n\n\n\n resource \"aws_s3_bucket\" \"data\" {\n #checkov:skip=CKV_AWS_144:Single-region deployment, replication not required\n #checkov:skip=CKV2_AWS_62:No downstream consumers need S3 event notifications\n bucket = \"company-customer-data-bucket\"\n }\n\n\nThe result:\n\n\n\n Passed checks: 79, Failed checks: 0, Skipped checks: 5\n\n\n**From 35 failed checks to 0** , with every skip justified and auditable. ✅\n\n## Automating with GitHub Actions\n\nSame principle as any SAST tool: a manual scan is a snapshot, a CI scan is a security gate. This workflow runs on every push and PR, and fails the build if the hardened configuration regresses:\n\n\n\n name: Checkov IaC Scan\n\n on:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\n jobs:\n checkov:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with:\n python-version: \"3.12\"\n - name: Install Checkov\n run: pip install checkov\n - name: Scan vulnerable config (findings expected, does not block)\n run: checkov -f main.tf --compact || true\n - name: Scan fixed config (security gate)\n run: checkov -f main_fixed.tf --compact\n - name: Generate JSON report\n if: always()\n run: checkov -f main_fixed.tf -o json > checkov-report.json || true\n - uses: actions/upload-artifact@v4\n if: always()\n with:\n name: checkov-report\n path: checkov-report.json\n\n\nNow a pull request that opens port 22 to the internet gets a red ❌ before any human even reviews it.\n\n## Strengths and limitations\n\nWhat impressed me about Checkov: enormous policy coverage out of the box, scans finish in seconds with zero cloud credentials, checks map to real breach patterns, and the graph-based engine understands _connections_ between resources (that's what the `CKV2_*` checks do — for example, it knows my bucket lacks a Public Access Block _resource_ , not just a bad attribute).\n\nLimitations, in line with what OWASP says about SAST generally: it analyzes _declared_ state, not what actually runs in your account (drift is invisible to it); it can't know your business context, so expect policies that don't apply to your workload — that's what documented skips are for; and it won't catch logic flaws in how your services use the infrastructure. Combine it with cloud posture management (CSPM) and runtime monitoring for full coverage.\n\n## Conclusion\n\nThe same lesson as application SAST, but the stakes are arguably higher: nobody exploits your off-by-one error as fast as a scanner finds your public S3 bucket. Checkov made 35 concrete problems visible in seconds, taught me the hardening pattern for each one, and now guards my infrastructure on every commit — for free.\n\n**Full demo code + workflow:** github.com/Dayan-18/checkov-demo\n\n_Do you scan your IaC in CI? Which tool? Tell me in the comments!_ 👇\n\n_References: OWASP Source Code Analysis Tools · Checkov documentation · Terraform AWS provider docs_",
"title": "Your Infrastructure Has Bugs Too: Scanning Terraform with Checkov (IaC SAST)"
}