Introduction

Infrastructure misconfigurations remain the single largest attack surface in cloud environments. In 2025, roughly 23% of all cloud security incidents originated from misconfigured resources, with 55% of cloud breaches tracing back to configuration drift or oversight. The average cost per misconfiguration incident now exceeds $4.3 million, and Gartner continues to forecast that through 2025 and beyond, 99% of cloud security failures will be the customer’s fault — primarily due to misconfigurations.

The good news: policy-as-code, automation, and continuous scanning can prevent up to 75% of these issues before deployment ever happens. That is what shift-left IaC security scanning delivers. By catching insecure defaults, overly permissive IAM policies, unencrypted storage, and exposed network configurations at the code level, you eliminate entire classes of vulnerabilities before they reach production.

This guide compares the open-source IaC security scanners — Checkov and tfsec (now part of Trivy) — with AWS-native controls including CloudFormation Guard and AWS Config Rules. The takeaway: run both OSS and AWS-native tools for defense-in-depth coverage.

IaC Security Scanning Defense in Depth IaC security scanning defense-in-depth: pre-commit, CI/CD, and post-deploy continuous compliance

The Shift-Left Security Model for Infrastructure

What Shift-Left Means for IaC

Traditional infrastructure security operates reactively: deploy resources, then scan for misconfigurations after the fact. Shift-left inverts this model by embedding security validation directly into the development workflow:

  1. Pre-commit — Scan IaC files on the developer’s machine before code is pushed
  2. CI/CD pipeline — Automated security gates that block merges or deployments on policy violations
  3. Post-deployment — Continuous compliance monitoring to catch drift and runtime changes

Each layer catches what the previous one missed. Pre-commit hooks provide fast developer feedback. CI/CD gates enforce organizational policy. Post-deployment monitoring handles manual console changes and configuration drift.

Why OSS Scanners Lead the Shift-Left Movement

Open-source IaC scanners have a structural advantage over vendor-specific tools: community-driven rule sets grow faster and cover more frameworks. Checkov ships with over 2,500 built-in policies. Trivy (the successor to tfsec) integrates IaC scanning alongside container and dependency scanning in a single binary. These tools are cloud-agnostic, free, and extensible.

AWS-native tools like CloudFormation Guard and AWS Config Rules are essential complements, but they operate within the AWS ecosystem and require you to author or adopt rules from a smaller registry. The winning strategy is to run OSS scanners for breadth and AWS-native tools for depth on AWS-specific compliance.

AWS CloudFormation Guard: Capabilities and Limitations

What Is CloudFormation Guard?

AWS CloudFormation Guard (cfn-guard) is an open-source, general-purpose policy-as-code evaluation tool. Despite the name, it validates any JSON or YAML structured data — not just CloudFormation templates. You can use it against Terraform plan JSON output, Kubernetes manifests, or any structured configuration.

Guard uses a purpose-built domain-specific language (DSL) for writing rules. Guard 3.0 introduced stateful rules through built-in functions, advanced regex support, structured JSON/YAML output, and shell auto-completions.

Guard Rule Syntax

Guard rules are declarative and readable. Here is an example that enforces S3 bucket encryption:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# s3_encryption.guard
let s3_buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]

rule s3_bucket_encryption_enabled when %s3_buckets !empty {
    %s3_buckets.Properties.BucketEncryption exists
    %s3_buckets.Properties.BucketEncryption.ServerSideEncryptionConfiguration[*] {
        ServerSideEncryptionByDefault.SSEAlgorithm in ["aws:kms", "AES256"]
    }
}

rule s3_bucket_public_access_blocked when %s3_buckets !empty {
    %s3_buckets.Properties.PublicAccessBlockConfiguration exists
    %s3_buckets.Properties.PublicAccessBlockConfiguration {
        BlockPublicAcls == true
        BlockPublicPolicy == true
        IgnorePublicAcls == true
        RestrictPublicBuckets == true
    }
}

Running Guard Locally

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Install cfn-guard
brew install cloudformation-guard

# Validate a CloudFormation template
cfn-guard validate \
  --data template.yaml \
  --rules rules/s3_encryption.guard \
  --output-format json

