Skip to main content
In this lesson we shift focus from protecting secrets in your configuration (sensitive variables, environment variables, and external secret stores) to the place where those secrets ultimately reside: the Terraform state file. Terraform state contains the complete configuration for every resource it manages, including attributes required to create and manage infrastructure — and sensitive values such as database passwords, API keys, private keys, and TLS certificates. Because state is plain JSON by default, any user who can read the file can access those secrets.
Terraform state files often include secrets. Treat state as a sensitive artifact: secure where it’s stored, who can access it, and how it’s audited.
Example state snippet (JSON) showing sensitive values stored in plain text:
{
  "version": 4,
  "terraform_version": "1.5.0",
  "serial": 1,
  "lineage": "abcd1234-ef56-7890-abcd-ef1234567890",
  "outputs": {
    "database_password_output": {
      "value": "MySuperSecretPassword123!",
      "type": "string",
      "sensitive": true
    }
  },
  "resources": [
    {
      "mode": "managed",
      "type": "aws_db_instance",
      "name": "main",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
            "password": "MySuperSecretPassword123!"
          }
        }
      ]
    }
  ]
}
Do not commit Terraform state files to version control. They commonly contain secrets and other sensitive infrastructure data.
Why local state is risky
  • Local state is stored on a developer workstation unencrypted unless you encrypt your disk.
  • It’s easy to accidentally commit terraform.tfstate to Git.
  • Sharing local state via email or chat is insecure.
  • Local state provides no built-in access control, role-based permissions, or audit logging.
Why remote state is safer
  • Centralized storage prevents long-term state residency on developer machines.
  • Backends commonly support encryption at rest (SSE-S3, SSE-KMS, Azure Storage encryption, GCS encryption).
  • Access is managed via IAM/RBAC, allowing fine-grained permissions for read/write/list operations.
  • Audit logging (e.g., CloudTrail) records who accessed state and when.
  • State locking prevents concurrent modifications that could corrupt state.
Comparison: Local vs Remote State
FeatureLocal StateRemote State (recommended)
Encryption at restDepends on disk encryptionTypically supported (SSE-S3, SSE-KMS, provider-managed)
Access controlFile system permissions onlyIAM / RBAC controls via backend
Audit loggingNoneProvider logs (e.g., CloudTrail, audit records)
State lockingNot availableSupported by many backends (DynamoDB, etc.)
Team collaborationRisky / manual sharingCentralized, safe collaboration
For production workloads, always use a remote backend with encryption and locking enabled. Local state is acceptable only for learning or single-developer experiments. How to configure encrypted remote state (example: Amazon S3) Below is an S3 backend configuration that stores state in an S3 bucket and uses DynamoDB for locking. Put this in a Terraform configuration file (e.g., backend.tf).
terraform {
  backend "s3" {
    bucket         = "terraform-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"         # state locking table
    encrypt        = true                      # SSE-S3 (AES-256) server-side encryption
    kms_key_id     = "arn:aws:kms:us-east-1:123456789012:key/abcd-efgh-ijkl" # SSE-KMS (recommended)
  }
}
Notes on encryption and locking:
  • encrypt = true enables S3 server-side encryption with AES-256 (SSE-S3).
  • Specifying a kms_key_id causes S3 to use AWS KMS (SSE-KMS). SSE-KMS is recommended because it provides additional access controls and audit trails for decryption.
  • Use dynamodb_table to enable state locking, which prevents concurrent terraform apply runs from corrupting state.
  • In production prefer SSE-KMS over SSE-S3 for better access control and auditing.
Migrating existing local state to a remote backend You can switch to a remote backend at any time. If a backend block is added to a project that already has local state, running terraform init will prompt you to migrate the existing local state to the remote backend. This makes migration safe and non-disruptive. Layered security for state files Apply defense-in-depth when protecting state. Typical layers include:
  1. Encryption at rest (SSE-S3 or SSE-KMS).
  2. State locking to prevent concurrent changes.
  3. Fine-grained IAM/RBAC policies to restrict access.
  4. Audit logging (CloudTrail or provider logs) to track access and changes.
Each layer complements the others: if one control is misconfigured, additional layers still protect your secrets. State file security checklist
RequirementWhy it mattersExample / Action
Remote state for productionCentralized control and safer collaborationUse S3, Azure Storage, or GCS backend
Encryption at restPrevents plaintext secrets on storageEnable SSE-KMS or SSE-S3 (encrypt = true)
Fine-grained access controlLimits who can read/write stateUse IAM roles, policies, or RBAC
State lockingPrevents concurrent writes and corruptionConfigure DynamoDB (dynamodb_table) or backend locking
Audit loggingTrace access and detect misuseEnable CloudTrail, provider audit logs
The image displays a "State File Security Checklist" with four items: remote state use for production, encryption at rest, access controls, and state locking, all checked off. A note emphasizes the importance of protecting state files containing secrets.
Conclusion Terraform state files will contain secrets — this is unavoidable. Your responsibility is to secure the storage and access to state. Follow the checklist: use a remote backend, enable encryption (prefer KMS), implement access controls, enable state locking, and keep audit logging enabled. Provider-specific features (for example, write-only provider arguments) can add an extra layer of protection. Links and references

Watch Video