← All posts
· 4 min read ·
SecurityTerraformCI/CDIaCDevSecOpsAWS

IaC Security Scanning with Trivy: Catching Misconfigurations Before They Reach AWS

Trivy scans Terraform, CloudFormation, Kubernetes manifests, and Dockerfiles for security misconfigurations in CI. Here's how to integrate it, tune the findings, and build a .trivyignore policy that doesn't become noise.

Terminal output showing Trivy scan results for Terraform files

Infrastructure-as-Code misconfigurations are responsible for a significant share of cloud security incidents - public S3 buckets, overly permissive security groups, unencrypted databases, IAM roles with wildcard actions. The code that creates these resources is reviewable, testable, and scannable before it ever touches your AWS account. Trivy makes this easy to integrate into any CI pipeline.

Why Trivy Over Checkov or tfsec

Trivy, Checkov, and tfsec all scan IaC for misconfigurations. Trivy wins on breadth: it scans container images, filesystems, git repositories, Terraform, CloudFormation, Kubernetes, Helm, Dockerfiles, and SBOM - from a single binary with a single CI integration. If you’re already using Trivy for image scanning (you should be), adding IaC scanning is zero extra tooling.

Running Trivy Against Terraform Locally

# Install
brew install trivy  # macOS
# or
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh

# Scan a Terraform directory
trivy config ./infra/

# Output as table (default) or JSON for CI
trivy config --format json --output trivy-results.json ./infra/

# Only fail on HIGH and CRITICAL
trivy config --severity HIGH,CRITICAL ./infra/

The output groups findings by severity (CRITICAL, HIGH, MEDIUM, LOW) with the AVD ID, resource, description, and a link to the full advisory.

GitHub Actions Integration

name: Security Scan

on:
    push:
        branches: [main, develop]
    pull_request:
        branches: [main, develop]

permissions: {}

jobs:
    trivy-iac:
        name: Trivy IaC Scan
        runs-on: ubuntu-latest
        permissions:
            contents: read
            security-events: write  # For SARIF upload to GitHub Security tab

        steps:
            - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

            - name: Run Trivy IaC scan
              uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe8 # v0.29.0
              with:
                  scan-type: config
                  scan-ref: .
                  format: sarif
                  output: trivy-iac.sarif
                  severity: HIGH,CRITICAL
                  exit-code: 1
                  trivyignores: .trivyignore

            - name: Upload SARIF to GitHub Security
              uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3
              if: always()
              with:
                  sarif_file: trivy-iac.sarif
                  category: trivy-iac

exit-code: 1 makes the workflow fail on findings. if: always() uploads results even when the scan fails - so you can see findings in the Security tab regardless of pipeline status.

Building a .trivyignore Policy

A raw Trivy scan on a real Terraform codebase will surface findings that are intentional design decisions - encrypting with AWS-managed keys instead of CMKs, skipping WAF on a personal project, no versioning on a log bucket. Ignoring these with a .trivyignore file is correct, but the file needs to carry context so future maintainers understand why each suppression exists.

Good .trivyignore format:

# AVD-AWS-0132: S3 not encrypted with CMK
# AES256 (SSE-S3) is intentional  -  CloudFront OAC requires kms:Decrypt on CMK-encrypted
# objects, but the AWS-managed key policy cannot grant this. A CMK costs $1/month.
AVD-AWS-0132

# AVD-AWS-0010 / AVD-AWS-0011: WAF not enabled on CloudFront
# WAF costs ~$5/month minimum. Compensating controls: Lambda authorizer + APIGW throttling.
AVD-AWS-0010
AVD-AWS-0011

# AVD-AWS-0090: S3 log bucket versioning
# Versioning on a delivery-only log bucket adds storage cost with no recovery benefit.
AVD-AWS-0090

The rule: every suppression needs a one-line justification and, if relevant, what compensating control exists. Suppressions without justification are a finding in their own right.

Common Findings and How to Evaluate Them

AVD-AWS-0057 / AVD-AWS-0058: No MFA delete on S3 Usually acceptable for application buckets. Required for buckets containing compliance artifacts.

AVD-AWS-0006: Security group allows ingress from 0.0.0.0/0 Legitimate for port 443 on a public ALB. Not legitimate for port 22 or port 3306.

AVD-AWS-0028 / AVD-AWS-0029: CloudTrail not enabled Should almost never be suppressed. CloudTrail is the foundation of incident response.

AVD-AWS-0086: IAM policy uses wildcard resource Evaluate each one. s3:GetObject on * is very different from ec2:* on *.

AVD-AWS-0176: Lambda not configured for active tracing X-Ray Active mode is in the free tier (100k traces/month). Enable it.

Scanning Container Images Alongside IaC

trivy-image:
    name: Trivy Image Scan
    runs-on: ubuntu-latest
    permissions:
        contents: read
        security-events: write

    steps:
        - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

        - name: Build image
          run: docker build -t app:${{ github.sha }} .

        - name: Scan image
          uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe8 # v0.29.0
          with:
              image-ref: app:${{ github.sha }}
              format: sarif
              output: trivy-image.sarif
              severity: HIGH,CRITICAL
              exit-code: 1
              ignore-unfixed: true  # Don't fail on CVEs with no fix available

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

ignore-unfixed: true is sensible for image scanning - failing the build on a CVE with no available fix creates noise with no actionable resolution path.

Integrating Findings Into Your PR Review Process

With SARIF upload, Trivy findings appear directly in the GitHub Security tab and can be configured to appear as PR annotations. This puts the finding in front of the reviewer at the point of decision - before the code merges.

For teams, configure branch protection to require the security scan job to pass. Combined with Dependabot for dependency updates and CodeQL for application code, Trivy for IaC closes the loop on automated security signal across your entire stack.

The goal isn’t zero findings. It’s a documented, reviewed set of accepted risks - everything else is blocked before it reaches production.

← All posts