Skip to main content

IaC Security

What is IaC Security?

IaC security checks detect security misconfigurations in infrastructure-as-code files (such as Terraform, CloudFormation, Kubernetes YAML, and Dockerfile) before deployment, including public S3 buckets, missing encryption, and overly broad permissions. Because infrastructure misconfigurations can cause broader damage than application vulnerabilities, blocking them at the code review stage is critical.

The configuration below is an example — a fully working implementation lives in the reference repository

The YAML and commands on this page are examples that show the essentials. For a complete, copy-and-run pipeline (including policy files and a sample app), see the Best Practice repository.

Tags in these examples versus production settings

The examples below keep mutable tags such as @v7 for readability. A tag can be repointed to a different commit later, so in production pin each action to a full commit SHA and grant only the permissions a job needs with a permissions: block. See Pipeline Security for the reasoning and the procedure.


Tool Comparison

ToolKey CharacteristicsSupported TargetsLicense
CheckovBroad coverage + custom policy supportTerraform, K8s, CF, Dockerfile, ARMApache-2.0
tfsecTerraform-focused + fastTerraformMIT
TrivyIncludes IaC scan (integrated with container security)Terraform, K8s, DockerfileApache-2.0
KubesecKubernetes-only security scoringKubernetes YAMLApache-2.0

For multi-IaC environments, Checkov is recommended; for integration with container security, use Trivy. Maintenance of tfsec is being migrated to Trivy, so if you need Terraform-only checks, consider trivy config first.


Checkov Setup

Checkov provides more than 500 built-in policies and runs both locally and in CI without a separate server. It supports SARIF output, so integrating with the GitHub Security tab lets you review results directly in PRs.

Basic Usage

Bash
# scan entire current directory
checkov -d .

# scan specific framework only
checkov -d . --framework terraform
checkov -d . --framework kubernetes

# run specific checks only
checkov -d . --check CKV_AWS_18,CKV_AWS_19

# output results as JSON
checkov -d . -o json > checkov-report.json

GitHub Actions

YAML
# .github/workflows/iac-security.yml

name: IaC Security — Checkov

on:
pull_request:
branches: [main, develop]

jobs:
checkov:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # required to upload SARIF
steps:
- uses: actions/checkout@v7

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

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

GitLab CI

YAML
# .gitlab-ci.yml (iac-security job section)

iac-security:
stage: test
image: bridgecrew/checkov:latest
script:
# exit 1 on violations (hard fail) is the default behavior
- checkov -d .
--framework terraform,kubernetes,dockerfile
--output cli
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

tfsec Setup (Terraform-only)

tfsec is specialized for Terraform, runs quickly, and includes built-in security rules for major cloud providers such as AWS, Azure, and GCP. However, its maintenance is being migrated to Trivy, so for new adoption consider trivy config first. It is also useful alongside Checkov when you want deeper Terraform-specific checks.

GitHub Actions

YAML
# .github/workflows/iac-security-tfsec.yml (Terraform only)

name: IaC Security — tfsec

on:
pull_request:
branches: [main]

jobs:
tfsec:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7

- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.3
with:
soft_fail: false

Exception Handling

Keep exceptions traceable by declaring them inline in code

If you must skip specific checks, document the reason with inline comments in infrastructure code.

Hcl
# Terraform inline exception example

resource "aws_s3_bucket" "logs" {
bucket = "my-log-bucket"

# checkov:skip=CKV_AWS_18:access-log bucket does not require self-logging
# checkov:skip=CKV_AWS_144:Log bucket does not need cross-region replication
}
YAML
# Kubernetes inline exception example

metadata:
annotations:
checkov.io/skip1: 'CKV_K8S_14=test-environment-only pod'

What is the green badge actually based on

A scan passing and a scan checking are two different things

Once an IaC scan is wired up, the badge turns green. Whether that green means "no violations" or "nothing was examined" is not something the badge can tell you. What follows are four cases from adding scanning and a blocking gate to TRUSCA (iac-security.yml, on Trivy config rather than Checkov). In the first three the scan result looked correct; in the fourth the setting itself was not what it appeared to be.

Passing while scanning a chart that never rendered

When trivy config scans a Helm chart whose templates require values, rendering fails. Trivy records the render error as a warning, returns zero results, and exits 0. The job passes. The badge is green with nothing examined at all.

Two things have to happen together here. Supply a scan-only values file through --helm-values so the templates render, and separately count the files that were inspected.

Bash
count=$(jq '[.Results[]?] | length' trivy-chart.json)
echo "config files inspected: ${count}"
if [ "${count}" -eq 0 ]; then
echo "::error::Trivy inspected 0 files. The chart failed to render."
exit 1
fi

Adding the values file alone is not enough. When a later template change breaks rendering again, you land in the same place. The count is what turns that regression into a failure.

