Keeping Secrets Out of State

Terraform writes secrets to state in plaintext — so encrypt the backend, restrict access, and fetch secrets at runtime.

Here is the uncomfortable truth every senior engineer learns: marking a variable sensitive only hides it from the console. The value still lands in the state file in plaintext. If your state is readable, your secrets are readable.

How to actually protect secrets

  • Encrypt the backendencrypt = true on S3, plus a locked-down bucket policy and no public access.
  • Least-privilege access — only CI and a few humans should read the state bucket.
  • Fetch, don't store — pull secrets from a secrets manager via a data source at apply time instead of passing them as variables.
  • Let the cloud generate them — use random_password or a managed-rotation feature so the secret never touches your laptop.

Example

Example · hcl
# Pull the secret at apply time instead of hardcoding it
data "aws_secretsmanager_secret_version" "db" {
  secret_id = "prod/db/password"
}

resource "aws_db_instance" "main" {
  engine   = "postgres"
  username = "appuser"
  password = data.aws_secretsmanager_secret_version.db.secret_string

  # Note: this value STILL ends up in state — encrypt the backend!
}

When to use it

  • A team discovers their RDS password is stored in plaintext in the state file and immediately moves to fetching it from AWS Secrets Manager at runtime instead.
  • An organization enables S3 server-side encryption and restricts state bucket access to a dedicated CI IAM role so plaintext secrets in state cannot be read by developers.
  • An engineer replaces a hard-coded database password variable with an aws_secretsmanager_secret_version data source so no plaintext ever reaches the Terraform configuration.

More examples

Fetch secret at runtime with data source

Fetches the DB password from Secrets Manager at plan/apply time rather than accepting it as a variable, but warns that state still records the value.

Example · hcl
data "aws_secretsmanager_secret_version" "db_pass" {
  secret_id = "prod/myapp/db_password"
}

resource "aws_db_instance" "app" {
  engine   = "postgres"
  username = "admin"
  password = data.aws_secretsmanager_secret_version.db_pass.secret_string
  # password value is still written to state — encrypt the backend!
}

Restrict state bucket access

Adds a bucket policy that denies state reads to everyone except the dedicated CI IAM role, limiting exposure of plaintext secrets in state.

Example · hcl
resource "aws_s3_bucket_policy" "tfstate" {
  bucket = aws_s3_bucket.tfstate.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Deny"
      Principal = "*"
      Action    = "s3:GetObject"
      Resource  = "${aws_s3_bucket.tfstate.arn}/*"
      Condition = {
        StringNotEquals = {
          "aws:PrincipalArn" = "arn:aws:iam::123456789012:role/TerraformCIRole"
        }
      }
    }]
  })
}

Use random_password, not a variable

Generates a random password inside Terraform and stores it in Secrets Manager so no human ever sees or types the value.

Example · hcl
resource "random_password" "db" {
  length  = 32
  special = true
}

resource "aws_db_instance" "app" {
  password = random_password.db.result
  username = "admin"
  engine   = "mysql"
  # ...
}

resource "aws_secretsmanager_secret_version" "db" {
  secret_id     = aws_secretsmanager_secret.db.id
  secret_string = random_password.db.result
}

Discussion

  • Be the first to comment on this lesson.