Researchers scanning public S3 buckets regularly find Terraform state files with live AWS IAM keys, Azure Service Principal secrets, and GCP service account keys. The scan requires no credentials of its own and leaves no CloudTrail events. Passive discovery is the dominant exposure vector.
terraform.tfstate is the highest-density credential artifact in most cloud environments. Every database password, IAM key, and TLS private key managed by Terraform exists in plaintext in that file. Marking outputs as sensitive = true does not change this fact, and the most common exposure vectors do not require obvious misconfiguration.
The tfstate Is Not a Config File: It's an Attribute Database
Terraform stores every managed resource attribute at the moment of the last apply. Without the full attribute values, Terraform cannot compute diffs between current state and future plans. Encrypting individual values in the backend would break the plan/apply mechanism.
This limitation has been documented since 2014 in issue #516 of the official Terraform GitHub repository. HashiCorp engineers confirmed at the time that the choice is architectural, not an oversight. Plaintext in tfstate is a design requirement, not a flaw scheduled for correction.
The format is JSON with a resources[] array. Each entry contains instances[].attributes with every attribute the provider returned after Create or Update. There is no type distinction between sensitive and non-sensitive attributes inside the JSON.
The aws_iam_access_key resource stores id (the key ID) and secret (the secret access key) in plaintext. aws_db_instance stores the password field without any encryption in state. tls_private_key writes private_key_pem with the full private key in PEM format. No provider-level option encrypts individual attribute values inside the state file.
sensitive=true Is a Display Filter, Not a Security Control
The most cited control in tfstate security discussions is marking outputs as sensitive = true. That flag exclusively affects terminal output during plan and apply, and the display in the Terraform Cloud UI. It does not affect what is written to the state file.
HashiCorp's own documentation is direct: sensitive values are stored in both state and plan files. Any user with access to those files can read them in plaintext. The command terraform output -json confirms this on every state file, regardless of the flag.
Terraform Cloud redacts sensitive outputs in the workspace web interface. The underlying state JSON remains complete. API access to state returns the full JSON without any redaction of values.
Terraform 1.10 introduced ephemeral values with the write_only attribute, which genuinely omit values from state after apply. Only resources with explicit write_only support benefit from this. aws_iam_access_key does not support write_only.
Three Remote Backend Configurations That Become Credential Stores
The S3 backend with encrypt = true configures at-rest encryption via SSE-S3 or KMS. At-rest encryption does not fix access control failures. A bucket with permissive policies or disabled block_public_access exposes state that is encrypted on disk but fully accessible via API.
Terraform Cloud supports public workspaces. A public workspace exposes state via GET /api/v2/workspaces/:id/current-state-version-outputs with no authentication. The URL is predictable given the organization name and workspace name, both of which are publicly listed for public workspaces.
The Consul backend without ACLs responds to GET /v1/kv/terraform/<workspace> with the state base64-encoded. Shodan indexes Consul instances with exposed UIs; the query http.title:"Consul" port:8500 returns direct results, and /v1/kv/?keys lists all available paths in the KV store.
The generic HTTP backend, which Terraform supports for custom state storage integrations, frequently runs without authentication in internal environments. A GET request to the configured endpoint URL returns the full state JSON with no audit log entry beyond what the server itself captures.
Files *.tfstate committed to git and later added to .gitignore remain in the full repository history. The command git log --all --full-history -- '*.tfstate' retrieves them from any clone, including future forks.
How Attackers Find State Files Without Triggering Alerts
Discovery by bucket name enumeration requires no credentials and generates no GuardDuty findings by default. S3 produces no anomaly detection on unauthenticated enumeration against public buckets.
The process starts with scraping crt.sh for subdomains of the target organization. Extracted base names feed permutation generation: {name}-terraform-state, {name}-tfstate, terraform-{name}-prod. HEAD requests at 10 req/s identify publicly accessible buckets without authentication.
Buckets matching these naming patterns turn up within hours using this methodology. S3 access logs capture the request only if logging is configured on the target bucket. Most misconfigured public backends also had logging disabled.
The GitHub dork filename:terraform.tfstate finds files committed directly to public repositories. Refining with aws_access_key_id within results narrows the set to files with confirmed credentials.
The MAGO Intel tool (intel.mago.team) scans for publicly accessible S3 buckets and Terraform Cloud workspaces that expose state files, extracting credential patterns from tfstate JSON.
What an Attacker Extracts From a Single State File
A state file from a mid-size deployment contains credentials across all 3 tiers: cloud provider, database, and third-party services. Compromising a single tfstate file typically equals compromising the entire environment.
IAM keys from CI/CD service accounts appear in aws_iam_access_key.id and aws_iam_access_key.secret. CI/CD service accounts frequently have the AdministratorAccess policy attached, because it is the simplest choice to ensure the deployment pipeline works.
aws_db_instance stores endpoint and password together. These credentials are frequently copied as application environment variables directly from Terraform output. The tfstate becomes the single point of compromise for the entire data tier.
Third-party providers like GitHub, Datadog, and PagerDuty write tokens to state when resources are managed by Terraform. The network map is also in state: VPC CIDRs, security group IDs, and full ARNs provide the complete topology for lateral movement planning. The combination of administrator-level IAM keys and a full network map reduces initial reconnaissance to zero.
Defenses That Actually Reduce Exposure
Three independent controls cover distinct threats. Access control covers most exposure vectors. Encryption with a CMK covers storage-layer compromise. Architectural changes keep secrets out of state.
For the S3 backend, all 4 public block flags must be active at the bucket level: block_public_acls, block_public_policy, ignore_public_acls, restrict_public_buckets. An IAM role scoped to the specific state bucket and prefix prevents lateral access between workspaces. A KMS CMK with a key policy restricting Decrypt to the CI/CD role protects against storage-layer compromise. Enabling bucket versioning preserves pre-breach state for forensic comparison during incident response.
The architectural change with the largest impact is keeping secrets out of state. Creating aws_iam_access_key or aws_db_instance resources guarantees that credentials appear in tfstate. Referencing the same data from AWS Secrets Manager via data source eliminates the problem at the source. The HashiCorp Vault provider exposes secrets as data sources at plan time without persisting them to state. This is the definitive fix for credentials that must never appear in tfstate.
In Terraform Cloud, workspaces set to Private and OIDC dynamic credentials eliminate the risk of static IAM keys. OIDC credentials expire after each run. Compromising the tfstate does not expose valid credentials when the environment uses OIDC.
For git, *.tfstate and *.tfstate.backup must be in .gitignore. Verify current state with git ls-files | grep tfstate in every repository that has ever run terraform apply.
Run find . -name '*.tfstate' | xargs grep -l 'secret\|password\|private_key' across every repository before any other audit. The state file is not a side effect of your infrastructure. It is the credential ledger of your infrastructure.
Top comments (0)