Failing on something that is never deployed

The opposite direction also happens. A container image scan kept reporting a package that had been patched upstream weeks earlier. The cause was a cached image layer. A cached layer does not run again, so the apt-get upgrade inside it does not run either, and the layer keeps serving the package versions it was first built against. The image being released carried the patched version; only the scan was looking at the old one.

A red gate does get noticed eventually. The cost is what happens in the meantime, as reviewers grow used to the red. The fix is to key the cache scope on time so drift is bounded. Putting the ISO week in the key caps it at a week, and one cold build per week pays for it.

Raising the blocking level and losing the block

This one shows up when moving from observe to block. Say the gate counts like this:

Bash
n=$(jq --arg s "${BLOCKING_SEVERITY}" \
'[.Results[]?.Misconfigurations[]? | select(.Severity == $s)] | length' "$file")

Changing BLOCKING_SEVERITY from CRITICAL to CRITICAL,HIGH looks like it widens what gets blocked. But no finding has a Severity equal to the string "CRITICAL,HIGH". Nothing matches, the count is zero on every run, and the gate always passes. The block disappears at the exact moment you believe you strengthened it.

The value and the comparison have to move together.

Bash
sevs=$(printf '%s' "${BLOCKING_SEVERITY}" | jq -Rc 'split(",")')
n=$(jq --argjson sevs "$sevs" \
'[.Results[]?.Misconfigurations[]? | select(.Severity as $s | $sevs | index($s))] | length' "$file")

Fixing it is not where this ends. A gate passing with zero violations looks identical to a gate passing because its comparison broke. Only deliberately reviving a violation separates the two. In TRUSCA that meant rendering the chart with a securityContext removed and confirming side by side that the new condition counts 3 while the old one counts 0 on the same input.

A setting you think you turned off

This comes up when a chart ships a default and someone wants to drop it. An empty map looks like it should remove the block. It does not.

YAML
# Does NOT remove it. Helm deep-merges maps, so the defaults stay.
worker:
containerSecurityContext: {}

# This removes it.
worker:
containerSecurityContext: null

What makes this dangerous is the direction of the error. Whoever tried to turn it off believes it is off while it is still on. For a security setting that is the lucky case, and the reverse holds too. If a values file's own comment says {} clears the block, anyone who trusts that comment deploys something other than what they intended.

What they have in common

The first three displayed correctly: green, or red with a plausible-sounding reason. The fourth has no display at all - it exists only for whoever believed they had changed a setting. In all four, what was wrong was not the verdict but the basis for it.

So build gates that report their basis alongside their verdict: how many files were inspected, how many findings at which severity, which target was read. Print it to the log or the job summary. And after raising a gate to blocking, create a violation on purpose and watch it get stopped once. "There were zero findings" and "the scanner counted zero" are different statements, and without something that tells them apart, a pipeline quietly slides back to observing.


Key Checks

The items below are major root causes of real incidents

At initial adoption, enabling these checks first can quickly reduce real risk. After the team gets used to results, expand to the full policy set.

ItemCheckov IDDescription
Block S3 public accessCKV_AWS_53Configure bucket public access blocking
S3 encryptionCKV_AWS_19Enable server-side encryption
Security group 0.0.0.0CKV_AWS_24Disallow SSH port (22) open to the world
Prevent root in K8sCKV_K8S_23Block containers running as root
K8s resource limitsCKV_K8S_11 · CKV_K8S_13Set CPU limits and memory limits
K8s secret managementCKV_K8S_35Inject secrets as files instead of environment vars

IaC Security Fixer

Upload a Checkov result file to automatically generate fixed code for each violation. It provides directly applicable fixed files, not just reports.

Preview with a sample first (no API key required)

You can view the fixed code for a pre-built sample scan result right away, without an API key. Get a feel for what the tool produces first, then run a real analysis on your own results below.

Fix your own results

This tool requires an Anthropic API key

It calls the Anthropic API directly from your browser. Enter your own Anthropic API key to use it right away. Your key and inputs are sent only from your browser to Anthropic (they never pass through a trustedoss server). Usage is billed to your own Anthropic account.

Self-Study

Generate IaC remediation code directly with Claude Code

The fixer above is available directly in your browser. If you need full files generated with fixes directly applied to original .tf files, use the agent below.

Prerequisite: Clone the Trusted OSS repository

Bash
cd agents/en/iac-fixer
claude

The agent automatically performs the following:

  • Automatically parses Checkov result files (JSON)
  • Generates direct remediation code for fixable findings
  • Inserts checkov:skip comments for non-fixable items
  • Generates full fixed files when originals are provided

Next Steps

  • Verify after deployment with dynamic analysis: DAST
  • Integrate the full pipeline: Pipeline Design