# Validate Terraform plan output
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json
cfn-guard validate \
  --data tfplan.json \
  --rules rules/terraform_rules.guard

Guard Rules Registry

AWS maintains the Guard Rules Registry, an open-source repository of rule sets mapped to compliance frameworks including CIS AWS Foundations Benchmark, NIST 800-53, PCI DSS, and HIPAA. This registry gives you a head start, but the total rule count is significantly smaller than what OSS scanners provide.

Limitations

  • DSL learning curve: Guard’s rule language is purpose-built and unfamiliar to most teams
  • Smaller rule ecosystem: The community-contributed rule set is a fraction of Checkov’s 2,500+ policies
  • CloudFormation-centric: While it can parse any JSON/YAML, the tooling and documentation focus on CloudFormation
  • No graph-based analysis: Guard evaluates resources individually and does not analyze cross-resource relationships

AWS Config Rules: Post-Deployment Compliance

AWS Config Rules provide continuous compliance monitoring for deployed resources. They operate as the final safety net in a defense-in-depth strategy.

How Config Rules Work

AWS Config continuously records resource configurations and evaluates them against rules. Rules can be AWS-managed (pre-built by AWS) or custom (Lambda-backed or Guard-based).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Example: Enable the s3-bucket-server-side-encryption-enabled managed rule
aws configservice put-config-rule --config-rule '{
  "ConfigRuleName": "s3-bucket-encryption",
  "Source": {
    "Owner": "AWS",
    "SourceIdentifier": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  },
  "Scope": {
    "ComplianceResourceTypes": ["AWS::S3::Bucket"]
  }
}'

# Check compliance status
aws configservice get-compliance-details-by-config-rule \
  --config-rule-name s3-bucket-encryption \
  --compliance-types NON_COMPLIANT

Config Rules with CloudFormation Guard

You can now use Guard rules directly as AWS Config custom rules, bridging pre-deployment and post-deployment scanning with the same policy language:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# CloudFormation template for a Guard-based Config rule
AWSTemplateFormatVersion: '2010-09-09'
Resources:
  S3EncryptionGuardRule:
    Type: AWS::Config::ConfigRule
    Properties:
      ConfigRuleName: s3-encryption-guard
      Source:
        Owner: CUSTOM_POLICY
        SourceDetails:
          - EventSource: aws.config
            MessageType: ConfigurationItemChangeNotification
        CustomPolicyDetails:
          PolicyRuntime: guard-2.x.x
          PolicyText: |
            rule s3_encrypted {
              resourceType == "AWS::S3::Bucket"
              configuration.ServerSideEncryptionConfiguration exists
            }
      Scope:
        ComplianceResourceTypes:
          - AWS::S3::Bucket

Limitations of Config Rules

  • Post-deployment only: Config Rules evaluate after resources exist, not before
  • Cost: Each rule evaluation costs $0.001 per evaluation ($0.10/month per active rule with daily recording)
  • Latency: Evaluation can take minutes after a configuration change
  • Remediation complexity: Auto-remediation requires additional Lambda functions or SSM Automation

Config Rules are essential for detecting drift and unauthorized manual changes, but they should not be your first line of defense.

Checkov Deep Dive

Overview

Checkov is the most comprehensive open-source IaC scanner available. Maintained by Palo Alto Networks (Bridgecrew), it ships with over 2,500 built-in policies covering AWS, Azure, GCP, Kubernetes, and general security best practices.

Key Differentiators

  • Graph-based scanning: Checkov parses IaC files into a graph representation and evaluates cross-resource relationships. For example, it checks that an S3 bucket referenced by a CloudFront distribution has the correct origin access policy.
  • Framework breadth: Supports Terraform, CloudFormation, Kubernetes, Helm, Dockerfiles, ARM templates, Serverless Framework, Bicep, and more.
  • Compliance mapping: Built-in mapping to CIS, SOC2, HIPAA, PCI DSS, NIST 800-53, and ISO 27001.
  • Custom check authoring: Write custom checks in Python or YAML.
  • SCA integration: Scans open-source dependencies for vulnerabilities alongside IaC checks.

