Why IaC Hardening Matters

Here’s the thing about infrastructure as code that makes it both wonderful and terrifying: once you write a mistake into a Terraform module, that mistake doesn’t stay put. It gets copied. A junior engineer finds your s3-bucket module in the internal registry, sees it’s already “battle tested,” and reuses it in three more projects without reading past the variable names. If that module happened to leave logging disabled or granted a role "*" on "*", you haven’t made one mistake, you’ve made a template for making that mistake forever.

That’s the core argument for treating IaC scanning as a first-class part of your pipeline rather than an afterthought. When infrastructure was provisioned by hand, a bad IAM policy was a one-off incident you’d catch in a review or, worse, in an audit. Now the same policy might be instantiated across dozens of environments before anyone notices, because nobody manually inspects every terraform apply. The code is the source of truth, and if the code is wrong, everything downstream inherits that wrongness at scale.

This is what people mean when they talk about “shifting security left” in the DevSecOps sense: instead of waiting for a penetration test or a compliance audit to catch a misconfigured bucket six months after it’s been running in production, you catch it in the pull request, before a single resource gets created. Tools like tfsec and Checkov exist to do exactly that: parse your Terraform (or CloudFormation, or Kubernetes manifests) and flag patterns that are known to be risky, using rule sets built from years of collective incident post-mortems across the industry.

It helps to have a mental model of what kinds of mistakes these tools are actually looking for. A few categories show up again and again in real-world breaches and audits:

Risk TypeExample
Excessive PrivilegeIAM roles with "*" in actions or resources
Unencrypted DataS3 buckets or RDS instances without encryption enabled
Public ExposureSecurity groups with 0.0.0.0/0 ingress
Drift from PolicyMissing tagging or noncompliant naming conventions

Excessive privilege is the classic one: it’s almost always the path of least resistance during development. You’re debugging a Lambda function, you can’t figure out which of the fifteen IAM permissions it actually needs, so you slap "Action": "*" on it to unblock yourself and promise to tighten it later. Later rarely comes, and now you’ve got a function that can do anything in the account sitting behind a public API Gateway endpoint. Unencrypted data is similar: encryption at rest is usually a single boolean flag in Terraform, but it’s off by default in a lot of provider resources, so it silently stays off unless something forces the question. Public exposure (0.0.0.0/0 in a security group ingress rule) is the one that shows up in breach reports constantly, because it’s an easy typo to make when you meant to scope access to your office IP or a VPN CIDR and instead scoped it to the entire internet. And drift from policy, things like missing cost-center tags or naming conventions, feels cosmetic until you realize it’s often what compliance frameworks and cost allocation both depend on; you can’t enforce least privilege or track spend on resources nobody can identify.

Both tfsec and Checkov aim to catch these patterns before they ever reach apply, by treating your HCL or YAML as data to be statically analyzed rather than code to be executed. Neither tool spins up real infrastructure or calls cloud APIs; they just read your configuration files and pattern-match against known-bad shapes. That’s what makes them fast enough to run on every commit.

tfsec Overview

tfsec is a static analysis tool built specifically for Terraform, and that specificity is really its whole design philosophy. It’s written in Go, which gives it a single compiled binary with no runtime dependencies to install, and it works by parsing your HCL into an abstract syntax tree and walking that tree against a library of built-in checks. Because it never actually evaluates your Terraform (no state file, no provider calls, no terraform plan), it can run in a fraction of a second on most repositories and doesn’t need any cloud credentials at all. That’s a meaningful property for a security tool: a scanner that needs your AWS keys to function is itself a bit of an attack surface.

Worth knowing if you’re picking this up today: Aqua Security, who maintains tfsec, folded the project into Trivy back in 2023 as part of consolidating their scanning tools under one roof. The standalone tfsec binary still works and the rule engine lives on, but new development effort has moved to trivy config. If you’re starting a project from scratch it’s worth checking which one your CI templates and Actions marketplace listings actually point to, since both exist in the wild right now.

Key Characteristics

