How to Fix Terraform 'Invalid Type Specification: type = string but list passed' Error
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 5 mins
TL;DR
- What broke: A Terraform variable is declared
type = stringbut the caller (.tfvars, module input, or-varflag) is passing a list literal — Terraform's type checker hard-fails atplantime, nothing deploys. - How to fix it: Change the variable's
typeconstraint tolist(string)(or the correct complex type), update thedefaultvalue to a list, and add avalidationblock to enforce element constraints. - Fast path: Use our Client-Side Sandbox below to auto-refactor this — paste your broken variable block and the tool rewrites the type constraint and default in-browser without sending your config anywhere.
The Incident (What Does the Error Mean?)
Terraform's type system is evaluated at parse time, before any provider API call is made. When the runtime value does not satisfy the declared constraint, Terraform emits:
Error: Invalid value for input variable
on terraform.tfvars line 4:
4: allowed_cidrs = ["10.0.0.0/8", "192.168.1.0/24"]
The given value is not suitable for var.allowed_cidrs declared at
variables.tf:12,1-32: string required, but a list was provided.
Immediate consequence: terraform plan exits with code 1. No diff is computed. Every downstream resource, data, and module block that references this variable is dead. In a CI/CD pipeline this means the entire apply stage is gated and the deployment is blocked.
The Attack Vector / Blast Radius
This is a type contract violation — the variable declaration and the caller are out of sync. The blast radius depends on where the mismatch lives:
- Root module
.tfvarsmismatch: Entire root module fails to plan. All resources in that workspace are undeployable until fixed. - Child module input mismatch: The parent module passes a
listto a child module variable typedstring. Every resource inside the child module is blocked. If the child module manages a security group, VPC, or IAM policy — that infrastructure is frozen in its last-applied state, which may be stale or insecure. -varflag at CLI/CI level: An engineer passes--var 'subnets=["subnet-abc","subnet-def"]'against atype = stringvariable. The pipeline fails silently in some wrappers that swallow exit codes, meaning the deploy appears to succeed but no infrastructure was actually changed — a silent drift scenario.- Cascading
for_eachfailure: If the string variable was being used in afor_each = toset(var.something)expression, the type mismatch prevents the resource graph from being constructed, wiping out the entire resource set from the plan.
How to Fix It (The Solution)
Basic Fix — Correct the Type Constraint
The caller is right; the declaration is wrong. Fix the variable block:
variable "allowed_cidrs" {
- type = string
- default = "10.0.0.0/8"
+ type = list(string)
+ default = ["10.0.0.0/8"]
description = "List of allowed CIDR blocks for ingress rules."
}
And in terraform.tfvars, the caller syntax was already correct — no change needed there:
allowed_cidrs = ["10.0.0.0/8", "192.168.1.0/24"]
Enterprise Best Practice — Add a Validation Block and Use toset() for Deduplication
Never trust that callers will pass well-formed CIDRs. Lock it down:
variable "allowed_cidrs" {
- type = string
- default = "10.0.0.0/8"
+ type = list(string)
+ default = ["10.0.0.0/8"]
+
+ validation {
+ condition = alltrue([
+ for cidr in var.allowed_cidrs :
+ can(cidrnetmask(cidr))
+ ])
+ error_message = "All entries in allowed_cidrs must be valid CIDR notation (e.g., 10.0.0.0/8)."
+ }
+
description = "List of allowed CIDR blocks for ingress rules."
}
In the resource block, use toset() to eliminate duplicate entries before iterating:
resource "aws_security_group_rule" "allow_ingress" {
- for_each = var.allowed_cidrs
+ for_each = toset(var.allowed_cidrs)
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [each.value]
...
}
If you genuinely need a single string and the caller is wrong, coerce the list at the call site:
module "network" {
source = "./modules/network"
- primary_cidr = ["10.0.0.0/8"]
+ primary_cidr = "10.0.0.0/8"
}
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
This class of error should never reach a human in a pull request review. Automate it out:
1. terraform validate as a Pre-Commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.92.0
hooks:
- id: terraform_validate
- id: terraform_tflint
terraform validate catches type constraint violations without needing provider credentials — it's a pure static analysis pass. There is no excuse for not running this in every PR pipeline.
2. TFLint Rule — terraform_typed_variables
Add to .tflint.hcl:
rule "terraform_typed_variables" {
enabled = true
}
This rule flags any variable missing an explicit type constraint, forcing engineers to be explicit before the mismatch can even occur.
3. Checkov Policy — Enforce Variable Type Declarations
checkov -d . --check CKV_TF_1 --framework terraform
For custom enforcement, write an OPA policy in your Atlantis or Spacelift workflow:
# policy/variable_types.rego
package terraform.variables
deny[msg] {
variable := input.variables[name]
not variable.type
msg := sprintf("Variable '%v' must have an explicit type constraint.", [name])
}
4. GitHub Actions Gate
# .github/workflows/terraform-validate.yml
name: Terraform Validate
on: [pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "~1.9"
- run: terraform init -backend=false
- run: terraform validate
- run: tflint --recursive
Block merges on this job. A failed terraform validate in CI is a 30-second fix caught before it becomes a 3 AM production incident.