All modules in this track (11)
In this module, you'll learn to:
- Explain why
sensitive = truedoesn't keep a secret out of state - Keep database passwords out of Terraform, and pass secret ARNs instead of values
- Replace static access keys with OIDC roles, and harden access to state
Before you start: read State Isolation, Remote Locking & Blast Radius Control. This module builds on its state bucket protections.
The sensitive = true misconception
Terraform provides a sensitive = true attribute for variables and outputs:
variable "db_password" {
type = string
sensitive = true
}
output "connection_string" {
value = "postgresql://admin:${var.db_password}@db.example.com"
sensitive = true
}
The catch
Marking a value sensitive = true only hides it from the terminal and CI logs.
It does not protect it in the state file. Open terraform.tfstate and the password is there in plain JSON. Anyone who can read the state bucket can read every secret Terraform has touched.
So the goal is simple: keep secrets out of Terraform altogether wherever you can.
Let AWS create and keep the password
Never pass production passwords in through terraform.tfvars. For RDS and Aurora, let AWS create the master password and keep it in Secrets Manager for you:
resource "aws_rds_cluster" "primary" {
cluster_identifier = "prod-aurora-cluster"
engine = "aurora-postgresql"
master_username = "dbadmin"
manage_master_user_password = true # RDS creates and rotates it in Secrets Manager
master_user_secret_kms_key_id = aws_kms_key.secrets.arn
# ...
}
Terraform never sees the password, so it never reaches state. Applications read it at runtime from Secrets Manager with their own IAM role (or EKS Pod Identity), and no engineer needs to know it.
random_password resource is better than a typed-in password, but its result is still stored in state. Use it only where the service can't manage its own secret, and protect the state bucket as in Module 02.Pass references, not values
Reading a secret into Terraform puts it in state too. A data "aws_secretsmanager_secret_version" lookup stores the secret's value in the state file, exactly like a variable would. Where the consumer can fetch the secret itself, give it the secret's ARN and let it read the value at runtime:
# AVOID: the password lands in state and in the job's arguments
default_arguments = {
"--db_password" = data.aws_secretsmanager_secret_version.db.secret_string
}
# PREFER: the job reads the secret at runtime with its own IAM role
default_arguments = {
"--db_secret_arn" = aws_secretsmanager_secret.db_credentials.arn
}
The workload's IAM role is then granted secretsmanager:GetSecretValue on that one secret, and nothing secret passes through Terraform at all.
The same rules hold everywhere: no secrets in terraform.tfvars, in variable defaults, or in any .tf file. Any variable that genuinely has to carry a sensitive value is declared sensitive = true, with the caveat above that this only hides it from output.
Eliminate static AWS access keys
A common anti-pattern is creating static aws_iam_access_key resources to give services or CI/CD pipelines access to AWS:
# ANTI-PATTERN: Generating static keys in Terraform
resource "aws_iam_user" "ci" {
name = "github-actions-deployer"
}
resource "aws_iam_access_key" "ci" { # Stored in plaintext state!
user = aws_iam_user.ci.name
}
The alternative: OIDC federation
CI pipelines such as GitHub Actions, GitLab CI, and CircleCI can assume a temporary IAM role through OIDC federation, with no long-lived keys at all:
# Secure OIDC Trust Policy for GitHub Actions
resource "aws_iam_role" "ci_deployer" {
name = "github-actions-terraform-deployer"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = "arn:aws:iam:::oidc-provider/token.actions.githubusercontent.com"
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
# only the main branch of this one repository can assume the role
"token.actions.githubusercontent.com:sub" = "repo:my-org/my-repo:ref:refs/heads/main"
}
}
}
]
})
}
Hardening remote state access
Some sensitive values will always end up in state, so protect the bucket as well (the full checklist is in Module 02):
- A dedicated KMS key: only the CI/CD roles and the read-only plan roles may decrypt state.
- Network limits: allow state reads only through your VPC endpoint or from approved CI runners.
- Access logging: turn on S3 data events in CloudTrail, and alert on unexpected
GetObjectcalls to*.tfstate.
Key terms
sensitive = true: hides a value in CLI and CI output only. It's still plain text in state.manage_master_user_password: RDS or Aurora creates the master password and keeps it in Secrets Manager. Terraform never sees it.random_password: better than a typed-in password, but its result is still stored in state.- Secret reference: a secret's ARN, passed instead of its value. The workload reads the value at runtime with its own IAM role.
- OIDC federation: a pipeline assumes a short-lived IAM role, limited by audience and subject, instead of using access keys.
Pop quiz
Five questions: three on the ideas in this module, and two scenarios where you apply them. The order changes every time you take it, and 4 out of 5 passes.
5 questions. Decide on your answer to each, then check it.
-
Concept
A variable is marked
sensitive = true. Where can its value still be read?Show answer
In the state file, in plain text
sensitiveonly hides the value from terminal and CI output. Anyone who can read the state can read it. -
Concept
How does
manage_master_user_password = truekeep the database password out of state?Show answer
RDS creates the password and keeps it in Secrets Manager, so Terraform never handles it
A value Terraform never receives can't end up in state. Applications read it from Secrets Manager at runtime.
-
Concept
In the GitHub Actions OIDC trust policy, what does the
subcondition limit?Show answer
Which repository and branch may assume the role
repo:my-org/my-repo:ref:refs/heads/mainmeans only the main branch of that one repository can assume the role. -
Scenario
A Glue job gets its database password from
data "aws_secretsmanager_secret_version", passed indefault_arguments. What's wrong, and what's the fix?Show answer
The password lands in state; pass the secret's ARN and let the job's role read it at runtime
Reading a secret into Terraform stores its value in state. Pass the ARN and grant the job's role
secretsmanager:GetSecretValueon that one secret. -
Scenario
A pipeline deploys with an IAM user whose
aws_iam_access_keyis created in Terraform. What do you replace it with?Show answer
An IAM role the pipeline assumes through OIDC, with no long-lived keys
The access key sits in state in plain text and never expires. OIDC gives the pipeline short-lived credentials instead.