DEV Community

Cover image for Integrating Checkov and tfsec into CI for Automated IaC Security
beefed.ai
beefed.ai

Posted on • Originally published at beefed.ai

Integrating Checkov and tfsec into CI for Automated IaC Security

  • Picking the right scanner for your stack
  • Tame the noise: tuning policies and managing false positives
  • Pipeline patterns that provide fast feedback and enforce security gates
  • Reporting, triage, and remediation workflows that scale
  • Operational checklist: integrating Checkov and tfsec into CI

Start here: static IaC scanning only protects you when the scanners are embedded into CI with tuned rules, predictable exit behavior, and a triage loop that treats exceptions as tracked, time-boxed decisions rather than permanent silences. Raw scans that flood PRs create resentment; properly integrated scanners create safety gates that developers respect.

The problem
Scans run on every push but produce a large number of noisy findings, PRs stall or get bypassed, and security teams drown in context-less output. You see symptoms like PR checks that fail on low-severity policy violations, long lists of legacy findings nobody owns, and engineers who add ad-hoc inline ignores just to merge. Those consequences create technical debt, blind spots, and governance gaps that compound over time.

Picking the right scanner for your stack

Make the tool-choice decision based on scope and workflow, not on popularity.

  • What each tool is best at

    • Checkov is broad: it scans many IaC frameworks (Terraform, CloudFormation, Kubernetes manifests, ARM/Bicep, Helm charts, Dockerfiles, GitHub/GitLab configuration and more) and supports powerful CLI flags for fail logic, external checks, baseline creation, and plan enrichment. This breadth makes it the natural fit when you maintain heterogeneous IaC or need a single tool to cover multiple templates and artifact types.
    • tfsec focuses on Terraform/OpenTofu. It runs very fast, is developer-friendly for Terraform-first teams, and supports JSON/YAML custom checks plus Rego where needed; it also has a PR commenter action that makes feedback inline and visible in GitHub PRs. Use tfsec when you need lightweight, fast Terraform scanning that will run in every PR.
  • A practical, contrarian rule

    • Running both tools in the exact same stage on every PR often doubles noise without commensurate benefit. A more surgical approach is to use a fast Terraform-first scanner in the PR loop and a broader scanner (or the other scanner) on nightly/full runs or enforcement gates. This reduces friction while preserving broad coverage.
  • Quick comparison (at-a-glance)

