{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreihcfn6yjuoh5u7oohseov7q5ehg27k5nirsqzd4sdpaotc37okipy",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mpjrzrpdff32"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreihfkgj2iivobgy7xhwvm5pamdyvoylxbtxaly7dyxsmjz5ksilheu"
},
"mimeType": "image/webp",
"size": 89092
},
"path": "/diego_fabrizioandianava/applying-a-sast-tool-to-infrastructure-as-code-scanning-a-terraform-stack-with-checkov-5402",
"publishedAt": "2026-06-30T19:10:58.000Z",
"site": "https://dev.to",
"tags": [
"terraform"
],
"textContent": "## Abstract\n\nApplication code isn't the only place a SAST tool belongs — infrastructure definitions are source code too, and misconfigurations written into a Terraform file ship to production exactly as reliably as a bug in application logic does. This article applies **Checkov** , an open-source static analysis tool for Infrastructure as Code, to a Terraform stack that provisions storage, networking, a database, and IAM permissions for an order service. The unmodified stack produced 36 failed checks against 14 passed. After remediating every finding that represented an actual exploitable risk, the same stack passed 57 checks with 6 remaining — and those 6 turned out to be business decisions (replication, lifecycle policies) rather than vulnerabilities, which is itself the more useful finding: not every flagged item deserves the same response.\n\n## Why IaC needs its own static analysis, separate from application SAST\n\nA SAST tool for application code looks for dangerous function calls and data flows. A SAST tool for IaC looks for a different category entirely: resource configurations that violate a known-safe default. An `aws_s3_bucket` with no `aws_s3_bucket_public_access_block` isn't a code bug in the traditional sense — the Terraform is syntactically perfect and will apply cleanly. The risk is purely in what gets provisioned. That's precisely the gap Checkov, Terrascan, and similar tools are built to close: they evaluate the _intended infrastructure state_ , not the code that produces it.\n\n## The real-world example: a Terraform stack for an order service\n\nA small but realistic stack: an S3 bucket for order exports, a security group, an RDS database, an IAM policy, an EBS volume, and a CloudWatch log group.\n\n\n\n # terraform-vulnerable/main.tf\n resource \"aws_s3_bucket\" \"order_exports\" {\n bucket = \"store-order-exports\"\n acl = \"public-read\"\n }\n\n resource \"aws_security_group\" \"app_sg\" {\n ingress {\n from_port = 22\n to_port = 22\n protocol = \"tcp\"\n cidr_blocks = [\"0.0.0.0/0\"]\n }\n }\n\n resource \"aws_db_instance\" \"orders_db\" {\n password = \"Sup3rSecret!2024\"\n publicly_accessible = true\n storage_encrypted = false\n }\n\n resource \"aws_iam_policy\" \"app_policy\" {\n policy = jsonencode({\n Statement = [{ Effect = \"Allow\", Action = \"*\", Resource = \"*\" }]\n })\n }\n\n\nNothing here fails `terraform validate` — it's all valid HCL that will provision successfully. That's exactly why this category of risk needs a dedicated scanner instead of relying on the provider to reject something dangerous.\n\n## Running Checkov against it\n\n\n pip install checkov\n checkov -d terraform-vulnerable --compact\n\n\nReal, unedited output (excerpt):\n\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.order_exports\n\n Check: CKV_AWS_24: \"Ensure no security groups allow ingress from 0.0.0.0:0 to port 22\"\n FAILED for resource: aws_security_group.app_sg\n\n Check: CKV_AWS_16: \"Ensure all data stored in the RDS is securely encrypted at rest\"\n FAILED for resource: aws_db_instance.orders_db\n\n Check: CKV_AWS_17: \"Ensure all data stored in RDS is not publicly accessible\"\n FAILED for resource: aws_db_instance.orders_db\n\n Check: CKV_AWS_62: \"Ensure IAM policies that allow full \"*-*\" administrative privileges are not created\"\n FAILED for resource: aws_iam_policy.app_policy\n\n Check: CKV_AWS_3: \"Ensure all data stored in the EBS is securely encrypted\"\n FAILED for resource: aws_ebs_volume.app_data\n\n Passed checks: 14, Failed checks: 36, Skipped checks: 0\n\n\nEvery failed check carries a stable ID (`CKV_AWS_*`), which is what makes a finding something a team can track, suppress with a documented reason, or treat as a release blocker — instead of a one-off comment in a pull request.\n\n## Fixing what's actually a risk\n\nResource | Finding | Fix\n---|---|---\nS3 bucket | Public-read ACL, no versioning, no encryption | Remove the ACL, add `aws_s3_bucket_public_access_block`, `aws_s3_bucket_versioning`, `aws_s3_bucket_server_side_encryption_configuration`\nSecurity group | SSH and Postgres open to `0.0.0.0/0` | Scope `cidr_blocks` to the office VPN and the app subnet specifically\nRDS instance | Public access, unencrypted storage, hardcoded password | `publicly_accessible = false`, `storage_encrypted = true`, `manage_master_user_password = true` (AWS-managed via Secrets Manager)\nIAM policy | `Action: \"*\"` on `Resource: \"*\"` | Scope to the two S3 actions the service actually needs, on the specific bucket ARN\nEBS volume | Unencrypted | `encrypted = true` with a customer-managed KMS key\nCloudWatch log group | No retention period | `retention_in_days = 365`\n\n\n # terraform-fixed/main.tf\n resource \"aws_security_group\" \"app_sg\" {\n ingress {\n description = \"SSH from the office VPN only\"\n from_port = 22\n to_port = 22\n protocol = \"tcp\"\n cidr_blocks = [\"203.0.113.0/24\"]\n }\n }\n\n resource \"aws_db_instance\" \"orders_db\" {\n manage_master_user_password = true\n publicly_accessible = false\n storage_encrypted = true\n deletion_protection = true\n }\n\n resource \"aws_iam_policy\" \"app_policy\" {\n policy = jsonencode({\n Statement = [{\n Effect = \"Allow\"\n Action = [\"s3:GetObject\", \"s3:PutObject\"]\n Resource = \"${aws_s3_bucket.order_exports.arn}/*\"\n }]\n })\n }\n\n\n## Re-scanning: from 36 failures to 6\n\n\n $ checkov -d terraform-fixed --compact\n Passed checks: 57, Failed checks: 6, Skipped checks: 0\n\n\nThe remaining 6 are worth looking at individually, because none of them are the same category of risk as the original 36:\n\n\n\n CKV2_AWS_62 Ensure S3 buckets should have event notifications enabled\n CKV_AWS_144 Ensure that S3 bucket has cross-region replication enabled\n CKV2_AWS_61 Ensure that an S3 bucket has a lifecycle configuration\n CKV2_AWS_64 Ensure KMS key Policy is defined\n CKV2_AWS_30 Ensure Postgres RDS has Query Logging enabled\n CKV_AWS_354 Ensure RDS Performance Insights are encrypted using KMS CMKs\n\n\nCross-region replication and lifecycle rules are cost and disaster-recovery decisions, not vulnerabilities — applying them blindly to satisfy a scanner would add infrastructure cost without addressing any actual exposure. Query logging and Performance Insights encryption are reasonable hardening that a team might genuinely choose to defer. This is the point a checklist mentality misses: the job of a SAST tool is to surface every deviation from a safe default, and the job of the engineer is still to decide which deviations matter for this specific system.\n\n## Wiring it into CI\n\n\n # .github/workflows/checkov.yml\n name: IaC SAST scan (Checkov)\n\n on:\n push:\n branches: [\"main\"]\n pull_request:\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 - run: pip install checkov\n - run: checkov -d terraform-fixed --compact --soft-fail-on LOW\n\n\nRunning this on every `terraform plan` means a public S3 bucket or an open security group never reaches `apply` without someone explicitly seeing — and accepting — the finding first.\n\n## Conclusion\n\nThe first scan caught real, exploitable misconfiguration: a publicly readable bucket, an internet-facing database with a hardcoded password, an IAM policy with unrestricted `*` access. Those aren't edge cases — they're exactly the kind of thing that ends up in a breach disclosure. The second scan is the more instructive result: not every one of the remaining 6 findings deserved a fix, and treating a SAST tool's output as a literal to-do list rather than a prioritized list of _deviations to evaluate_ is how teams either burn time hardening low-risk defaults or, worse, learn to ignore the tool altogether. Static analysis for infrastructure is most useful exactly where this example landed — as the thing that catches the dangerous default, while leaving the judgment calls to the team that owns the system.",
"title": "Applying a SAST Tool to Infrastructure as Code: Scanning a Terraform Stack with Checkov"
}