linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/kb/Security/tf-secrets-in-state

kb/security ── Security ── intermediate

Секреты и Terraform state: где хранить и как читать

State содержит всё что прошло через apply, пароли, ключи, токены в открытом виде. Решения: хранить секреты в Secrets Manager / Vault / KMS, читать через data-source, шифровать backend (S3 SSE-KMS), OIDC вместо access-keys для CI. «sensitive=true», про логи, не про шифрование.

view as markdownaka: terraform-secrets, secrets-manager-terraform, terraform-oidc-secrets

Откуда секреты в 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 про полный 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 про полную настройку.

Что ещё попадает в 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-агенте, типовая дыра.

§ команды

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.

§ см. также

  • tf-sensitivesensitive в Terraform: про логи, не про шифрование`sensitive = true` у variable/output/local прячет значение от CLI-вывода и логов. В state и .tfstate.backup значение **открытое**. Это redaction, не encryption. Реально секретное, храни в Secrets Manager или Vault, читай через data-source, плана не сохраняй в артефактах. `nonsensitive()` нужен в редких случаях когда sensitive-флаг каскадируется и мешает.
  • tf-remote-backend-s3Remote state в S3: бакет, DynamoDB lock, encryptionS3-backend хранит `terraform.tfstate` в бакете, DynamoDB-таблица даёт блокировку одного-apply-за-раз. Конфигурация, в блоке `backend "s3"` внутри `terraform { ... }`. State в S3. Это единственный source of truth, локального файла больше нет. Переезд с local на S3, через `terraform init -migrate-state`.
  • tf-oidc-awsOIDC между GitHub Actions и AWS, без access keysРаньше CI-runner ходил в AWS с долгоживущим access key. OIDC переворачивает это: GitHub выдаёт workflow signed JWT, STS обменивает на временные credentials через AssumeRoleWithWebIdentity. Никаких secrets, scope роли сужается до repo+branch+env. Три артефакта: IAM OIDC-провайдер, IAM-роль с trust policy, workflow с `id-token: write`.
  • tf-checkovCheckov: статический анализ HCLCheckov, Python-сканер от Prisma Cloud, проверяет `.tf` и `plan.json` против ~2000 встроенных правил (CKV_AWS_*, CKV_K8S_*, etc.). Запускается на HCL до plan'а (быстрее) или на JSON-plan (богаче, видит вычисленные значения). Suppressions делаются комментарием в HCL или в файле конфигурации; baseline-файл фиксирует найденные issues как «приемлемые на сейчас», чтобы новые сразу ломали CI.
  • aws-providerAWS Provider: настройки и где Terraform берёт ключиAWS-провайдер ищет credentials в нескольких местах подряд: env-переменные, ~/.aws/credentials, IAM-роль инстанса. Чаще всего достаточно `aws configure` локально или роль на EC2, больше ничего не настраивать.
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки