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 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.
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
| Tool | Key Characteristics | Supported Targets | License |
|---|---|---|---|
| Checkov | Broad coverage + custom policy support | Terraform, K8s, CF, Dockerfile, ARM | Apache-2.0 |
| tfsec | Terraform-focused + fast | Terraform | MIT |
| Trivy | Includes IaC scan (integrated with container security) | Terraform, K8s, Dockerfile | Apache-2.0 |
| Kubesec | Kubernetes-only security scoring | Kubernetes YAML | Apache-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
# 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
# .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
# .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
# .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
If you must skip specific checks, document the reason with inline comments in infrastructure code.
# 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
}
# Kubernetes inline exception example
metadata:
annotations:
checkov.io/skip1: 'CKV_K8S_14=test-environment-only pod'
What is the green badge actually based on
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.
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:
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.
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.
# 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
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.
| Item | Checkov ID | Description |
|---|---|---|
| Block S3 public access | CKV_AWS_53 | Configure bucket public access blocking |
| S3 encryption | CKV_AWS_19 | Enable server-side encryption |
| Security group 0.0.0.0 | CKV_AWS_24 | Disallow SSH port (22) open to the world |
| Prevent root in K8s | CKV_K8S_23 | Block containers running as root |
| K8s resource limits | CKV_K8S_11 · CKV_K8S_13 | Set CPU limits and memory limits |
| K8s secret management | CKV_K8S_35 | Inject 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
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
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
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:skipcomments 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