Перейти к содержимому
Hogin Hogin
Назад

OpenTofu: шифрование стейта и уход с Terraform без драмы

8 мин чтения

Откройте terraform.tfstate любого проекта старше года — там почти наверняка лежат пароли баз данных, приватные ключи и токены API открытым текстом. Так было всегда: state — это дамп всего, что Terraform знает о вашей инфраструктуре, включая значения sensitive-переменных. OpenTofu — форк Terraform, который в 2024 году получил то, чего у оригинала до сих пор нет: шифрование state и plan прямо на стороне клиента. И при этом миграция с Terraform занимает час, а не спринт.

Содержание

Открыть содержание

OpenTofu vs Terraform коротко

OpenTofu появился, когда HashiCorp в 2023 сменила лицензию Terraform с MPL 2.0 на BSL — код остался открытым для чтения, но использование в конкурентных продуктах запретили. Linux Foundation форкнула последнюю MPL-версию, и с тех пор OpenTofu живёт как независимый проект под управлением сообщества, а не одной компании.

Для практики это означает три вещи:

Из всех новых фич именно шифрование state — самый весомый аргумент для миграции: это закрывает дыру, с которой команды жили годами, и делает это без сторонних инструментов.

Проблема: state — это plaintext-дамп секретов

Terraform state хранит все атрибуты ресурсов, включая помеченные sensitive. Пометка sensitive прячет значение только в CLI-выводе — в самом JSON-файле state оно лежит открытым текстом. То же касается plan-файлов, которые часто сохраняют в CI как артефакт для apply в отдельном job.

Бэкенды — S3, Azure Blob, GCS, Terraform Cloud — обычно шифруют данные at rest на уровне хранилища. Но это шифрование прозрачно для любого, у кого есть право читать объект: S3 сам расшифровывает содержимое перед выдачей, если у вызывающего есть s3:GetObject. Значит, реальный периметр защиты — это IAM-политика бакета, а не крипто. Утечёт креды к бакету — утечёт весь state в открытом виде.

Кто может прочитать state: бэкенд шифрует диск, но отдаёт plaintext по IAM-праву

Шифрование на стороне клиента: как это работает в OpenTofu

С версии 1.7 OpenTofu умеет шифровать state и plan до того, как данные уйдут в бэкенд, и расшифровывать их после того, как заберёт локально. Бэкенд в этой схеме видит только шифротекст — даже если у атакующего есть полный read-доступ к S3-бакету, без ключа он получит бессмысленный blob.

Конфигурация живёт в блоке encryption внутри terraform {} и состоит из трёх частей: key provider (откуда берётся ключевой материал), method (алгоритм — на практике всегда aes_gcm) и привязка метода к state/plan:

terraform {
  encryption {
    key_provider "pbkdf2" "main" {
      passphrase = var.state_passphrase
    }

    method "aes_gcm" "main" {
      keys = key_provider.pbkdf2.main
    }

    state {
      method = method.aes_gcm.main
    }

    plan {
      method = method.aes_gcm.main
    }
  }
}

Ключевых провайдеров несколько, и они закрывают разные сценарии:

Шифруется весь файл целиком — не отдельные поля, а весь JSON state или plan, включая метаданные о ресурсах. Именно поэтому бэкенд после включения шифрования хранит нечитаемый бинарный blob, а не state с частично замазанными полями.

Поток шифрования: key provider → method → зашифрованный state в бэкенде

Backend-encryption — это не то же самое

Путаница возникает регулярно: «у нас S3 с SSE-KMS включён, разве этого не достаточно?» Нет, и вот почему разница принципиальна.

Backend-шифрование (SSE-S3, SSE-KMS, Azure Storage Service Encryption) защищает от кражи диска — физического носителя или снапшота хранилища в обход API. Оно ничего не защищает от легитимного запроса на чтение объекта: если у вызывающего есть IAM-право, бэкенд сам расшифрует и отдаст plaintext. Это шифрование транспорта и хранения, а не шифрование данных для получателя.

Client-side шифрование OpenTofu защищает сам контент: state шифруется до отправки и остаётся шифротекстом для всех, у кого нет ключа — включая администратора бакета, если он не имеет прав на KMS-ключ. Два уровня решают разные угрозы и хорошо работают вместе: SSE закрывает физический доступ к диску, client-side encryption закрывает избыточные IAM-права и логи, в которые мог утечь state.

Сравнение

Backend encryption (SSE)OpenTofu client-side encryption
Что шифруетдиск/объект в хранилищесам JSON state/plan
Кто видит plaintextлюбой с IAM-правом на бэкендтолько держатель ключа шифрования
Прозрачность для читателяполная (расшифровка автоматом)никакой без ключа
Настраивается настороне бэкенда (S3/Azure/GCS)стороне OpenTofu, блок encryption
Защищает откражи диска/снапшотаизбыточных прав, логов, чужих глаз в бэкенде
Есть в Terraformданет

Что нужно, чтобы включить шифрование существующего state

У проекта уже есть stateфайл с реальной инфраструктурой — переводить его в шифрованный режим нужно без простоя и без риска потерять данные. Механизм — fallback: OpenTofu пробует основной (шифрующий) метод, а если не получилось прочитать — откатывается на запасной.

Сначала добавляем шифрование с fallback на чтение старого plaintext-state:

terraform {
  encryption {
    key_provider "aws_kms" "main" {
      kms_key_id = "alias/tofu-state"
      region     = "eu-central-1"
      key_spec   = "AES_256"
    }

    method "aes_gcm" "main" {
      keys = key_provider.aws_kms.main
    }

    method "unencrypted" "migrate" {}

    state {
      method = method.aes_gcm.main
      fallback {
        method = method.unencrypted.migrate
      }
    }
  }
}

Дальше — один tofu apply (или даже tofu refresh): OpenTofu читает текущий plaintext state через fallback, а записывает уже зашифрованную версию основным методом. После этого проверяем на бэкенде, что файл действительно бинарный, и убираем блок method "unencrypted" "migrate" вместе с fallback — оставлять его смысла нет, обратной совместимости он больше не даёт.

Как проверить, что секреты больше не в plaintext

Самая надёжная проверка — руками посмотреть на объект в бэкенде, а не доверять конфигурации на слово:

aws s3 cp s3://my-bucket/env/prod/terraform.tfstate - | file -
# ожидаем: data (не ASCII text, не JSON)

aws s3 cp s3://my-bucket/env/prod/terraform.tfstate - | grep -ai "password\|secret" 
# ожидаем: пусто — до миграции эта команда находила бы совпадения

Если file распознаёт валидный JSON, а не бинарные данные — шифрование не применилось, конфигурация не подхватилась или state пишется старым terraform вместо tofu. Проверьте это до того, как считать миграцию завершённой.

Ловушки

Итог

Шифрование state — редкий случай в IaC, где цена входа почти нулевая, а закрываемая дыра — многолетняя и реальная: секреты, которые годами лежали открытым текстом в бэкенде, доступном по IAM-праву, а не по факту владения ключом. Миграция с Terraform на OpenTofu ради одной этой фичи оправдана сама по себе — совместимость на уровне HCL и state делает переход однодневной задачей, а не проектом. Если ваш state ещё нигде не зашифрован клиентской стороной — это самая дешёвая security-победа, которую можно взять на этой неделе.


Поделиться:

Следующая статья
Istio ambient: service mesh без сайдкаров