AttributeDescription
Language SupportTerraform (HCL)
Output FormatsCLI, JSON, SARIF
IntegrationGitHub Actions, GitLab CI, Jenkins, CLI
PerformanceVery fast; no external dependencies
Custom RulesSupported via JSON or Rego (OPA)

The SARIF output format is worth calling out specifically, because it’s what lets GitHub render tfsec findings directly in the “Security” tab of a repository, complete with inline annotations on the offending line of code, rather than making a developer scroll through raw CLI output in a build log. That integration alone is a big part of why tfsec became popular for smaller teams: the feedback loop is tight enough that a developer sees the problem in the same interface they’re already using to review the pull request.

Running it locally is about as simple as these tools get:

PLAINTEXT
tfsec ./terraform
Click to expand and view more

And a finding looks like this:

PLAINTEXT
[AWS005][ERROR] S3 Bucket allows public access
  /main.tf:15
  Resource: aws_s3_bucket.data_logs
  Impact: Data could be exposed to the public internet
  Resolution: Restrict public ACLs and block all public access
Click to expand and view more

Notice the shape of that output: it’s not just “this is wrong,” it tells you the resource, the impact if left unfixed, and a concrete resolution. That’s a deliberate choice that pays off in adoption. A scanner that just spits out rule IDs and file paths forces developers to go look up documentation to understand why something matters; one that explains the impact in plain language gets fixed faster because the person reading it doesn’t need security expertise to understand the stakes.

tfsec emphasizes speed and simplicity above all else. It’s easy to drop into a pipeline with almost no configuration, and because it only knows about Terraform, its rule set doesn’t have to hedge or generalize across a dozen different IaC dialects. The tradeoff, and it’s a real one, is that if your organization also manages Kubernetes manifests, CloudFormation templates, or Dockerfiles, tfsec simply has nothing to say about them. You’d need a second tool anyway.


Checkov Overview

Checkov, originally built by Bridgecrew and now part of Palo Alto Networks’ Prisma Cloud portfolio, takes the opposite bet from tfsec: instead of doing one thing extremely well, it tries to be the single scanner that covers your entire IaC surface area. Alongside Terraform it understands CloudFormation, Kubernetes manifests, Helm charts, ARM templates, Dockerfiles, Serverless Framework configs, and a handful of others. Under the hood it converts each of these formats into a common graph representation, which is what lets a single policy engine reason about resources regardless of which language originally defined them.

That policy engine is YAML-based (with an option to write full Python for anything too complex to express declaratively), and its prebuilt rule library is explicitly mapped to compliance frameworks like CIS Benchmarks, NIST 800-53, and PCI DSS. That mapping is not a small detail if your organization has to produce evidence for an auditor. Instead of a security engineer manually cross-referencing “which of our 400 Checkov findings correspond to CIS control 2.1.1,” Checkov can just tell you, because the rule metadata already carries that linkage. This is genuinely useful when the primary audience for your scan results isn’t just developers but also a compliance team that needs to check boxes for SOC 2 or ISO 27001.

Key Characteristics

AttributeDescription
Language SupportTerraform, CloudFormation, Kubernetes, ARM, Dockerfile, etc.
Output FormatsCLI, JSON, JUnit, SARIF, GitHub Security Alerts
IntegrationCI/CD, VS Code, Prisma Cloud
PerformanceModerate; heavier rule engine
Custom RulesStrong support via YAML or Python

The heavier rule engine is a direct consequence of that breadth: Checkov has to load and evaluate a much larger library of checks (well over a thousand, spanning multiple resource graphs) and build out a graph representation of your resources before it can even start applying rules. On a small repo you won’t notice. On a large monorepo with hundreds of modules, that graph construction step is where the extra runtime goes, and it’s a real consideration if you’re trying to keep CI feedback under a couple of minutes.

Running a scan is similarly straightforward:

PLAINTEXT
checkov -d ./terraform
Click to expand and view more

And a typical finding:

PLAINTEXT
Check: CKV_AWS_20: Ensure S3 bucket has versioning enabled
  File: /main.tf:23
  Resource: aws_s3_bucket.data_logs
  Severity: Medium
  Guide: https://docs.bridgecrew.io/docs/s3_13-enable-versioning
Click to expand and view more

