Откройте terraform.tfstate любого проекта старше года — там почти наверняка лежат пароли баз данных, приватные ключи и токены API открытым текстом. Так было всегда: state — это дамп всего, что Terraform знает о вашей инфраструктуре, включая значения sensitive-переменных. OpenTofu — форк Terraform, который в 2024 году получил то, чего у оригинала до сих пор нет: шифрование state и plan прямо на стороне клиента. И при этом миграция с Terraform занимает час, а не спринт.
Содержание
Открыть содержание
- OpenTofu vs Terraform коротко
- Проблема: state — это plaintext-дамп секретов
- Шифрование на стороне клиента: как это работает в OpenTofu
- Backend-encryption — это не то же самое
- Сравнение
- Что нужно, чтобы включить шифрование существующего state
- Как проверить, что секреты больше не в plaintext
- Ловушки
- Итог
OpenTofu vs Terraform коротко
OpenTofu появился, когда HashiCorp в 2023 сменила лицензию Terraform с MPL 2.0 на BSL — код остался открытым для чтения, но использование в конкурентных продуктах запретили. Linux Foundation форкнула последнюю MPL-версию, и с тех пор OpenTofu живёт как независимый проект под управлением сообщества, а не одной компании.
Для практики это означает три вещи:
- Лицензия. MPL 2.0 — свободно используется где угодно, в том числе в коммерческих IaC-платформах, которым BSL Terraform запрещал.
- Совместимость. Тот же HCL, тот же формат state, тот же Terraform Registry для провайдеров и модулей. Написанный для Terraform код в 95% случаев работает без изменений.
- Фичи. Ветки разошлись: у OpenTofu появились
for_eachпо провайдерам, встроенное шифрование state/plan и ряд улучшений тестирования, которых в Terraform нет и вряд ли появятся в ближайшее время.
Из всех новых фич именно шифрование 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 в открытом виде.
Шифрование на стороне клиента: как это работает в 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
}
}
}
Ключевых провайдеров несколько, и они закрывают разные сценарии:
pbkdf2— ключ выводится из пароля/пассфразы. Просто для старта, но пассфразу всё равно нужно куда-то положить — обычно в переменную окруженияTF_VAR_..., не в код.aws_kms/gcp_kms— ключевой материал приходит из облачного KMS. Ротацию, аудит доступа и права на использование ключа берёт на себя облако — это правильный выбор для команды.- внешние провайдеры (
external) — произвольная программа по простому JSON-протоколу отдаёт ключ. Так подключают Vault/OpenBao Transit engine или любое собственное хранилище секретов.
Шифруется весь файл целиком — не отдельные поля, а весь JSON state или plan, включая метаданные о ресурсах. Именно поэтому бэкенд после включения шифрования хранит нечитаемый бинарный blob, а не 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. Проверьте это до того, как считать миграцию завершённой.
Ловушки
- Ротация ключей. Смена KMS-ключа или пассфразы — та же схема, что и первичная миграция: новый метод как основной, старый — в
fallback, дождаться, пока весь state и все локальные копии участников команды перепишутся новым ключом, потом убрать fallback. Пропустить этот шаг — значит получить state, который часть команды не сможет прочитать. - Локальные копии и plan-артефакты.
tofu plan -out=tfplan, сохранённый в CI как артефакт, шифруется тем же механизмом — и расшифровать его сможет только тот, у кого есть доступ к ключу. Еслиapplyпо сохранённому plan запускает отдельный CI-раннер без прав на KMS, пайплайн сломается уже после включения шифрования, а не до. - Командная работа и потерянный пассфраз. Для
pbkdf2пассфраза — единственный путь к данным: потеряли её без бэкапа — потеряли state безвозвратно, никакого «сброса пароля» не существует. Для команд облачный KMS почти всегда лучше именно поэтому: ключом управляет облако, доступ выдаётся и отзывается через IAM, а не расшаривается в переменных окружения между разработчиками. terraformиtofuв одном пайплайне. Если часть job’ов ещё вызывает бинарникterraform, он просто не поймёт блокencryptionи упадёт (или, в худшем случае, при использовании смешанных версий — начнёт работать с state неожиданным образом). Миграция должна переключить все места запуска разом, включая локальные машины разработчиков.
Итог
Шифрование state — редкий случай в IaC, где цена входа почти нулевая, а закрываемая дыра — многолетняя и реальная: секреты, которые годами лежали открытым текстом в бэкенде, доступном по IAM-праву, а не по факту владения ключом. Миграция с Terraform на OpenTofu ради одной этой фичи оправдана сама по себе — совместимость на уровне HCL и state делает переход однодневной задачей, а не проектом. Если ваш state ещё нигде не зашифрован клиентской стороной — это самая дешёвая security-победа, которую можно взять на этой неделе.