Running Checkov

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Install
pip install checkov

# Scan a Terraform directory
checkov -d ./terraform/ --framework terraform

# Scan with specific compliance framework
checkov -d ./terraform/ --check CIS_AWS

# Scan CloudFormation templates
checkov -f cloudformation-template.yaml --framework cloudformation

# Output in JUnit XML for CI/CD integration
checkov -d ./terraform/ -o junitxml > checkov-results.xml

# Skip specific checks
checkov -d ./terraform/ --skip-check CKV_AWS_18,CKV_AWS_19

# Scan with custom external checks
checkov -d ./terraform/ --external-checks-dir ./custom-checks/

Example: Scanning Terraform with Intentional Misconfigurations

Here is a Terraform file with several common security issues:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# main.tf - Intentionally insecure for demonstration
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket"
  # Missing: encryption, versioning, public access block, logging
}

resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]  # Open to the world
  }

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

resource "aws_db_instance" "main" {
  engine               = "mysql"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  publicly_accessible  = true     # Database exposed to internet
  storage_encrypted    = false    # Unencrypted storage
  skip_final_snapshot  = true
}

Running Checkov against this file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ checkov -f main.tf

       _               _
   ___| |__   ___  ___| | _______   __
  / __| '_ \ / _ \/ __| |/ / _ \ \ / /
 | (__| | | |  __/ (__|   < (_) \ V /
  \___|_| |_|\___|\___|_|\_\___/ \_/

By Prisma Cloud | version: 3.2.x

terraform scan results:

Passed checks: 0, Failed checks: 12, Skipped checks: 0

Check: CKV_AWS_145: "Ensure S3 bucket is encrypted with KMS"
  FAILED for resource: aws_s3_bucket.data
  File: /main.tf:2-5

Check: CKV_AWS_18: "Ensure S3 bucket has access logging enabled"
  FAILED for resource: aws_s3_bucket.data
  File: /main.tf:2-5

Check: CKV_AWS_21: "Ensure S3 bucket has versioning enabled"
  FAILED for resource: aws_s3_bucket.data
  File: /main.tf:2-5

Check: CKV_AWS_260: "Ensure no security group allows ingress from 0.0.0.0/0 to all ports"
  FAILED for resource: aws_security_group.web
  File: /main.tf:7-21

Check: CKV_AWS_16: "Ensure RDS instance is encrypted at rest"
  FAILED for resource: aws_db_instance.main
  File: /main.tf:23-31

Check: CKV_AWS_17: "Ensure RDS instance is not publicly accessible"
  FAILED for resource: aws_db_instance.main
  File: /main.tf:23-31
...

Writing Custom Checkov Checks in Python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# custom_checks/s3_naming_convention.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckCategories, CheckResult


class S3NamingConvention(BaseResourceCheck):
    """Ensure S3 buckets follow organizational naming convention."""

    def __init__(self):
        name = "Ensure S3 bucket follows naming convention: {env}-{app}-{purpose}"
        id = "CKV_CUSTOM_1"
        supported_resources = ["aws_s3_bucket"]
        categories = [CheckCategories.CONVENTION]
        super().__init__(name=name, id=id,
                         categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        bucket_name = conf.get("bucket", [""])[0]
        # Enforce pattern: env-app-purpose
        import re
        pattern = r"^(dev|staging|prod)-[a-z]+-[a-z]+$"
        if re.match(pattern, bucket_name):
            return CheckResult.PASSED
        return CheckResult.FAILED


check = S3NamingConvention()

Writing Custom Checkov Checks in YAML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# custom_checks/s3_tags_required.yaml
metadata:
  id: "CKV_CUSTOM_2"
  name: "Ensure S3 buckets have required tags"
  category: "GENERAL_SECURITY"
definition:
  and:
    - cond_type: "attribute"
      resource_types:
        - "aws_s3_bucket"
      attribute: "tags.Environment"
      operator: "exists"
    - cond_type: "attribute"
      resource_types:
        - "aws_s3_bucket"
      attribute: "tags.Owner"
      operator: "exists"
    - cond_type: "attribute"
      resource_types:
        - "aws_s3_bucket"
      attribute: "tags.Application"
      operator: "exists"

tfsec and Trivy IaC Scanning

The tfsec to Trivy Transition

tfsec was one of the first purpose-built Terraform security scanners, acquired by Aqua Security in 2021. In 2023, Aqua merged tfsec’s scanning engine into Trivy, their unified security scanner. While tfsec remains available as a standalone CLI, all new development happens in Trivy. The community is encouraged to migrate.

The migration path is straightforward: Trivy includes every security check available in tfsec, plus additional capabilities for scanning containers, dependencies, SBOM generation, and Kubernetes cluster scanning — all in a single binary.

Running Trivy for IaC Scanning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Install Trivy
brew install trivy

# Scan a Terraform directory
trivy config ./terraform/

# Scan with specific severity threshold
trivy config --severity HIGH,CRITICAL ./terraform/

# Scan CloudFormation templates
trivy config ./cloudformation/

# Scan Kubernetes manifests
trivy config ./k8s-manifests/

# Output in SARIF format for GitHub Security tab
trivy config --format sarif --output trivy-results.sarif ./terraform/

# Scan with custom Rego policies
trivy config --policy ./custom-policies/ ./terraform/

Example Trivy Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
$ trivy config ./terraform/

2026-04-01T10:00:00.000Z  INFO  Misconfiguration scanning is enabled

main.tf (terraform)
===================
Tests: 24 (SUCCESSES: 12, FAILURES: 12, EXCEPTIONS: 0)
Failures: 12 (HIGH: 6, CRITICAL: 3, MEDIUM: 2, LOW: 1)

CRITICAL: S3 bucket does not have encryption enabled
════════════════════════════════════════════════════
S3 Buckets should be encrypted to protect data at rest.

See https://avd.aquasec.com/misconfig/avd-aws-0088

 ──────────────────────────────────────────────
  main.tf:2-5
 ──────────────────────────────────────────────
   2 │ resource "aws_s3_bucket" "data" {
   3 │   bucket = "my-data-bucket"
   4 │ }
 ──────────────────────────────────────────────

HIGH: Security group rule allows ingress from 0.0.0.0/0
════════════════════════════════════════════════════════
Opening to the world is rarely a good idea.

See https://avd.aquasec.com/misconfig/avd-aws-0107
...

Custom Rego Policies for Trivy

Trivy uses Open Policy Agent (OPA) Rego for custom policies, which is more widely adopted than Checkov’s Python checks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# custom-policies/s3_lifecycle.rego
package user.terraform.s3

import rego.v1

__rego_metadata__ := {
    "id": "CUSTOM-001",
    "title": "S3 buckets must have lifecycle policies",
    "severity": "MEDIUM",
    "type": "Terraform Security Check",
}

__rego_input__ := {
    "combine": false,
    "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}],
}

deny contains res if {
    bucket := input.aws.s3.buckets[_]
    count(bucket.lifecycleconfig.rules) == 0
    res := result.new(
        "S3 bucket does not have a lifecycle policy configured",
        bucket,
    )
}

Comparison Table

Feature Checkov Trivy (tfsec) CloudFormation Guard AWS Config Rules
Built-in Rules 2,500+ 1,500+ ~350 (registry) ~300 managed
Terraform Yes Yes Via JSON plan No
CloudFormation Yes Yes Yes (native) N/A (post-deploy)
Kubernetes Yes Yes Yes (JSON/YAML) No
Helm Charts Yes Yes No No
Dockerfiles Yes Yes No No
ARM Templates Yes Yes No No
Graph Analysis Yes No No No
Custom Rules Python + YAML Rego (OPA) Guard DSL Lambda + Guard
SCA/Dependencies Yes Yes No No
Compliance Frameworks CIS, SOC2, HIPAA, PCI, NIST, ISO 27001 CIS, PCI, HIPAA CIS, NIST, PCI, HIPAA CIS, PCI (limited)
CI/CD Integration Native Native CLI-based Post-deploy only
Cost Free (OSS) Free (OSS) Free (OSS) $0.001/evaluation
Multi-Cloud AWS, Azure, GCP AWS, Azure, GCP AWS-focused AWS only
SARIF Output Yes Yes No (JSON/YAML) No
IDE Plugins VS Code, JetBrains VS Code No No

Pre-Commit Hook Integration

Pre-commit hooks provide the fastest feedback loop. Developers catch issues before code leaves their machine.

Pre-Commit Configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# .pre-commit-config.yaml
repos:
  # Checkov
  - repo: https://github.com/bridgecrewio/checkov
    rev: '3.2.0'
    hooks:
      - id: checkov
        args: [
          '--framework', 'terraform',
          '--skip-check', 'CKV_AWS_18',  # Skip if logging is handled elsewhere
          '--quiet',
          '--compact'
        ]

  # Trivy IaC scanning
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.96.0
    hooks:
      - id: terraform_trivy
        args:
          - --args=--severity=HIGH,CRITICAL
          - --args=--skip-dirs=modules/deprecated

  # CloudFormation Guard
  - repo: local
    hooks:
      - id: cfn-guard
        name: CloudFormation Guard
        entry: cfn-guard validate --data
        language: system
        files: '\.ya?ml$'
        args: ['--rules', 'guard-rules/']
        types: [file]

  # Terraform formatting and validation
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.96.0
    hooks:
      - id: terraform_fmt
      - id: terraform_validate
      - id: terraform_tflint

Installing and Running Pre-Commit

1
2
3
4
5
6
7
8
9
10
11
# Install pre-commit
pip install pre-commit

# Install hooks in your repository
pre-commit install

# Run against all files (initial scan)
pre-commit run --all-files

# Run specific hook
pre-commit run checkov --all-files

CI/CD Pipeline Integration

GitHub Actions Workflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# .github/workflows/iac-security.yml
name: IaC Security Scanning

on:
  pull_request:
    paths:
      - 'terraform/**'
      - 'cloudformation/**'

jobs:
  checkov:
    name: Checkov IaC Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform
          output_format: cli,sarif
          output_file_path: console,checkov-results.sarif
          soft_fail: false
          skip_check: CKV_AWS_18

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: checkov-results.sarif

  trivy:
    name: Trivy IaC Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy IaC scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: 'terraform/'
          severity: 'HIGH,CRITICAL'
          format: 'sarif'
          output: 'trivy-results.sarif'
          exit-code: '1'

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif

  cfn-guard:
    name: CloudFormation Guard
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install cfn-guard
        run: |
          curl --proto '=https' --tlsv1.2 -sSf \
            https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh \
            | sh
          export PATH=~/.guard/bin:$PATH

      - name: Validate CloudFormation templates
        run: |
          cfn-guard validate \
            --data cloudformation/ \
            --rules guard-rules/ \
            --output-format json \
            --structured

  security-gate:
    name: Security Gate
    needs: [checkov, trivy, cfn-guard]
    runs-on: ubuntu-latest
    steps:
      - name: All security checks passed
        run: echo "All IaC security scans passed. Safe to deploy."

AWS CodeBuild Buildspec

For teams using AWS-native CI/CD, here is a CodeBuild buildspec that integrates Checkov and cfn-guard:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# buildspec-security.yml
version: 0.2

env:
  variables:
    CHECKOV_SKIP: "CKV_AWS_18"

phases:
  install:
    runtime-versions:
      python: 3.12
    commands:
      - pip install checkov
      - curl --proto '=https' --tlsv1.2 -sSf
          https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install-guard.sh
          | sh
      - export PATH=~/.guard/bin:$PATH
      - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh
          | sh -s -- -b /usr/local/bin

  pre_build:
    commands:
      - echo "Running IaC security scans..."

      # Checkov scan
      - |
        checkov -d ./terraform/ \
          --framework terraform \
          --skip-check $CHECKOV_SKIP \
          --output cli \
          --output junitxml \
          --output-file-path console,checkov-results.xml \
          --compact

      # Trivy IaC scan
      - |
        trivy config ./terraform/ \
          --severity HIGH,CRITICAL \
          --exit-code 1 \
          --format json \
          --output trivy-results.json

      # CloudFormation Guard validation
      - |
        cfn-guard validate \
          --data ./cloudformation/ \
          --rules ./guard-rules/ \
          --output-format json > guard-results.json

  build:
    commands:
      - echo "Security scans passed. Proceeding with deployment..."
      - terraform init
      - terraform plan -out=tfplan

reports:
  checkov-report:
    files:
      - checkov-results.xml
    file-format: JUNITXML

artifacts:
  files:
    - checkov-results.xml
    - trivy-results.json
    - guard-results.json
  name: security-scan-results

Custom Policy Authoring

Each tool takes a different approach to custom policies. Choose based on your team’s existing skills.

Checkov: Python or YAML

Python checks give you full programmatic control:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# checks/rds_deletion_protection.py
from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
from checkov.common.models.enums import CheckCategories, CheckResult


class RDSDeletionProtection(BaseResourceCheck):
    """Ensure RDS instances have deletion protection enabled in production."""

    def __init__(self):
        name = "Ensure RDS has deletion protection enabled"
        id = "CKV_CUSTOM_10"
        supported_resources = ["aws_db_instance"]
        categories = [CheckCategories.BACKUP_AND_RECOVERY]
        super().__init__(name=name, id=id,
                         categories=categories,
                         supported_resources=supported_resources)

    def scan_resource_conf(self, conf):
        deletion_protection = conf.get("deletion_protection", [False])
        if isinstance(deletion_protection, list):
            deletion_protection = deletion_protection[0]
        if deletion_protection is True:
            return CheckResult.PASSED
        return CheckResult.FAILED


check = RDSDeletionProtection()

Trivy: Rego (OPA)

Rego is the standard policy language across the cloud-native ecosystem:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# policies/rds_multi_az.rego
package user.terraform.rds

import rego.v1

__rego_metadata__ := {
    "id": "CUSTOM-010",
    "title": "RDS instances should use Multi-AZ for production",
    "severity": "HIGH",
    "type": "Terraform Security Check",
}

__rego_input__ := {
    "combine": false,
    "selector": [{"type": "defsec", "subtypes": [{"service": "rds", "provider": "aws"}]}],
}

deny contains res if {
    instance := input.aws.rds.instances[_]
    not instance.multiaz.value
    res := result.new(
        "RDS instance does not have Multi-AZ enabled",
        instance,
    )
}

CloudFormation Guard: Guard DSL

Guard DSL is declarative and purpose-built for infrastructure validation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# rules/rds_security.guard
let rds_instances = Resources.*[ Type == 'AWS::RDS::DBInstance' ]

rule rds_encryption_required when %rds_instances !empty {
    %rds_instances.Properties.StorageEncrypted == true
        << RDS instances must have storage encryption enabled >>
}

rule rds_no_public_access when %rds_instances !empty {
    %rds_instances.Properties.PubliclyAccessible == false
        << RDS instances must not be publicly accessible >>
}

rule rds_backup_retention when %rds_instances !empty {
    %rds_instances.Properties.BackupRetentionPeriod >= 7
        << RDS backup retention must be at least 7 days >>
}

rule rds_multi_az_production when %rds_instances !empty {
    %rds_instances.Properties {
        when Tags[*].Key == "Environment" {
            when Tags[*].Value == "production" {
                MultiAZ == true
                    << Production RDS instances must use Multi-AZ >>
            }
        }
    }
}

Defense-in-Depth: The Complete Scanning Architecture

The strongest IaC security posture layers all three scanning stages. Here is how to architect the full pipeline:

Layer 1: Pre-Commit (Developer Machine)

  • Tools: Checkov, Trivy, cfn-guard
  • Purpose: Immediate feedback before code is committed
  • Behavior: Block commit on CRITICAL/HIGH findings
  • Speed: Must complete in under 30 seconds for developer adoption

Layer 2: CI/CD Pipeline (Automated Gate)

  • Tools: Checkov (full scan), Trivy (full scan), cfn-guard (CloudFormation validation)
  • Purpose: Enforce organizational policy on every pull request
  • Behavior: Block merge/deploy on any policy violation
  • Integration: SARIF output to GitHub Security tab, JUnit XML to build reports
  • Speed: Full scan in 2-5 minutes is acceptable

Layer 3: Post-Deploy (Continuous Compliance)

  • Tools: AWS Config Rules (managed + custom Guard rules), Security Hub
  • Purpose: Detect configuration drift and manual console changes
  • Behavior: Alert or auto-remediate non-compliant resources
  • Integration: Security Hub aggregates findings from Config, GuardDuty, Inspector
  • Frequency: Daily recording for cost optimization, event-based for critical resources

Putting It Together

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Developer workflow:
# 1. Write Terraform code
# 2. Pre-commit hooks catch issues immediately
git commit -m "Add S3 bucket for data pipeline"
# checkov runs -> finds missing encryption -> blocks commit

# 3. Fix and recommit
git commit -m "Add encrypted S3 bucket for data pipeline"
# checkov passes -> trivy passes -> commit succeeds

# 4. Push and open PR
git push origin feature/data-pipeline
# GitHub Actions runs full Checkov + Trivy + cfn-guard scans
# Security gate must pass before merge is allowed

# 5. Merge and deploy
# Terraform applies through CodePipeline/GitHub Actions
# AWS Config Rules begin evaluating the new resources
# Security Hub aggregates compliance status

# 6. Ongoing monitoring
# Someone modifies the bucket via console -> Config detects drift
# Config Rule marks resource NON_COMPLIANT
# Security Hub alert fires -> SNS notification -> team investigates

Coverage Matrix

Misconfiguration Type Pre-Commit CI/CD Post-Deploy
Unencrypted storage Checkov, Trivy Checkov, Trivy Config Rules
Open security groups Checkov, Trivy Checkov, Trivy Config Rules
Public RDS instances Checkov, Trivy Checkov, Trivy Config Rules
Missing tags Checkov, Trivy Checkov, Trivy Config Rules
IAM over-permissions Checkov (graph) Checkov (graph) IAM Access Analyzer
Cross-resource issues Checkov (graph) Checkov (graph) Security Hub
Configuration drift N/A N/A Config Rules
Manual console changes N/A N/A Config Rules
Compliance violations Checkov, cfn-guard Checkov, cfn-guard Config Rules
  1. Week 1: Install Checkov and Trivy locally. Run against your existing IaC to baseline findings.
  2. Week 2: Add pre-commit hooks with Checkov. Start with --soft-fail to avoid blocking developers immediately.
  3. Week 3: Add Checkov and Trivy to your CI/CD pipeline as non-blocking checks. Review findings and suppress false positives.
  4. Week 4: Switch CI/CD checks to blocking (fail the build on HIGH/CRITICAL). Add cfn-guard for CloudFormation validation.
  5. Month 2: Enable AWS Config Rules for your top 10 compliance requirements. Integrate with Security Hub.
  6. Month 3: Write custom checks for organizational policies (naming conventions, tagging, required configurations). Remove --soft-fail from pre-commit.

Conclusion

IaC security scanning is not a single-tool problem. AWS CloudFormation Guard provides native policy-as-code validation with a growing rules registry. AWS Config Rules deliver continuous post-deployment compliance monitoring. But open-source scanners like Checkov (2,500+ policies) and Trivy (the successor to tfsec with 1,500+ checks) offer unmatched breadth, multi-cloud portability, and community-driven rule velocity.

The defense-in-depth approach — pre-commit hooks, CI/CD security gates, and post-deployment Config Rules — catches misconfigurations at every stage. Start with OSS scanners for breadth, layer in AWS-native controls for depth, and write custom policies for your organization’s specific requirements.

The IaC security market is projected to reach $8.76 billion by 2033 because organizations are recognizing that catching a misconfiguration in code costs orders of magnitude less than remediating a breach in production. Shift left, scan early, scan often, and automate everything.


For more on building secure DevSecOps pipelines and implementing cloud security automation, connect with me on LinkedIn.

Updated: