Terraform state files hold the real values of database passwords, generated keys and API tokens, and marking a variable sensitive does not keep any of them out. HashiCorp's documentation is explicit that sensitive values are stored in both state and plan files, and that terraform show -json prints them in plain text. Most teams also have more copies of state than they think: old object versions in the backend bucket, local terraform.tfstate files committed to Git, saved plan files in CI artefacts. This runbook shows how to find every copy, list which attributes hold secrets, scan for credentials that are still live, and then rotate and clean up in an order that does not break deployments. It is for platform, cloud security and DevOps engineers. For how to secure the backend and pipeline credentials going forward, see the Terraform and IaC Pipeline Credential Security Guide.
This is an independent guide from NHI Mgmt Group, based on HashiCorp and AWS documentation read on 11 November 2026. Commands are read-only until the remediation steps.
Key takeaways
sensitiveis display masking, not protection. It redacts CLI and UI output; the value is still in state, andterraform output -jsonor-rawshows it.- Find the copies before the contents. Backend objects, prior versions, workspace paths,
.tfstate.backupfiles, Git history and plan artefacts each need checking. - Use the JSON format to list what is sensitive. Each resource in
terraform show -jsoncarries asensitive_valuesobject marking secret attributes withtrue, which gives an inventory without printing values. - Read access to state is credential access. Anyone who can
s3:GetObjectthe state key, download a state version, or useterraform_remote_statecan read every secret in it. - Rotation comes before deletion. Old state versions and Git history keep the old value, so a secret found in state is treated as exposed and rotated, then the copies are cleaned up.
When you need this
- Access review: you are reviewing who can read state buckets or HCP Terraform workspaces and need to know what that access really exposes.
- Leak or misconfiguration: a state bucket was public, a CI log printed state, or a repository with committed state was made public.
- Migration: before adopting ephemeral resources and write-only arguments, you need the list of attributes to move.
- Audit evidence: an assessor asks where infrastructure secrets are stored and who can read them. NIST SP 800-53 SC-28 (Protection of Information at Rest) and IA-5(7) are the controls usually in play.
Leaks from infrastructure tooling follow the same pattern as other leaked secrets: the credential is found by someone else first. The Sisense Breach is reported to have started with a credential in a code repository that led to storage holding customer tokens.
Before you start
| Need | Detail |
|---|---|
| Read access to state | S3 backend: s3:ListBucket on the bucket and s3:GetObject on state keys (plus kms:Decrypt if kms_key_id is set). HCP Terraform: read access to state versions for each workspace |
| A safe workstation | Output of these steps contains live secrets. Work on a hardened host or isolated runner, write output to an encrypted location, and delete it when done |
| Tools | Terraform CLI matching the configurations, jq, and a secret scanner that can scan files (for example gitleaks dir or TruffleHog filesystem) |
| Approval | A ticket naming the scope. Reading every state file is itself a privileged action and should be logged as such |
| Owners | A list of workspace or stack owners, because rotation will need them |
Where state and its copies live
| Location | How to find it | What it can contain |
|---|---|---|
| S3 backend, default workspace | The bucket and key in each backend "s3" block | Current state for that configuration |
| S3 backend, other workspaces | Objects under workspace_key_prefix (default env:) as env:/<workspace>/<key> | One state per workspace, often forgotten test or feature workspaces |
| Prior object versions | Bucket versioning, which HashiCorp strongly recommends for recovery | Every earlier state, including secrets that were later rotated or removed |
| HCP Terraform or Terraform Enterprise | GET /state-versions filtered by workspace and organisation; GET /workspaces/:workspace_id/current-state-version | Current and historical state versions per workspace |
| Local state | terraform.tfstate and terraform.tfstate.backup on laptops, jump hosts and runners | Plaintext state, often from before a backend was configured |
| Git history | Search repositories and history for *.tfstate and *.tfstate.backup | State committed by mistake, still in history after a later delete |
| Saved plans and CI artefacts | Pipeline artefacts and caches holding terraform plan -out files or JSON plan output | Plan files carry sensitive values just as state does |
| Remote state consumers | Search configurations for data "terraform_remote_state" | Not a copy, but a read path to the entire snapshot for every consumer |
Step-by-step
Phase 1: inventory the copies (read-only)
- List backends from code: search all infrastructure repositories for
backend "s3",backend "gcs",backend "azurerm",cloud {andbackend "remote"blocks and record bucket, key, prefix and workspace or organisation names. - List objects in each state bucket, including all keys under the workspace prefix and, where versioning is on, all object versions. Count versions per key: a key with hundreds of versions carries hundreds of historical secrets.
- List HCP Terraform state versions per workspace with
GET /state-versions?filter[workspace][name]=<ws>&filter[organization][name]=<org>. Each version returnshosted-state-download-urland, for state written by Terraform 1.3 or later,hosted-json-state-download-url. - Search repositories and history for committed state and plan files. A scanner in Git mode (for example
gitleaks gitwith--log-opts="--all") reads all branches and history, not just the working tree. - Search CI systems for retained artefacts named
*.tfplan,plan.outortfplan.json, and for job logs that ranterraform show -jsonorterraform output -json. - List remote state consumers: every
terraform_remote_statedata source and the identity it runs as. HashiCorp warns that this data source needs access to the entire state snapshot and should not be used where resources handle sensitive data.
Phase 2: list which attributes are sensitive (without printing values)
- Pull state for each configuration with
terraform state pull(raw format, written to stdout) or download the JSON state version from HCP Terraform, saving to the encrypted working area. - Convert to the documented JSON format with
terraform show -json <state file>. Treat the output as secret: HashiCorp's documentation states that sensitive values are displayed in plain text with-json. - Walk the module tree:
values.root_module.resources[]and, recursively,child_modules[].resources[]. For each resource recordaddress,typeand the keys insidesensitive_valuesthat aretrue. Ajqfilter such as.values.root_module.resources[] | select(.sensitive_values != {}) | {address, type, sensitive: .sensitive_values}lists these without touchingvalues; repeat for child modules. - Check outputs:
values.outputsmaps each root output tovalue,typeandsensitive. Any output withsensitive: trueis readable by anyone who can read outputs. - Do not trust the flag alone. Providers decide which attributes are marked sensitive. Attributes such as user data, connection strings, environment variable maps and templated configuration files often contain secrets without being flagged.
Phase 3: scan for credentials, including unflagged ones
- Scan the pulled files with a file scanner, for example
gitleaks dir <path> --redact --report-format json --report-path <report>ortrufflehog filesystem <path>. Use redaction so the report does not become another copy of every secret. - Scan the bucket directly if pulling is impractical: TruffleHog documents an
s3source (trufflehog s3 --bucket=<bucket> --results=verified,unknown) and agcssource. - Decide on verification deliberately. TruffleHog's "verified" status means it tested the credential against the provider's API. That confirms exposure quickly but is a live authentication from your scanning host; agree it with the owners and expect it to appear in the provider's logs.
- Merge the results into one table per secret: location(s), resource address, secret type, owner, whether live, and which identities can read the location.
Phase 4: remediate
- Rotate first, starting with live credentials in locations with broad read access. For database and service passwords set by Terraform, use the provider's write-only argument (names usually end in
_wo, with a_wo_versionto trigger change) where available, so the new value never enters state. See Credential Rotation at Scale. - Move secrets out of state: read secrets at run time through ephemeral resources (Terraform 1.10 or later) or have the service generate and store its own secret in a secrets manager. Ephemeral values cannot be root module outputs, so redesign consumers to read from the secrets manager instead.
- Replace
terraform_remote_statewhere state is sensitive, withtfe_outputson HCP Terraform or with values published to a parameter store that has its own access control. - Clean up copies only after rotation: remove committed state from Git history, expire old object versions with a lifecycle rule that keeps enough history for recovery, delete stale local and CI copies. Keep at least the most recent good versions so you can roll back state.
- Restrict read access to state keys to the pipeline identity and a break-glass role, and enable logging of object reads (see below).
Choosing how far to go
| Option | Effort | Effect on state | Choose when |
|---|---|---|---|
| Restrict and log read access only | Low | Secrets remain; fewer people can read them | Small estates, or as the first week's action |
| Rotate exposed values, keep design | Medium | New values still land in state | After a confirmed exposure, when providers lack write-only support |
| Write-only arguments and ephemeral resources | Medium to high; needs Terraform 1.10 or 1.11 and provider support | Values never written to state or plan | Database passwords, generated credentials, provider secrets |
| Secret generated and held outside Terraform | High | State holds only a reference | Shared or high-value secrets, regulated environments |
Small teams with one state bucket can finish all four phases in a day. Large estates with hundreds of workspaces should automate Phases 1 to 3 per workspace, prioritise production and shared-services state, and track remediation as a backlog with owners.
Verify it worked
- Re-run the Phase 2 inventory and confirm rotated attributes now hold new values, or are gone where write-only arguments or ephemeral resources were adopted.
- Re-run the scanner on current state and confirm no verified findings remain.
- Confirm old values fail authentication at the provider, which proves rotation rather than just a state change.
- Run
terraform planfor each changed configuration and confirm no unexpected changes.
Detection: enable S3 server access logging on state buckets (delivery is best effort and can take hours) or object-level CloudTrail data events for GetObject, and alert on any reader other than the pipeline role and break-glass role. In HCP Terraform, review who holds state read permission per workspace. Alert on new *.tfstate files appearing in repositories.
Metrics:
- Workspaces scanned: share of workspaces and state buckets covered by the inventory.
- Live secrets in state: count of verified credentials in current state, trending to zero for production.
- Sensitive attributes moved: share of flagged attributes migrated to write-only or ephemeral patterns.
- State readers: number of identities able to read each production state; target two or fewer.
- Stray copies: state or plan files found in Git or CI artefacts per quarter.
Rollback and common errors
- State corrupted during clean-up: restore the last good object version (or state version in HCP Terraform). This is why lifecycle rules should never delete the most recent versions.
- Rotation broke a dependent service: the consumer read the secret from state or an output. Roll forward by pointing the consumer at the secrets manager rather than restoring the old value.
terraform show -jsonerrors on schema: providers newer than the state were installed. Use the Terraform and provider versions pinned by that configuration.- Write-only change not applied: changing a write-only value requires incrementing its
_wo_version. - Scanner reports flood: base64 and JSON-encoded blobs in user data trigger generic rules. Triage by resource type and suppress by fingerprint, not by turning rules off.
Common failure patterns
- Relying on
sensitive = trueand assuming state is clean, then granting broad state read to developers. - Cleaning current state but not versions: bucket versioning keeps every earlier secret until lifecycle rules expire it.
- Forgotten workspaces: feature and test workspaces under
env:/hold real credentials long after the environment is gone. - Remote state as an integration API: dozens of configurations read a shared networking state that also holds database passwords.
- Scanner output as a new leak: unredacted reports stored in CI artefacts or tickets. The same applies to CI job logs that ran
terraform output -json.
Framework mapping
| Framework | Reference | Relevance |
|---|---|---|
| NIST SP 800-53 Rev 5 | SC-28 Protection of Information at Rest; IA-5(7) No Embedded Unencrypted Static Authenticators; AU-6 Audit Record Review | Secrets at rest in state; review of state reads |
| NIST CSF 2.0 | PR.DS-01; PR.AA-01 | Data-at-rest protection; managed service credentials |
| CIS Controls v8.1 | 3.11 Encrypt Sensitive Data at Rest | Backend encryption and access to state |
| ISO/IEC 27001:2022 Annex A | A.5.17 Authentication information; A.8.24 Use of cryptography | Handling of secrets and their encryption |
| MITRE ATT&CK | T1552.001 Credentials In Files; T1530 Data from Cloud Storage | State files in buckets are a known credential source |
Where identity fits
Almost every secret found in state belongs to a non-human identity: a database account, a cloud access key, a service principal secret, an API token. Record each finding against its identity owner, and prefer designs where the pipeline itself authenticates with short-lived federated credentials so there is no long-lived provider key to land in state. See the Terraform Cloud and HCP Security Guide and the OWASP NHI Top 10 NHI2 Secret Leakage Guide.
Practitioner checklist
- Inventory every backend, workspace, object version, local copy, Git copy and plan artefact.
- Work on an isolated host and redact scanner output.
- List sensitive attributes from
sensitive_valuesand sensitive outputs per configuration. - Scan state for unflagged secrets in user data, maps and templates.
- Rotate live findings before deleting any copy.
- Adopt write-only arguments and ephemeral resources where providers support them.
- Replace
terraform_remote_statefor sensitive state. - Limit state read to the pipeline role and break-glass, and log every read.
- Expire old object versions with a lifecycle rule that keeps recent versions for recovery.
Standards and references
- HashiCorp: Sensitive data in state
- HashiCorp: JSON output format
- HashiCorp: terraform show command
- HashiCorp: HCP Terraform state versions API
- HashiCorp: S3 backend
- HashiCorp: The terraform_remote_state data source
- AWS: Enabling Amazon S3 server access logging
- Gitleaks project documentation
Related NHI Mgmt Group resources: Terraform and IaC Pipeline Credential Security Guide · Terraform Cloud and HCP Security Guide · Atlantis Security · HashiCorp credentials · Leaked Credential Response Playbook