Notice this finding is a different flavor of problem than the tfsec example earlier: versioning isn’t about a resource being publicly exposed, it’s about resilience, being able to recover an object after an accidental delete or a ransomware-style overwrite. That’s a good illustration of Checkov’s broader net: because its rule library draws from more compliance frameworks and more resource types, it tends to surface a wider spectrum of concerns beyond the classic “is this public” and “is this encrypted” checks that dominate tfsec’s built-ins.

Checkov’s real strength is breadth combined with policy mapping. It’s the better fit for teams managing multi-cloud environments, or any team where “prove to an auditor that we’re compliant” is a recurring, not hypothetical, requirement.

tfsec vs. Checkov: Feature Comparison

CapabilitytfsecCheckov
Terraform SupportYesYes
Multi-IaC Language SupportNoYes
Speed and PerformanceFasterSlower but broader
Built-in Rule Count~500+~1000+
Custom Rule FrameworkJSON / RegoYAML / Python
Compliance Mapping (CIS/NIST/etc.)LimitedExtensive
Policy as Code IntegrationPartial (OPA)Full
Integration with Cloud PlatformsMinimalNative Prisma Cloud support

It’s tempting to read a table like this as a scoreboard and just crown a winner, but the more useful way to read it is as two different bets about where your bottleneck actually lives. If your bottleneck is developer attention, meaning you need scan results fast enough that they don’t feel like friction in the PR review process, tfsec’s speed and focused rule set win out; a scan that finishes before a developer has even switched tabs gets used, one that takes two minutes gets ignored or run less often. If your bottleneck is proving compliance to a third party, or you’ve got infrastructure spread across Terraform, Helm, and raw Kubernetes YAML in the same repo, Checkov’s breadth stops being a “nice to have” and becomes the only option that actually covers your surface area.

The rule counts are also worth a second look rather than taking at face value. More rules isn’t automatically better: a bigger rule library also means a bigger surface for false positives, and false positives are the single fastest way to get a security tool disabled entirely. Developers who get burned by “gotcha” findings that don’t apply to their context (a public S3 bucket that’s intentionally a static website host, say) stop trusting the tool’s output and start reflexively suppressing or ignoring it. Whichever tool you choose, budget real time for tuning the rule set to your environment rather than running with every default enabled and hoping for the best.

In summary:

  • tfsec is better suited for smaller, Terraform-only environments, or anywhere speed and a low-friction developer experience are the priority.
  • Checkov fits complex multi-cloud or compliance-focused pipelines that need deep customization and explicit framework alignment.

Integration in a DevSecOps Pipeline

Knowing that a scanner exists doesn’t help much if it only ever runs on someone’s laptop before they remember to run it, which in practice means it basically never runs. The value of these tools comes almost entirely from wiring them into CI/CD so that scanning happens automatically, on every push and every pull request, without anyone having to remember to do it. Here’s what that looks like as a GitHub Actions workflow running both tools in parallel:

YAML
name: IaC Security Scan
on: [push, pull_request]
jobs:
  tfsec-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run tfsec
        uses: aquasecurity/tfsec-action@v1.0.0
        with:
          working_directory: ./terraform

  checkov-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: ./terraform
Click to expand and view more

Running the two jobs in parallel rather than sequentially matters more than it might seem: since neither tool depends on the other’s output, there’s no reason to make a developer wait for tfsec to finish before Checkov even starts. On a busy repository, shaving even thirty seconds off the feedback loop adds up across hundreds of pull requests a month.

There’s a decision hiding in a workflow like this one that’s worth calling out explicitly: what happens when a check fails? Both actions can be configured to fail the build outright on any finding above a given severity, which is the strict approach, or to just annotate the PR and let a human decide whether to merge anyway, which is the permissive approach. Going strict from day one on a codebase that’s never been scanned before is a great way to make the whole team hate the security tool, because you’ll surface hundreds of pre-existing findings all at once and block every PR until someone triages the backlog. A more realistic rollout path is to start in report-only mode, use that period to baseline and fix the worst existing issues, and only flip the switch to fail-the-build once the noise floor is low enough that a new finding actually means something went wrong, rather than being lost in a sea of pre-existing debt.

