Откуда секреты в state
Каждый apply пишет в state атрибуты ресурсов, включая computed-значения от провайдера. Из реальной жизни:
aws_db_instance.main.password, если ты задал пароль, он в state.aws_iam_access_key.ci.secret, secret key, в state в открытом виде.random_password.db.result, сгенерированный пароль, в state открыто.aws_secretsmanager_secret_version.x.secret_stringесли читаешь data, оно тоже попадает в state.
State'ом владеет backend (локальный файл или S3). Любой с доступом к файлу видит секрет. «Sensitive=true» меняет только то, как Terraform показывает значение в CLI/logs, в state оно открытое.
Это значит: защита секретов = защита state.
Шифрование backend'а
Минимум для prod:
- S3 + SSE-KMS. State лежит в S3 bucket, KMS-key шифрует на уровне
объектов. Без права
kms:Decryptна ключе ты state не прочитаешь, даже если у тебяs3:GetObject. - Versioning + MFA delete. Никаких «случайно сломал state и некуда откатить».
- Bucket policy: запретить публичный доступ, разрешить только конкретным IAM-ролям/пользователям.
resource "aws_s3_bucket" "tf_state" {bucket = "company-tf-state"
}
resource "aws_kms_key" "tf_state" {description = "tfstate encryption key"
deletion_window_in_days = 30
enable_key_rotation = true
}
resource "aws_s3_bucket_server_side_encryption_configuration" "tf_state" {bucket = aws_s3_bucket.tf_state.id
rule { apply_server_side_encryption_by_default {kms_master_key_id = aws_kms_key.tf_state.arn
sse_algorithm = "aws:kms"
}
}
}
См. tf-remote-backend-s3 про полный backend-setup.
Реальные секреты, не в HCL
Антипаттерн:
variable "db_password" {default = "supersecret" # пароль в коде = пароль в git
}
Антипаттерн-2:
TF_VAR_db_password="supersecret" terraform apply
...если history shell записывает env-vars или CI-логи показывают переменные.
Канонический паттерн, внешний secret manager:
AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_master" {secret_id = "prod/db/master"
}
resource "aws_db_instance" "main" {# ...
password = data.aws_secretsmanager_secret_version.db_master.secret_string
}
Сам секрет создан вне Terraform, кем-то в AWS Console или через отдельный flow. Terraform его читает, не создаёт. В state попадёт текущее значение секрета (это снова открытое значение в state-файле, поэтому backend должен быть зашифрован).
HashiCorp Vault
provider "vault" {address = "https://vault.internal:8200"
}
data "vault_kv_secret_v2" "db" {mount = "kv"
name = "db/master"
}
resource "aws_db_instance" "main" {password = data.vault_kv_secret_v2.db.data["password"]
}
Authentication к Vault, отдельная тема: JWT/OIDC, K8s SA, AppRole. Terraform-runner должен иметь способ ауткать в Vault.
Generate-and-store
Альтернатива, Terraform создаёт секрет и кладёт его в менеджер:
resource "random_password" "db" {length = 32
special = true
}
resource "aws_secretsmanager_secret" "db" {name = "prod/db/master"
}
resource "aws_secretsmanager_secret_version" "db" {secret_id = aws_secretsmanager_secret.db.id
secret_string = random_password.db.result
}
resource "aws_db_instance" "main" {password = random_password.db.result
}
Минус: random_password.db.result в state. Плюс: всё declarative, ротация
через apply -replace=random_password.db. Решение зависит от threat-model
команды.
OIDC: больше никаких access-keys у Terraform-runner
Самый частый «секрет», это credentials для самого Terraform: AWS_ACCESS_KEY, AWS_SECRET_ACCESS_KEY. Их кладут в CI как secrets, ротируют редко, в результате они утекают через логи и build-кэши.
Современное решение, OIDC: CI-runner получает временный AWS-token по federated trust, без долгоживущего ключа.
Сценарий на GitHub Actions:
-
В AWS создаёшь IAM-роль, доверенную к OIDC-провайдеру GitHub.
-
Trust policy разрешает только указанному repo/branch:
json{"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com" },"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {"StringLike": {"token.actions.githubusercontent.com:sub": "repo:linuxlab/terraform-infra:ref:refs/heads/main"
}
}
}
-
В workflow:
yamlpermissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCOUNT:role/github-tf-runner
aws-region: us-east-1
Runner получает temp-credentials, действуют 1 час, никаких access-keys в Secrets. См. tf-oidc-aws про полную настройку.
Что ещё попадает в state как «секрет»
Не всегда очевидное:
user_dataуaws_instance. Если внутри хардкод-пароля, он в state. Bootstrapping с секретами делается через cloud-init + secret store (instance читает Secrets Manager на старте).environment.variablesуaws_lambda_function. Любая env-переменная в state. Для секретов, Lambda читает Secrets Manager.aws_ssm_parameterтипаSecureString. Значение в state, зашифровано только в SSM. Decrypt в HCL = открыто в state.tls_private_key.this.private_key_pem. Сгенерированный TF private-key лежит в state открыто. Альтернатива, пре-сгенерировать черезssh-keygen, загрузить как public-key only.
Ротация секретов
- Внешние секреты в менеджере. Ротация делается менеджером, Terraform на следующем apply подхватывает новое значение (потому что data-source refresh'ится).
- Generated в Terraform. Ротация =
terraform apply -replace=random_password.X. Это переcоздаст также все ресурсы, которые от него зависят (база с новым паролем, через downtime). - State backup ВСЕГДА перед ротацией. Если что-то пошло не так, откат.
Подводные камни
-
«sensitive=true» ≠ encryption. Это redaction для CLI. См. tf-sensitive.
-
.tfstate.backupтоже содержит секреты. Если шифруешь state, шифруй и его. На S3, encryption-by-default распространяется на все объекты, включая backup. -
terraform show -json plan.tfplanпоказывает sensitive значения. Plan-файл как артефакт между jobs = распространение секретов. Минимизируй retention, шифруй передачу, удаляй после apply. См. tf-plan-apply-ci. -
OIDC роль с слишком широким trust.
repo:org/*:*= любой repo организации может assume. Привязывай к конкретному repo+ref. Лучше ещё к environment GitHub Actions с required reviewers для prod-role. -
Не путать KMS-encryption with в-state шифрование. S3-backend с KMS шифрует файл в покое. Terraform читает file → расшифровывает → держит в RAM открыто. KMS защищает от «украли S3-bucket объект», не от «у меня есть IAM-роль для чтения этого state».
-
Secrets Manager, платный. За каждый secret. Не делай тысячу secret'ов «по одному на каждое маленькое значение». Объединяй в JSON.
-
Vault требует поддержки. Это сам по себе stateful-сервис, кластер из 3-5 узлов, его надо ставить и обновлять. Если у тебя 10 секретов AWS Secrets Manager проще; от 100 и кросс-аккаунтное использование Vault окупается.
См. также в LinuxLab
- file-permissions,
terraform.tfstateс правами 0644 = читаемый любым sudo-user. Должен быть 0600. То же касается*.tfvarsи lock-файлов в remote backend. - secrets-management, как устроена выдача/ротация секретов на уровне Linux (systemd-credentials, secret-tool, vault-agent). Terraform, потребитель, источник инфраструктура из этой статьи.
- setuid-setgid-sticky привычка проверять биты у директорий, куда terraform пишет state и лог-файлы; setuid на CI-агенте, типовая дыра.