# Секреты и Terraform state: где хранить и как читать _Security · TerraformLab Knowledge Base_ **TL;DR:** State содержит всё что прошло через apply, пароли, ключи, токены в открытом виде. Решения: хранить секреты в Secrets Manager / Vault / KMS, читать через data-source, шифровать backend (S3 SSE-KMS), OIDC вместо access-keys для CI. «sensitive=true», про логи, не про шифрование. ## Откуда секреты в 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-ролям/пользователям. ```hcl 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](/terraform/kb/tf-remote-backend-s3.md) про полный backend-setup. ## Реальные секреты, не в HCL Антипаттерн: ```hcl variable "db_password" { default = "supersecret" # пароль в коде = пароль в git } ``` Антипаттерн-2: ```bash TF_VAR_db_password="supersecret" terraform apply ``` ...если history shell записывает env-vars или CI-логи показывают переменные. Канонический паттерн, внешний secret manager: ### AWS Secrets Manager ```hcl 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 ```hcl 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 **создаёт** секрет и кладёт его в менеджер: ```hcl 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: 1. В AWS создаёшь IAM-роль, доверенную к OIDC-провайдеру GitHub. 2. 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" } } } ``` 3. В workflow: ```yaml permissions: 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](/terraform/kb/tf-oidc-aws.md) про полную настройку. ## Что ещё попадает в 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](/terraform/kb/tf-sensitive.md). - **`.tfstate.backup` тоже содержит секреты.** Если шифруешь state, шифруй и его. На S3, encryption-by-default распространяется на все объекты, включая backup. - **`terraform show -json plan.tfplan` показывает sensitive значения.** Plan-файл как артефакт между jobs = распространение секретов. Минимизируй retention, шифруй передачу, удаляй после apply. См. [tf-plan-apply-ci](/terraform/kb/tf-plan-apply-ci.md). - **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](/courses/linux/kb/file-permissions), `terraform.tfstate` с правами 0644 = читаемый любым sudo-user. Должен быть 0600. То же касается `*.tfvars` и lock-файлов в remote backend. - [secrets-management](/courses/linux/kb/secrets-management), как устроена выдача/ротация секретов на уровне Linux (systemd-credentials, secret-tool, vault-agent). Terraform, потребитель, источник инфраструктура из этой статьи. - [setuid-setgid-sticky](/courses/linux/kb/setuid-setgid-sticky) привычка проверять биты у директорий, куда terraform пишет state и лог-файлы; setuid на CI-агенте, типовая дыра. ## Команды ```bash aws secretsmanager create-secret --name prod/db/master --secret-string '{"password":"..."}' ``` Создать секрет вне Terraform. Terraform потом будет читать через data. ```bash terraform state pull | jq '.resources[].instances[].attributes | select(.password)' ``` Что в state из паролей. Используй чтобы понять что у тебя там вообще лежит. ```bash aws kms encrypt --key-id alias/tfstate --plaintext fileb://state.tfstate --output text ``` Изолированно зашифровать чувствительный файл: на S3 этого не нужно (SSE-KMS), но полезно для local-backup. ```bash aws sts assume-role-with-web-identity --role-arn arn:aws:iam::...:role/github-tf-runner --web-identity-token "$TOKEN" ``` OIDC-flow вручную. В CI это делает aws-actions/configure-aws-credentials. ## См. также - [sensitive в Terraform: про логи, не про шифрование](/terraform/kb/tf-sensitive.md) - [Remote state в S3: бакет, DynamoDB lock, encryption](/terraform/kb/tf-remote-backend-s3.md) - [OIDC между GitHub Actions и AWS, без access keys](/terraform/kb/tf-oidc-aws.md) - [Checkov: статический анализ HCL](/terraform/kb/tf-checkov.md) - [AWS Provider: настройки и где Terraform берёт ключи](/terraform/kb/aws-provider.md)