This kind of setup gives developers feedback on IaC risks before code merges or infrastructure changes get applied, which is the entire point: the fix is a comment on a diff that hasn’t shipped yet, not an incident ticket after a bucket’s already been public for a week.

Extending Policy Coverage

The built-in rule sets in both tools are drawn from broad, generic security knowledge: things that are risky for basically any organization, like public buckets or wildcard IAM. But your organization almost certainly has its own conventions that no generic tool could know about ahead of time, like a requirement that every resource carry a CostCenter tag, or a rule that nothing in a particular VPC can use an instance type above a certain size. That’s what custom policies are for, and both tools support writing them, though the mechanics differ quite a bit.

Here’s a custom tfsec rule expressed as JSON, checking that every resource has an Environment tag:

JSON
{
  "code": "CUST001",
  "description": "Ensure all resources have environment tags",
  "impact": "Lack of tagging reduces traceability",
  "resolution": "Add the 'Environment' tag to all resources",
  "severity": "LOW",
  "matchSpec": {
    "name": "tags.Environment",
    "action": "isPresent"
  }
}
Click to expand and view more

This is a fairly declarative, fill-in-the-blanks style: you’re describing what field to look at and what condition should hold, and tfsec’s engine does the rest. For anything more elaborate than “does this field exist,” tfsec also supports writing rules in Rego, the policy language behind Open Policy Agent, which gives you a real logic language to express conditions across multiple resources or attributes at once.

The equivalent custom policy in Checkov is written in YAML:

YAML
metadata:
  id: CUST001
  name: Ensure resources have environment tag
  severity: LOW
scope:
  provider: aws
definition:
  and:
    - cond_type: attribute
      resource_types: ["aws_instance"]
      attribute: "tags.Environment"
      operator: exists
Click to expand and view more

Checkov’s YAML policies read almost like configuration for the checker itself: you declare a scope (which provider, which resource types), then a definition block that can combine multiple conditions with logical operators like and and or. For genuinely complex logic, like “this rule should only apply if the instance is in a production VPC and doesn’t already have an exception annotation,” you can drop down to writing the check directly in Python, which gives you the full expressiveness of a real programming language rather than being boxed into YAML’s declarative shape.

Writing and maintaining custom rules like these is one of the clearest signals of a mature DevSecOps practice, because it means security requirements have moved out of a wiki page nobody reads and into something that’s actually enforced automatically, the same way a linter enforces a style guide. The catch is that custom rules need the same care as any other code: they need tests, they need to live in version control, and someone needs to own updating them as your infrastructure conventions evolve. A stale custom rule that no longer matches reality is just as capable of generating noise and eroding trust in the tool as a bad default rule would be.

Recommendations

ScenarioRecommended Tool
Single-cloud Terraform projectstfsec
Multi-cloud IaC environmentsCheckov
High compliance requirements (CIS, NIST)Checkov
Developer-friendly and fast checkstfsec
Advanced customization and reportingCheckov

If you take nothing else away from this comparison, it’s that “which tool is better” is the wrong question. They’re solving overlapping but genuinely different problems: tfsec optimizes for a fast, low-friction developer feedback loop on Terraform specifically, while Checkov optimizes for breadth of coverage and compliance traceability across your whole IaC estate. For a lot of teams, the honest answer isn’t to pick one, it’s to run both in sequence: tfsec first, for the near-instant “did you just do something obviously wrong” feedback that developers will actually wait around for, and Checkov afterward (maybe as a required check rather than a blocking one on every commit) for the deeper compliance validation that a security or platform team actually needs to sign off on a release. Neither tool replaces good judgment or a real threat model, but together they catch the class of mistake that’s easy to make and expensive to leave sitting in production: the misconfiguration nobody meant to ship.

Copyright Notice

Author: Kernelstub

Link: https://blog.kernelstub.dev/posts/comparing-tfsec-and-checkov-for-hardening-infrastracture/

License: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.

© 2026 Prepakis Georgios | Kernelstub | Security Researcher. All rights reserved.

Start searching

Enter keywords to search articles

↑↓
ESC
⌘K Shortcut