Characteristic Checkov tfsec
Primary scope Multi-IaC (Terraform, CloudFormation, K8s, etc.). Terraform/OpenTofu-focused, very fast.
Custom rules Python & YAML custom checks; external checks via Git. JSON/YAML custom checks; Rego support; .tfsec/*_tfchecks.json or .yaml.
Inline suppression #checkov:skip=<ID>:<reason> comment support. #tfsec:ignore:<rule> with optional expiry.
CI integrations Official GitHub Action and CLI flags for soft/hard fail. PR commenter action, SARIF-friendly integrations.
Best use Cross-framework policy enforcement, plan enrichment, custom logic. Fast Terraform-only checks, immediate PR feedback.

Use these strengths to design a pipeline where the right tool runs in the right phase.

Tame the noise: tuning policies and managing false positives

Noise kills adoption. Policy tuning, disciplined exceptions, and testable custom rules are how you get to high signal.

  • Inline suppression vs. centralized exceptions

    • For per-resource, code-annotated suppressions use Checkov’s inline comment form: checkov:skip=<check_id>:<reason>. That skip lives next to the code and appears in Checkov output as SKIPPED. Treat inline skips as time-boxed exceptions with documented justification.
    • For tfsec, add inline ignores like #tfsec:ignore:aws-vpc-no-public-ingress-sgr and use the :exp:YYYY-MM-DD suffix to force expiry so exceptions aren’t forgotten.
  • Balancing soft vs hard fail

    • Use soft-fail behavior in rapid PR feedback and hard-fail in gate jobs. Checkov exposes --soft-fail, --soft-fail-on, and --hard-fail-on to tune exit behavior by check ID or severity, which lets you say “do not fail the PR for MEDIUM or lower, but fail on HIGH/CRITICAL” at run-time.
    • tfsec supports --exclude/-e for rule-level exclusions and integrates with PR commenter actions that can be run with --soft-fail to annotate without failing the pipeline.
  • Baselines for legacy noise

    • Use Checkov’s baseline feature to capture the current universe of findings and only fail on new findings: --create-baseline to generate .checkov.baseline, and --baseline <file> to run against it. Baselines let you adopt IaC scanning incrementally rather than trying to fix thousands of legacy issues overnight.
  • Policy-as-code: make rules testable and versioned

    • Treat custom checks like software: put them in a repo, require PRs and unit tests, and load them into CI using --external-checks-dir or --external-checks-git for Checkov, or by placing _tfchecks.json/_tfchecks.yaml under .tfsec/ (or using --custom-check-dir) for tfsec. This supports auditability and reproducibility.
    • Example Checkov custom check (Python skeleton):
    # python: custom_checks/s3_pci_acl.py
    from checkov.terraform.checks.resource.base_resource_check import BaseResourceCheck
    from checkov.common.models.enums import CheckResult, CheckCategories
    
    class S3PCIBucketPrivate(BaseResourceCheck):
        def __init__(self):
            name = "S3 PCI-scoped buckets must not be public"
            id = "CKV_CUSTOM_001"
            supported_resources = ("aws_s3_bucket",)
            categories = (CheckCategories.BACKUP_AND_RECOVERY,)
            super().__init__(name=name, id=id, categories=categories, supported_resources=supported_resources)
    
        def scan_resource_conf(self, conf):
            tags = conf.get("tags", [])
            # implement logic to check tags and ACL
            return CheckResult.PASSED
    
    check = S3PCIBucketPrivate()
    

    Example creation and execution details are documented in Checkov’s custom policy guide.

  • Record exceptions as tracked artifacts

    • Use expiry metadata (tfsec supports :exp:), baselines, and a central exceptions file or service so every ignore has an owner, reason, and expiry. This turns ad-hoc ignores into reviewable, auditable risk acceptances.

Important: Suppressions are risk acceptances, not fixes. Every suppression must include a reason, an owner, and a re-review date in the workflow.

Pipeline patterns that provide fast feedback and enforce security gates

Design pipelines that deliver immediate developer feedback without degrading velocity, and that enforce unacceptable risk before merge.

  • Two-phase pattern (fast feedback + enforcement gate)

    1. PR-stage (fast, noisy-reducing): run a focused, fast scanner with soft-fail and PR annotations so developers get line-level feedback without blocked merges for low-to-medium severity. Use tfsec-pr-commenter-action for Terraform-centric projects or Checkov with soft_fail: true for broader stacks.
    2. Merge/staging gate (strict): run the full, slow scan with --hard-fail-on for CRITICAL/HIGH and fail the pipeline if those checks trigger. Protect main with branch protection rules that require the enforcement job as a passing status check.
  • Concrete GitHub Actions examples

    • Fast PR scan using tfsec PR commenter (annotates the PR, soft-fail):
  name: tfsec PR scan
  on: [pull_request]
  jobs:
    tfsec-pr:
      runs-on: ubuntu-latest
      permissions:
        contents: read
        pull-requests: write
      steps:
        - uses: actions/checkout@v4
        - name: tfsec PR comments
          uses: aquasecurity/tfsec-pr-commenter-action@v1.2.0
          with:
            tfsec_args: --soft-fail --format sarif
            github_token: ${{ secrets.GITHUB_TOKEN }}
Enter fullscreen mode Exit fullscreen mode

This uses the tfsec PR commenter to surface findings as comments without failing the PR job.

  • Fast PR scan using Checkov action (soft feedback, SARIF output):
  - name: Checkov PR scan
    uses: bridgecrewio/checkov-action@v12
    with:
      output_format: cli,sarif
      soft_fail: true
  - name: Upload SARIF
    if: success() || failure()
    uses: github/codeql-action/upload-sarif@v4
    with:
      sarif_file: results.sarif
Enter fullscreen mode Exit fullscreen mode

The SARIF upload integrates results into GitHub code scanning, which supports triage in the GitHub UI.

  • Enforcement job (strict): install and run Checkov and fail on high/critical:
  - name: Install Checkov
    run: pip install checkov
  - name: Enforce IaC policies (Checkov)
    run: |
      checkov -d infra/ -o cli -o sarif --hard-fail-on CRITICAL,HIGH --output-file-path results.sarif
  - name: Upload SARIF to GitHub
    uses: github/codeql-action/upload-sarif@v4
    with:
      sarif_file: results.sarif
Enter fullscreen mode Exit fullscreen mode

Branch protection must require this enforcement job to pass before merge.

  • Performance and scope tactics
    • Limit PR scans to changed directories or modules using git diff --name-only to avoid scanning the entire repo every change. Use caching for downloaded modules and run the full scan only in nightly builds. Also use Checkov’s --repo-root-for-plan-enrichment when scanning terraform plan JSON to enrich results with file/line info.

Reporting, triage, and remediation workflows that scale

A scanner is only as good as the process that handles its output.

  • Automated triage pipeline

    1. Detect — scanner runs and emits SARIF/JSON. SARIF uploads populate GitHub code scanning or internal dashboards.
    2. Classify — map findings to severity, team/owner and rule-id. Use rule metadata (where available) to map to owners and to a remediation playbook.
    3. Assign & ticket — create an issue automatically (or tag the PR) for HIGH/CRITICAL findings assigned to the owning team. Low/medium findings can be left for the PR author with a remediation suggestion. Capture the reasoning when an exception is requested.
    4. Track — exceptions and baselines must be time-boxed and re-evaluated; use :exp: for tfsec inline ignores or baseline artifacts for Checkov so exceptions surface at expiry.
    5. Measure — track new vs. existing findings, mean time to remediate (MTTR) by severity, and rule churn. Keep a rolling dashboard.
  • Example triage policy table

Severity Default pipeline action Ownership SLA (example)
CRITICAL Block merge (hard-fail) Security on-call 24 hours
HIGH Block merge in gate; PR author notified Platform/Service owner 3 business days
MEDIUM PR warning (soft) PR author 2 weeks
LOW Advisory only PR author N/A
  • Automation hooks
    • Use SARIF uploads to populate GitHub’s code scanning UI, enabling developers to view and triage findings in a familiar place.
    • Use scan outputs to automatically create issues in the team’s tracker with rule metadata and remediation steps; include the failing code block, the check ID, and the suggested fix.

Operational checklist: integrating Checkov and tfsec into CI

A step-by-step checklist you can apply today.

  1. Create a baseline scan and identify triage owners:
    • Run a full Checkov scan and save baseline: checkov -d . --create-baseline to create .checkov.baseline.
  2. Wire fast feedback into PRs:
    • For Terraform-only repos, enable aquasecurity/tfsec-pr-commenter-action with --soft-fail so PRs get annotated rather than blocked immediately.
    • For mixed IaC, run bridgecrewio/checkov-action with soft_fail: true and SARIF output to upload to code scanning.
  3. Configure an enforcement gate:
    • Add a pipeline job that runs the full checks and uses --hard-fail-on CRITICAL,HIGH (or the rule IDs you consider blocking). Protect main with branch protection rules requiring this job.
  4. Centralize custom policies and tests:
    • Put Checkov Python/YAML custom checks in a dedicated repo and load with --external-checks-git or --external-checks-dir. Develop unit tests for rules as part of CI for the policy repo.
    • For tfsec, place _tfchecks.json/_tfchecks.yaml files under .tfsec/ and validate with tfsec-checkgen.
  5. Manage exceptions responsibly:
    • Use inline suppressions only with reasons and expiry metadata. Track exceptions in a central register and automate reminders for re-review.
  6. Publish outputs where developers work:
    • Upload SARIF to GitHub Code Scanning or produce JSON for your ticketing tool; create automation to open high-severity tickets with code context.
  7. Monitor and iterate:
    • Track MTTR by severity and rule; retire or rewrite rules that routinely create false positives, and add test cases to policy repos when a rule is changed.

Sources:
Checkov CLI Command Reference - Official Checkov CLI flags and behavior: skip/soft-fail/hard-fail, external checks, plan enrichment, baseline creation.

Suppressing and Skipping Policies - Checkov - Inline suppression syntax checkov:skip=<ID>:<reason> and examples.

bridgecrewio/checkov-action (GitHub) - Official GitHub Action README with output_format, soft_fail, baseline and usage examples.

Python Custom Policies - Checkov - How to author Python-based custom checks for Checkov, with examples.

Ignoring Checks - tfsec (Aquasecurity) - #tfsec:ignore:<rule> syntax and expiration metadata for inline ignores.

Custom Checks - tfsec (Aquasecurity) - Custom check format (_tfchecks.json/_tfchecks.yaml), --custom-check-dir, and tfsec-checkgen.

aquasecurity/tfsec-pr-commenter-action (GitHub) - PR commenter action for tfsec with tfsec_args examples.

About required status checks (GitHub Docs) - Branch protection and required status checks for enforcing CI gates.

Uploading a SARIF file to GitHub (GitHub Docs) - How to upload SARIF results (via github/codeql-action/upload-sarif) and integrate with GitHub code scanning.

Apply the patterns above: run the right scanner in the right stage, make policies code with tests, treat suppressions as tracked exceptions with expiry, and use SARIF + branch-protection to move from noisy scanning to trusted, enforceable security gates.

Top comments (0)