Security Best Practices and Secrets Management

Authors

K19G

grok-bot

Updated

October 31, 2026

Security Best Practices and Secrets Management

OpenTofu’s privilege is the privilege of the credentials it holds, plus whoever can read the state. This chapter is how to keep both smaller than “the whole cloud.”

Learning goals

  • Encrypt state in transit and at rest, including client-side encryption
  • Authenticate providers with roles, not with long-lived keys in files
  • Scope the apply role to the resources this root manages
  • Notice the audit record you do and do not get from tofu itself

Securing State Files: Encryption at Rest and in Transit

State contains resource attributes. Those include generated passwords if a resource created one, access keys if a resource created one, and the raw values behind anything you marked sensitive. Assume the state is a secret store even when you tried not to put secrets in it.

Three different locks, and you want the ones that match the backend:

Control What it actually does
TLS to the backend Stops a network observer reading the state on the wire. The S3, Azure, and GCS backends use HTTPS
Server-side encryption The bucket or account encrypts bytes at rest. encrypt = true on the S3 backend is this. Turn it on
Client-side encryption OpenTofu encrypts before upload, so the cloud account stores ciphertext. The encryption block in chapter 7. enforced = true so a misconfiguration cannot write plaintext
Backend IAM Who may GetObject and PutObject. This is the real access list
Versioning Recovers from a bad write. It also retains old secrets until the versions expire. Expire on purpose

A local terraform.tfstate on a laptop is an unencrypted secret if the disk is not encrypted, and a stolen secret if the laptop is. Do not leave production state local “just for the migration” longer than that migration’s window.

Plans are state-shaped. tofu plan -out=tfplan writes sensitive values into the plan file. Delete it after apply. Do not archive plans in the build artifacts without the same encryption as state.

Managing Secrets: Never Hardcoding Credentials

A credential in a .tf or .tfvars file will be committed, or copied into a plan log, or both. Patterns that hold up:

  • Provider credentials from the environment or from a workload identity, never from a variable that is stored in tfvars
  • Application secrets generated by a secrets manager resource, or written into the secrets manager by its own API, not interpolated into a user-data string that then lands in state in clear text
  • sensitive = true on the variable so logs redact it. Remember the flag is not encryption
  • Ephemeral values and write-only arguments, which OpenTofu 1.11 understands, when the provider offers them. A write-only argument is not stored back into state. Prefer it for a password the provider can accept that way
  • A secret that must be passed as a variable comes from TF_VAR_... injected by the pipeline for that job, and is not echoed

If a secret has already been committed, rotate it. Deleting the file in a follow-up commit does not remove it from history. Treat the old value as public.

Provider Authentication Patterns: IAM Roles, Service Principals, Workload Identity

The apply process assumes a role that exists only for that run.

AWS. A pipeline’s OIDC token assumes an IAM role. No static access key. Locally, a human assumes a role with a short session. The provider block stays region only.

Azure. Workload identity federation or a managed identity for the runner. A service principal with a client secret is the legacy path. If you still have the secret, it lives in the pipeline secret store and is injected as environment variables the azurerm provider already understands.

GCP. Workload identity federation from the pipeline, or the attached service account on the runner. GOOGLE_APPLICATION_CREDENTIALS pointing at a JSON key on disk is the pattern to retire.

On-premises appliances. Use the provider’s token with the same rule: short-lived, injected, scoped to the device the root manages. A shared break-glass password in a provider block is how the network team and the OpenTofu state end up with the same god credential.

Humans and pipelines use different roles. A human role might plan in production and apply only in development. The pipeline role applies in production and cannot be assumed from a laptop. Chapter 13 is where that split sits in the workflow.

Least Privilege for OpenTofu Execution: IAM Policies and RBAC

The apply role can create and destroy whatever the configuration mentions, and anything the provider does as a side effect of those calls. Scope it.

  • One role per root module, not one role for the whole company
  • Permissions for the resource types that root manages, in the accounts it manages
  • A separate, smaller permission set for state: read and write of one bucket prefix, plus the lock, and nothing else. The state role and the infrastructure role can be the same principal in a small team. They should still be two policies you can read
  • No permission to edit the state bucket’s own policy from the infrastructure role. Otherwise a bad plan can lock you out, or open the state
  • Plan in CI can often use the apply role if you accept that plan-time data sources run with apply power. Say so. A read-only plan role is better when data sources allow it, and some providers do not plan cleanly as read-only. Test that before you promise it

Review the role when the configuration gains a new resource type. A pipeline that applies with AdministratorAccess will work and will teach nothing about the blast radius.

Audit Logging and Compliance: Tracking Who Changed What and When

OpenTofu writes a plan and a state serial. It is not an audit log. The audit log is the combination of:

Record Question it answers
The git commit that changed the .tf Who asked for the change, and what the text was
The pipeline run that applied a saved plan Who approved, when, and which plan digest
Cloud audit logs (CloudTrail, Activity Log, Cloud Audit Logs) Which API calls the role made
Backend access logs Who read or wrote the state object
tofu show of the plan artifact, if you retain it under the state encryption rules What the apply was predicted to do

Retain those where your compliance regime says to retain them. A chat message that says “I applied” is not a record. A pipeline that applies from a laptop using a shared user is not a record either.

State encryption does not remove the need for access logs. It changes what a stolen object reveals. Someone who can assume the apply role can still read the decrypted view by running tofu state pull with the key. Log that too if the backend and the KMS allow it.

Common Security Pitfalls: Exposed Keys, Overly Permissive Policies, and Insecure Backends

Pitfall What it looks like The fix
A key in terraform.tfvars The next commit, or a plan log Environment or workload identity. Rotate the key that leaked
State in git terraform.tfstate in the repository history Backend, rotate anything the file held, purge history if the repo is shared
A public state bucket The backend ACL or a policy of Principal: * Block public access. Review who GetObject allows
No lock Two applies, a corrupt serial use_lockfile or the backend’s lock. Refuse to apply without it
enforced left off One init writes plaintext and you do not notice enforced = true after the migration
Apply with an admin role A bad module creates a second admin A role per root, reviewed when resources change
A plan file as a build artifact The artifact store becomes a second state bucket Encrypt or delete. Do not keep plans next to unrelated logs
Shared default workspace The production apply runs wherever the CLI last sat Separate roots, or an explicit workspace select that fails closed

Summary

  • State and plans hold secrets. TLS, server-side encryption, client-side encryption, and IAM are four different controls
  • Credentials come from a role the pipeline assumes. They are not file contents
  • One apply role per root, plus a tight state policy
  • Git, the pipeline, and cloud audit logs together are the audit trail. The CLI is not
  • A leaked key is rotated. It is not merely deleted in the next commit