Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCIX · Terraform Delivery PipelineSecurityLayer

tfsec and checkov in CI — the security layer

Advanced⏱ ~28 mingitterraformtfseccheckov

What you'll learn

  • Run tfsec and checkov as the security stage of a Terraform CI pipeline
  • Distinguish tfsec rule sets from checkov rule sets and the overlap between them
  • Wire scanner findings as build failures and post SARIF output to the PR
  • Identify the placement of the security stage between lint and plan

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The fourth stage of a production Terraform pipeline is the security layer. It runs after tflint --recursive (which catches provider-aware correctness) and before terraform plan (which reads remote state). Its job is to catch the policy violations that validate and tflint were never designed to catch: open security groups, unencrypted storage, missing logging, public S3 buckets, IAM policies without conditions. These are the changes that pass every earlier gate and reach production as silent incidents.

Why a security stage is needed

terraform validate checks the schema. tflint checks conventions. Neither evaluates whether the resource the configuration declares is safe in the policy sense. A security group ingress rule that allows 0.0.0.0/0 to port 22 is a perfectly valid HCL expression with a perfectly valid schema. The configuration is correct; the policy is wrong.

The two scanners most teams reach for are tfsec and checkov. Both walk the working tree, apply large rule sets of security and compliance checks, and emit structured output suitable for posting back to the PR. They overlap significantly — most rule sets in tfsec have a counterpart in checkov — but they were built by different teams with different priorities, and running both produces fewer false negatives than either alone.

The tfsec stage

tfsec is a static-analysis scanner for Terraform. It walks the configuration, evaluates expressions symbolically where possible, and applies a rule set that includes checks for AWS, Azure, Google Cloud, and Kubernetes providers.

tfsec .

The default invocation scans the working directory recursively. Findings are printed as a human-readable summary. The flag that belongs in CI is the SARIF output format:

tfsec . --format sarif --out tfsec.sarif

SARIF (Static Analysis Results Interchange Format) is the standard the GitHub code-scanning API consumes. Posting tfsec.sarif as a workflow artefact and uploading it via github/codeql-action/upload-sarif renders the findings inline on the PR as annotations on the offending lines.

flowchart LR
    A["Working tree HCL"] --> B["tfsec . --format sarif"]
    B --> C{"Any HIGH or CRITICAL finding?"}
    C -->|"yes"| D["Exit non-zero, SARIF uploaded"]
    C -->|"no"| E["Exit zero"]
    D --> F["PR blocked, findings inline"]

Findings are categorised by severity (CRITICAL, HIGH, MEDIUM, LOW). A common CI configuration fails the build on HIGH and CRITICAL only, treats MEDIUM as a warning check, and ignores LOW. The thresholds are a deliberate team policy and should be reviewed regularly.

The checkov stage

checkov is a static-analysis scanner with a larger and more aggressive rule set than tfsec. It walks the configuration, evaluates expression graphs, and applies checks that include CIS benchmarks, PCI-DSS controls, and HIPAA-relevant controls. It also scans Dockerfile, Kubernetes manifests, and ARM templates, which is useful in repositories that contain more than just Terraform.

checkov -d . --output sarif --output-file-path checkov.sarif

The -d flag sets the directory to scan. The --output sarif --output-file-path pair writes SARIF to disk for upload. Like tfsec, findings are severity-tagged and can be filtered by check severity, by resource type, or by check ID.

A pipeline that runs both produces an “intersection of coverage” view: a finding flagged by both scanners is high-confidence; a finding flagged by only one may be a false positive worth a --skip-check exemption with a documented rationale.

Placement in the pipeline

The security stage runs after tflint and before plan. The ordering is deliberate:

  1. fmt — formatting.
  2. init -backend=false — providers, no state.
  3. validate — HCL semantics.
  4. tflint — provider-aware rules and conventions.
  5. tfsec and checkov — security and compliance policy.
  6. plan — reads remote state, evaluates drift.
  7. policy gate — OPA or Sentinel on the plan output.
  8. human approval.
  9. apply.

Placing security scanning after tflint means the security scanner evaluates only configurations that pass the linter. Placing it before plan means a security finding blocks the pipeline before the plan job opens a network path to remote state. The cost of a tfsec rejection is roughly ten to twenty seconds; a checkov rejection is similar. A rejected apply is far more expensive.

Wiring findings back to the PR

Both scanners produce SARIF. Both can be wired to fail the build on severity threshold and to upload findings for inline display on the PR. The minimum viable integration:

- name: tfsec
  run: tfsec . --format sarif --out tfsec.sarif --minimum-severity HIGH
- name: upload tfsec SARIF
  if: always()
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: tfsec.sarif
- name: checkov
  run: checkov -d . --output sarif --output-file-path checkov.sarif --check HIGH,CRITICAL
- name: upload checkov SARIF
  if: always()
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: checkov.sarif

The if: always() on the upload steps ensures SARIF is uploaded even when the build fails, so the reviewer sees the findings inline rather than having to dig through CI logs.

Production discipline

  1. Both scanners run on every PR. Findings are uploaded as SARIF and rendered inline.
  2. HIGH and CRITICAL findings fail the build. MEDIUM posts as a warning check; LOW is ignored.
  3. Exemptions are documented inline with a reason, a reviewer, and a reference. An undocumented tfsec:ignore is a bug.
  4. Scanner versions are pinned. tfsec and checkov both ship rule-set changes; an unpinned version can fail builds on a new rule that has not been reviewed.
  5. The security stage runs after tflint and before plan. It evaluates only well-formed configurations and does not open a state-backend connection.

Cross-course references

  • Linux for Production Sysadmins - Parts XXVIII-XXXI (Secrets) cover the credential-handling rules the security stage assumes.
  • Terraform for Production Sysadmins - Parts XV-XVII (Security) cover the policy decisions this stage enforces.
  • This course, Part LIII (PolicyAsCode) - the OPA and Sentinel policy gates that consume the SARIF output and the plan file.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the primary purpose of running both `tfsec` and `checkov` in the security stage rather than just one?

  2. Q2. A `tfsec:ignore` comment in an HCL file overrides a security policy and requires no further documentation.

  3. Q3. Name the two output formats a Terraform security scanner can produce that are most useful in CI, and explain why each is useful.

  4. Q4. Diagnose a pipeline that ships an open security group despite running tfsec and checkov.

    A team runs tfsec and checkov on every PR. A contributor adds an `aws_security_group_rule` that exposes port 22 to `0.0.0.0/0` but tags it with `tfsec:ignore AWS-008 'intentional, internal jumphost'`. The PR is merged. Three months later, a security audit finds the rule and the team cannot reconstruct who approved the exemption or why.

Passing score: 75%. Answers are checked in this browser.