tfsec: The Terraform Security Scanner That Became Part of Trivy
Hook
tfsec scanned over 6 million Terraform files before its creators made the bold decision to sunset it in favor of their own competing tool, Trivy. Here's why that was the right move.
Context
In the early days of Terraform adoption, security testing happened too late—after infrastructure was already deployed to the cloud. Teams would discover misconfigured S3 buckets, overly permissive security groups, or unencrypted databases only after cloud security posture management tools flagged them in production. The feedback loop was measured in days or weeks, and fixes required coordination between security and infrastructure teams.
tfsec emerged in 2019 to shift security left, bringing static analysis directly into the Terraform development workflow. Unlike simple pattern matchers that looked for hardcoded secrets, tfsec understood Terraform's HCL syntax deeply enough to evaluate expressions, follow variable references across files, and detect security issues in resource relationships. It could catch a security group allowing 0.0.0.0/0 access even when that CIDR block was constructed through string concatenation or passed through module variables. The tool gained rapid adoption because it was fast, required no agent or cloud credentials, and integrated seamlessly into existing CI/CD pipelines. By the time Aqua Security acquired the project and eventually merged it into Trivy, tfsec had become the de facto standard for pre-deployment Terraform security scanning.
Technical Insight
What made tfsec architecturally interesting wasn't just that it parsed HCL—it's that it built an evaluation engine capable of reasoning about Terraform's execution model without actually running Terraform. When you write a Terraform security rule in most tools, you're pattern matching against static text. tfsec constructed an abstract syntax tree from your Terraform code and evaluated it with context about resource relationships, variable values, and even Terraform functions.
Consider this Terraform code that appears secure at first glance:
variable "allowed_cidrs" {
default = ["10.0.0.0/8"]
}
resource "aws_security_group" "api" {
name = "api-sg"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = concat(var.allowed_cidrs, ["0.0.0.0/0"])
}
}
A naive pattern matcher would miss the security issue because "0.0.0.0/0" isn't directly in the resource block. tfsec's expression evaluator would execute the concat() function, recognize that the resulting array contains the dangerous CIDR block, and flag rule aws-ec2-no-public-ingress-sgr. This expression-aware analysis extended to understanding format(), join(), conditional expressions, and even locals that reference other locals.
The rule engine itself was plugin-based, with each check implemented as a Go function that received parsed resource data and returned violations. Rules were organized by provider (AWS, Azure, GCP) and resource type, with metadata including severity, description, remediation advice, and links to compliance frameworks. Here's a simplified version of what a rule looked like internally:
func CheckS3BucketEncryption(block *terraform.Block) (results []Result) {
if block.Type() != "aws_s3_bucket" {
return
}
encryption := block.GetAttribute("server_side_encryption_configuration")
if encryption.IsNil() {
results = append(results, Result{
RuleID: "aws-s3-enable-bucket-encryption",
Description: "S3 bucket does not have encryption enabled",
Severity: High,
Range: block.Range(),
})
}
return
}
Beyond built-in rules, tfsec supported custom policies written in Rego, the policy language from Open Policy Agent. This was crucial for organizations with specific compliance requirements not covered by the default rule set. You could write a policy that enforced your company's tagging standards, naming conventions, or architectural patterns:
package custom.tags
import data.lib.terraform
required_tags := ["Owner", "CostCenter", "Environment"]
deny[msg] {
resource := terraform.resources[_]
resource.type == "aws_instance"
existing_tags := object.keys(resource.tags)
missing := required_tags[_]
not missing in existing_tags
msg := sprintf("EC2 instance '%s' missing required tag: %s",
[resource.name, missing])
}
tfsec's performance characteristics made it viable for pre-commit hooks and fast feedback loops. It scanned typical Terraform codebases in under a second by avoiding the overhead of initializing Terraform providers or making API calls. The tool parsed HCL using HashiCorp's own hcl library, built a dependency graph, evaluated expressions in topological order, and ran all enabled checks in parallel across goroutines. This architecture meant scanning time scaled linearly with codebase size rather than exhibiting the exponential growth of tools that performed full Terraform plan operations.
The output flexibility was another strength. tfsec could emit findings in JSON for programmatic processing, SARIF for GitHub Code Scanning integration, JUnit XML for CI dashboards, or human-readable terminal output with color-coded severity levels and code snippets showing exactly where issues occurred. This made it easy to fail CI builds on high-severity findings while allowing warnings to pass through for visibility without blocking deployments.
Gotcha
The biggest gotcha with tfsec today is that it's officially deprecated. Aqua Security consolidated the scanning engine into Trivy, which means tfsec won't receive new cloud provider rules, bug fixes beyond critical security issues, or updates to support new Terraform language features. If you're using Terraform 1.8+ with new HCL syntax, tfsec might not parse it correctly. The deprecation isn't sudden—the maintainers provided clear migration paths and Trivy inherited tfsec's entire rule set—but it means any new adoption is technical debt you're deliberately taking on.
The more fundamental limitation is inherent to static analysis: tfsec has no runtime context. It can't tell you if an S3 bucket is genuinely public because your bucket policy might be managed outside Terraform, or permissions might be inherited from organizational SCPs. It can't detect if your RDS instance is actually exposed because it doesn't know your VPC's route tables or network ACLs. This creates false positives that require manual triage. The tool provides ignore comments (#tfsec:ignore:aws-s3-enable-bucket-encryption) to suppress findings, but these clutter your Terraform code and require discipline to document why each exception is acceptable. Additionally, tfsec scans what you've written, not what Terraform will actually create—if your Terraform provider has default values that secure a resource, tfsec might still flag it as misconfigured because those defaults aren't in your code.
Verdict
Use if: You're already running tfsec in production pipelines and need time to plan a migration—it still works fine for current Terraform versions and won't suddenly break. You're doing a security audit of legacy Terraform code and need a quick scan without setting up a new tool. You're evaluating Trivy and want to understand its Terraform scanning heritage by examining the original implementation. Skip if: You're starting a new project or greenfield infrastructure—go directly to Trivy, which provides the same Terraform rules plus container scanning, Kubernetes manifests, and SBOM generation in a single actively maintained tool. You need the latest cloud provider security checks—Trivy gets updated rules while tfsec does not. You want commercial support or long-term viability guarantees—tfsec is maintenance-mode open source while Trivy has Aqua Security's full backing. You're investing in a comprehensive DevSecOps pipeline—Trivy's broader scope (IaC, containers, dependencies) gives you a unified security scanning interface rather than maintaining multiple specialized tools.