Шифрование состояния и плана
OpenTofu поддерживает шифрование файлов состояния и плана при хранении как локально, так и при использовании бэкенда. Кроме того, шифрование можно использовать с источником данных terraform_remote_state. На этой странице объясняется, как настроить шифрование и какой метод шифрования подходит для конкретного случая.
Общие рекомендации и подводные камни (обязательно прочитайте)
При включении шифрования файлы состояния и плана невозможно будет восстановить без соответствующего ключа шифрования. Перед включением шифрования внимательно прочитайте этот раздел.
От чего защищает шифрование?
При включении шифрования OpenTofu будет шифровать данные состояния при хранении. Если злоумышленник получит доступ к файлу состояния, он не должен иметь возможности прочитать его и использовать содержащиеся в нём конфиденциальные значения (например, ключи доступа).
Однако шифрование не защищает от потери данных (повреждения файла состояния), а также от атак повторного воспроизведения (когда злоумышленник использует старый файл состояния или плана и обманом заставляет вас выполнить его). Кроме того, OpenTofu не защищает и не может защитить конфиденциальные значения в файле состояния от человека, выполняющего команду tofu.
Какие меры предосторожности необходимо принять?
При включении шифрования подумайте, кому необходим прямой доступ к файлу состояния. Если доступ нужен более чем небольшому числу людей, рассмотрите возможность запуска производственных команд plan и apply из системы непрерывной интеграции, чтобы защитить как ключ шифрования, так и конфиденциальные значения в состоянии.
Также необходимо выбрать тип ключа с учётом ваших требований к безопасности. Можно использовать статическую парольную фразу или выбрать систему управления ключами. Если вы выбрали систему управления ключами, для некоторых методов шифрования крайне важно настроить автоматическую ротацию ключей. Это особенно важно, если выбранный алгоритм шифрования может достичь состояния «насыщения ключа», когда приближается максимальный безопасный предел использования ключа, например AES-GCM. Дополнительную информацию об этом можно найти в разделе методы шифрования ниже.
Наконец, прежде чем включить шифрование, проверьте план аварийного восстановления и создайте временную резервную копию незашифрованного файла состояния. Также убедитесь, что у вас есть резервные копии ключей. После включения шифрования OpenTofu не сможет прочитать файл состояния без правильного ключа.
Переход с незашифрованного состояния/плана
Если у вас уже есть файл состояния и вы хотите включить шифрование, одного лишь включения шифрования недостаточно: OpenTofu откажется читать обычный текст. Это механизм защиты, который не позволяет OpenTofu читать изменённые незашифрованные данные. Подробные инструкции по миграции приведены в разделе первоначальная настройка ниже.
Гарантия совместимости
Криптографические исследования могут быстро менять состояние дел в этой области. Мы будем поддерживать все задокументированные поставщики ключей и методы в течение +1 минорной версии, но можем вводить новые версии тех же поставщиков ключей и методов (например, aes_gcm_v2) или новые поставщики ключей и методы в любой минорной версии. Если мы объявим поставщика ключей или метод устаревшим, при запуске tofu plan или tofu apply в консоли появится предупреждение. Если вы получили такое предупреждение, переключитесь до обновления до следующей версии.
Конфигурация
Настроить шифрование в OpenTofu можно, указав конфигурацию в коде OpenTofu или используя переменную среды TF_ENCRYPTION. Оба способа равнозначны; если использовать их одновременно, OpenTofu объединит конфигурации, причём настройки среды переопределят настройки, заданные в коде.
Базовая структура конфигурации выглядит следующим образом:
- Код
- Среда (оболочка Linux/UNIX)
- Среда (Powershell)
terraform {
encryption {
key_provider "some_key_provider" "some_key_provider_name" {
# Key provider options here
}
method "some_method_type" "some_method_name" {
# Method options here
keys = key_provider.some_key_provider.some_key_provider_name
}
state {
# Encryption/decryption for state data
method = method.some_method_type.some_method_name
}
plan {
# Encryption/decryption for plan data
method = method.some_method_type.some_method_name
}
remote_state_data_sources {
# See below
}
}
}TF_ENCRYPTION=$(cat <<EOF
key_provider "some_key_provider" "some_name" {
# Key provider options here
}
method "some_method_type" "some_method_name" {
# Method options here
keys = key_provider.some_key_provider.some_name
}
state {
# Encryption/decryption for state data
method = method.some_method_type.some_method_name
}
plan {
# Encryption/decryption for plan data
method = method.some_method_type.some_method_name
}
remote_state_data_sources {
# See below
}
EOF)$Env:TF_ENCRYPTION = @"
key_provider "some_key_provider" "some_name" {
# Key provider options here
}
method "some_method_type" "some_method_name" {
# Method options here
keys = key_provider.some_key_provider.some_name
}
state {
# Encryption/decryption for state data
method = method.some_method_type.some_method_name
}
plan {
# Encryption/decryption for plan data
method = method.some_method_type.some_method_name
}
remote_state_data_sources {
# See below
}
"@После шифрования данных не переименовывайте поставщиков ключей и методы в конфигурации! Зашифрованные данные в бэкенде содержат метаданные, связанные с их конкретными именами. Вместо этого используйте блок резервного варианта для обработки изменений поставщиков ключей. Кроме того, можно указать уникальный ключ хранения метаданных в поле encrypted_metadata_alias поставщика ключей, чтобы переименовывать поставщика без проблем.
Вместо HCL для конфигурации шифрования можно использовать синтаксис конфигурации JSON.
Если вы используете конфигурацию среды, можно добавить следующую конфигурацию кода, чтобы незашифрованные данные не записывались при отсутствии переменной среды:
terraform {
encryption {
state {
enforced = true
}
plan {
enforced = true
}
}
}Смена ключа и метода
В некоторых случаях может потребоваться изменить конфигурацию шифрования. Это может включать переименование поставщика ключей или метода, изменение парольной фразы для поставщика ключей или переход на другую систему управления ключами. OpenTofu поддерживает автоматическую смену конфигурации шифрования, если вы укажете старую конфигурацию в блоке fallback:
terraform {
encryption {
# Methods and key providers here.
state {
method = method.some_method.new_method_name
fallback {
method = method.some_method.old_method_name
}
}
plan {
method = method.some_method.new_method_name
fallback {
method = method.some_method.old_method_name
}
}
}
}Если OpenTofu не сможет прочитать файл состояния или плана новым методом, он автоматически попробует резервный метод. При сохранении файла состояния или плана OpenTofu всегда будет использовать новый метод, а не резервный.
Первоначальная настройка
Новый проект
Если вы создаёте новый проект и у вас ещё нет файла состояния, начните с этой примерной конфигурации шифрования на основе парольной фразы:
variable "passphrase" {
# Change passphrase to be at least 16 characters long:
default = "changeme!"
sensitive = true
}
terraform {
encryption {
## Step 1: Add the desired key provider:
key_provider "pbkdf2" "my_key_provider_name" {
passphrase = var.passphrase
}
## Step 2: Set up your encryption method:
method "aes_gcm" "my_method_name" {
keys = key_provider.pbkdf2.my_key_provider_name
}
state {
## Step 3: Link the desired encryption method:
method = method.aes_gcm.my_method_name
## Step 4: Run "tofu apply".
## Step 5: Consider adding the "enforced" option:
# enforced = true
}
## Step 6: Repeat steps 3-5 for plan{} if needed.
}
}Существующий проект
При первой настройке шифрования в существующем проекте файлы состояния и плана не зашифрованы. По умолчанию OpenTofu отказывается их читать, поскольку они могли быть изменены. Чтобы разрешить чтение незашифрованных данных, необходимо указать метод unencrypted:
variable "passphrase" {
# Change passphrase to be at least 16 characters long:
default = "changeme!"
sensitive = true
}
terraform {
encryption {
## Step 1: Add the unencrypted method:
method "unencrypted" "migrate" {}
## Step 2: Add the desired key provider:
key_provider "pbkdf2" "my_key_provider_name" {
passphrase = var.passphrase
}
## Step 3: Add the desired encryption method:
method "aes_gcm" "my_method_name" {
keys = key_provider.pbkdf2.my_key_provider_name
}
state {
## Step 4: Link the desired encryption method:
method = method.aes_gcm.my_method_name
## Step 5: Add the "fallback" block referencing the
## "unencrypted" method.
fallback {
method = method.unencrypted.migrate
}
## Step 6: Run "tofu apply".
## Step 7: Remove the "fallback" block above and
## consider adding the "enforced" option:
# enforced = true
}
## Step 8: Repeat steps 4-8 for plan{} if needed.
}
}
В конфигурации можно использовать переменные и локальные значения, но они не должны ссылаться на данные состояния или функции, определённые поставщиками. Все значения должны быть вычислены во время tofu init, до того как состояние станет доступно.
Отключение шифрования
Как и при первоначальной настройке выше, перейти к незашифрованным файлам состояния и плана также можно с помощью метода unencrypted следующим образом:
terraform {
encryption {
## Step 1: Leave the original encryption method unchanged:
method "some_method_type" "old_method_name" {
## Parameters for the old method here.
}
# Step 2: Add the unencrypted method here:
method "unencrypted" "migrate" {}
state {
## Step 3: Disable or remove the "enforced" option:
enforced = false
## Step 4: Move the original encryption method into the "fallback" block:
fallback {
method = method.some_method_type.old_method_name
}
## Step 5: Reference the unencrypted method as your primary "encryption" method.
method = method.unencrypted.migrate
}
## Step 6: Run "tofu apply".
## Step 7: Remove the "state" block once the migration is complete.
## Step 8: Repeat steps 3-7 for plan{} if needed.
}
}
Не удаляйте и не изменяйте исходный метод шифрования, пока не завершите миграцию.
Источники данных удалённого состояния
Также можно настроить шифрование для проектов, использующих источник данных terraform_remote_state. Настройка шифрования может совпадать с основной конфигурацией, но можно также определить отдельный набор ключей и методов. Синтаксис конфигурации:
terraform {
encryption {
# Key provider and method configuration here
remote_state_data_sources {
default {
method = method.method_type.my_method_name
}
remote_state_data_source "my_state" {
method = method.method_type.my_other_method_name
}
}
}
}
data "terraform_remote_state" "my_state" {
# ...
}Для конкретных удалённых состояний можно использовать следующий синтаксис:
-
mynameдля обращения к источнику данных с указанным именем в основном проекте. -
mymodule.mynameдля обращения к источнику данных с указанным именем в заданном модуле. -
mymodule.myname[0]для обращения к первому источнику данных с указанным именем в заданном модуле.
В некоторых случаях имена ключей в разных проектах могут конфликтовать, и тогда для поставщика ключей в одном из проектов потребуется использовать другое имя. В этом случае используйте параметр encrypted_metadata_alias, чтобы задать фиксированный ключ метаданных и обеспечить работу шифрования.
Например, в проекте «A» можно создать сертификаты, на которые требуется ссылаться из проекта «B». В проекте «A» можно настроить следующее:
terraform {
encryption {
key_provider "pbkdf2" "my_key_provider_name" {
passphrase = "OpenTofu has encryption"
# Note the fixed encrypted_metadata_alias here:
encrypted_metadata_alias = "certificates"
}
method "aes_gcm" "my_method_name" {
keys = key_provider.pbkdf2.my_key_provider_name
}
state {
method = method.aes_gcm.my_method_name
}
}
}
resource "tls_private_key" "webserver" {
algorithm = "ED25519"
}
resource "tls_self_signed_cert" "webserver" {
private_key_pem = tls_private_key.webserver.private_key_pem
subject {
common_name = "someserver.opentofu.org"
organization = "OpenTofu"
}
validity_period_hours = 24*365*10
allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
]
}
output "cert_pem" {
value = tls_self_signed_cert.webserver.cert_pem
}Затем на них можно ссылаться в проекте «B» следующим образом:
terraform {
encryption {
# Note that the name of the key here is different:
key_provider "pbkdf2" "my_key_renamed" {
passphrase = "OpenTofu has encryption"
# Note the fixed encrypted_metadata_alias here:
encrypted_metadata_alias = "certificates"
}
method "aes_gcm" "my_method_name" {
keys = key_provider.pbkdf2.my_key_renamed
}
remote_state_data_sources {
default {
method = method.aes_gcm.my_method_name
}
}
}
}
data "terraform_remote_state" "cert" {
backend = "local"
config = {
# Refer to the other project here:
path = "../a/terraform.tfstate"
}
}
output "cert" {
# Use data from the other project by referencing it as follows:
value = data.terraform_remote_state.cert.outputs.cert_pem
}Поставщики ключей
PBKDF2
Поставщик ключей PBKDF2 позволяет использовать длинную парольную фразу для создания ключа для такого метода шифрования, как AES-GCM. Его можно настроить следующим образом:
terraform {
encryption {
key_provider "pbkdf2" "foo" {
# Specify a long / complex passphrase (min. 16 characters)
passphrase = "correct-horse-battery-staple"
# Alternatively, receive the passphrase from another key provider:
chain = key_provider.other.provider
# Adjust the key length to the encryption method (default: 32)
key_length = 32
# Specify the number of iterations (min. 200.000, default: 600.000)
iterations = 600000
# Specify the salt length in bytes (default: 32)
salt_length = 32
# Specify the hash function (sha256 or sha512, default: sha512)
hash_function = "sha512"
}
}
}| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
| passphrase (обязательный) | Введите длинную и сложную парольную фразу. Обязателен, если не указан chain. |
16 символов | - |
| chain (обязательный) | Получение парольной фразы от другого поставщика ключей. Обязателен, если не указан passphrase. |
- | |
| key_length | Количество байтов, генерируемых в качестве ключа. | 1 | 32 |
| iterations | Количество итераций. Рекомендации см. в этом документе. | 200.000 | 600.000 |
| salt_length | Длина соли для выработки ключа. | 1 | 32 |
| hash_function | Укажите sha256 или sha512 в качестве хеш-функции. sha1 не поддерживается. |
Н/Д | sha512 |
| encrypted_metadata_alias | Необязательный идентификатор, под которым будут храниться метаданные в зашифрованных файлах состояния/плана. Укажите его, чтобы можно было изменить имя поставщика ключей. | - | производное от имени поставщика ключей |
AWS KMS
Этот поставщик ключей использует сервис управления ключами Amazon Web Services для создания ключей. Параметры аутентификации совпадают с параметрами бэкенда S3, за исключением устаревших параметров. Кроме того, укажите следующие параметры:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
| kms_key_id | Идентификатор ключа для AWS KMS. | 1 | - |
| key_spec |
Спецификация ключа для AWS KMS. Настройте её в соответствии с методом шифрования (например, AES_256). |
1 | - |
| encrypted_metadata_alias | Необязательный идентификатор, под которым будут храниться метаданные в зашифрованных файлах состояния/плана. Укажите его, чтобы можно было изменить имя поставщика ключей. | - | производное от имени поставщика ключей |
В следующем примере показана минимальная конфигурация:
terraform {
encryption {
key_provider "aws_kms" "basic" {
kms_key_id = "a4f791e1-0d46-4c8e-b489-917e0bec05ef"
region = "us-east-1"
key_spec = "AES_256"
}
}
}GCP KMS
Этот поставщик ключей использует сервис управления ключами Google Cloud для создания ключей. Параметры аутентификации совпадают с параметрами бэкенда GCS, за исключением устаревших параметров. Кроме того, укажите следующие параметры:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
| kms_encryption_key (обязательный) | Идентификатор ключа для GCP KMS. | Н/Д | - |
| key_length (обязательный) | Количество байтов, генерируемых в качестве ключа. Должно быть в диапазоне от 1 до 1024 байт. |
1 | - |
| encrypted_metadata_alias | Необязательный идентификатор, под которым будут храниться метаданные в зашифрованных файлах состояния/плана. Укажите его, чтобы можно было изменить имя поставщика ключей. | - | производное от имени поставщика ключей |
В следующем примере показана минимальная конфигурация:
terraform {
encryption {
key_provider "gcp_kms" "basic" {
kms_encryption_key = "projects/local-vehicle-id/locations/global/keyRings/ringid/cryptoKeys/keyid"
key_length = 32
}
}
}OpenBao
Этот поставщик ключей использует модуль секретов Transit в OpenBao для создания ключей данных. Его можно настроить следующим образом:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
| key_name (обязательный) | Имя ключа шифрования Transit, используемого для шифрования и расшифровки ключа данных. Предварительно настройте его на сервере OpenBao. | Н/Д | - |
| token |
Токен авторизации, используемый при обращении к API OpenBao. OpenTofu также может считывать его из переменной среды BAO_TOKEN. |
Н/Д | - |
| address | Адрес сервера OpenBao для обращения к API. OpenTofu также может считывать его из переменной среды BAO_ADDR. Ваша система должна доверять TLS-сертификату сервера. |
Н/Д | https://127.0.0.1:8200 |
| transit_engine_path | Путь, по которому в OpenBao включён модуль секретов Transit. Измените это значение, если вы изменили путь модуля Transit. | Н/Д | /transit |
| key_length | Количество байтов, генерируемых в качестве ключа. Доступные значения: 16, 32 или 64 байт. |
16 | 32 |
| encrypted_metadata_alias | Необязательный идентификатор, под которым будут храниться метаданные в зашифрованных файлах состояния/плана. Укажите его, чтобы можно было изменить имя поставщика ключей. | - | производное от имени поставщика ключей |
В следующем примере показан возможный вариант конфигурации:
terraform {
encryption {
key_provider "openbao" "my_bao" {
# Required. Name of the transit encryption key
# to use to encrypt/decrypt the data key.
key_name = "test-key"
# Optional. Authorization Token to use when accessing OpenBao API.
# You can also set this in the BAO_TOKEN environment variable.
token = "s.Fg8wA4nDrP08TirpjEXkrTmt"
# Optional. OpenBao server address to access the API on.
# You can also set this using the BAO_ADDR environment variable.
address = "http://127.0.0.1:8200"
# Optional. You can customize this if you mounted the
# transit engine on a different path. Default: /transit
transit_engine_path = "/my-org/transit"
# Optional. Number of bytes to generate as a key. Default: 32
key_length = 16
}
}
}Поставщик ключей OpenBao совместим с последней версией HashiCorp Vault (1.14), распространявшейся по лицензии MPL, но не поддерживает последующие версии, распространяемые по лицензии BUSL.
Внешний (экспериментальный)
Поставщик внешних команд позволяет запускать внешние команды для получения ключей шифрования. Эти программы должны быть специально написаны для работы с OpenTofu. У этого поставщика ключей есть следующие поля:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
command |
Внешняя команда для запуска, заданная в виде массива, где каждый параметр является элементом массива. | 1 |
Например, внешнюю программу можно настроить следующим образом:
terraform {
encryption {
key_provider "external" "foo" {
command = ["./some_program", "some_parameter"]
}
}
}Этот поставщик можно использовать вместе с параметром chain поставщика ключей PBKDF2 для передачи парольной фразы из внешней программы.
Создание внешнего поставщика ключей
Внешний поставщик может быть любой программой, которую можно запустить как приложение. Протокол состоит из трёх шагов:
- Внешняя программа записывает заголовок в стандартный поток вывода.
- OpenTofu передаёт метаданные внешней программе через стандартный поток ввода.
- Внешняя программа записывает сведения о ключе в стандартный поток вывода.
- Шаг 1: Запись заголовка
- Шаг 2: Чтение входных данных
- Шаг 3: Запись выходных данных
- Пример: Go
- Пример: Python
- Пример: оболочка POSIX
На первом шаге внешняя программа должна вывести заголовок в стандартный поток вывода, чтобы OpenTofu распознал в ней допустимый внешний поставщик ключей. Заголовок всегда должен состоять из одной строки и содержать следующее:
{"magic":"OpenTofu-External-Key-Provider","version":1}Открыть файл схемы JSON
После записи заголовка OpenTofu записывает входные данные в стандартный поток ввода внешней программы. Если OpenTofu нужно только зашифровать данные, это будет null. Если OpenTofu нужно расшифровать данные, оно запишет в стандартный поток ввода метаданные, ранее сохранённые вместе с зашифрованными данными:
{
"external_data": {
"key1": "value1",
"key2": "value2"
}
}Открыть файл схемы JSON
Получив входные данные, внешняя программа может сформировать выходные данные. Если входных данных нет, внешней программе нужно лишь предоставить ключ шифрования. Если входные данные есть, необходимо также предоставить ключ расшифрования. При необходимости выходные данные могут также содержать метаданные, которые будут сохранены вместе с зашифрованными данными и переданы как входные при следующем запуске.
{
"key": {
"encryption_key": "newly generated base64-encoded encryption key",
"decryption_key": "base64-encoded decryption key, if input meta was present, omitted otherwise"
},
"meta": {
"external_data": {
"key1": "value1",
"key2": "value2"
}
}
}Открыть файл схемы JSON
package main
import (
"encoding/json"
"io"
"log"
"os"
)
// Header is the initial greeting the key provider sends out.
type Header struct {
// Magic must always be OpenTofu-External-Keyprovider
Magic string `json:"magic"`
// Version must be 1.
Version int `json:"version"`
}
// Metadata describes both the input and the output metadata.
type Metadata struct {
ExternalData map[string]any `json:"external_data"`
}
// Input describes the input data structure. This is nil on input if no existing
// data needs to be decrypted.
type Input *Metadata
// Output describes the output data written to stdout.
type Output struct {
Key struct {
// EncryptionKey must always be provided.
EncryptionKey []byte `json:"encryption_key,omitempty"`
// DecryptionKey must be provided when the input metadata is present.
DecryptionKey []byte `json:"decryption_key,omitempty"`
} `json:"key"`
// Meta contains the metadata to store alongside the encrypted data. You can
// store data here you need to reconstruct the decryption key later.
Meta Metadata `json:"meta"`
}
func main() {
// Write logs to stderr
log.Default().SetOutput(os.Stderr)
// Write the header:
header := Header{
"OpenTofu-External-Key-Provider",
1,
}
marshalledHeader, err := json.Marshal(header)
if err != nil {
log.Fatalf("%v", err)
}
_, _ = os.Stdout.Write(append(marshalledHeader, []byte("\n")...))
// Read the input
input, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("Failed to read stdin: %v", err)
}
var inMeta Input
if err := json.Unmarshal(input, &inMeta); err != nil {
log.Fatalf("Failed to parse stdin: %v", err)
}
// TODO produce the encryption key
if inMeta != nil {
// TODO produce decryption key
}
output := Output{
// TODO: produce output
}
outputData, err := json.Marshal(output)
if err != nil {
log.Fatalf("Failed to encode output: %v", err)
}
_, _ = os.Stdout.Write(outputData)
}#!/usr/bin/python
import base64
import json
import sys
if __name__ == "__main__":
# Write the header:
sys.stdout.write((json.dumps(
{"magic": "OpenTofu-External-Key-Provider", "version": 1}) + "\n"
))
sys.stdout.flush()
# Read the input:
inputData = sys.stdin.read()
data = json.loads(inputData)
# Construct the key:
key = b''
for i in range(1, 17):
key += chr(i).encode('ascii')
# Output the keys:
if data is None:
# No input metadata was passed, we shouldn't output a decryption key.
# If needed, we can produce an output metadata here, which will be
# stored alongside the encrypted data.
outputMeta = {"external_data":{}}
sys.stdout.write(json.dumps({
"keys": {
"encryption_key": base64.b64encode(key).decode('ascii')
},
"meta": outputMeta
}))
else:
# We had some input metadata, output a decryption key. In a real-life
# scenario we would use the metadata for something like pbdkf2.
inputMeta = data["external_data"]
# Do something with the input metadata if needed and produce the output
# metadata:
outputMeta = {"external_data":{}}
sys.stdout.write(json.dumps({
"keys": {
"encryption_key": base64.b64encode(key).decode('ascii'),
"decryption_key": base64.b64encode(key).decode('ascii')
},
"meta": outputMeta
}))#!/bin/sh
set -e
# Output the header as a single line:
echo '{"magic":"OpenTofu-External-Key-Provider","version":1}'
# Read the input metadata.
INPUT=$(echo -n $(cat))
if [ "${INPUT}" = "null" ]; then
# We don't have metadata and shouldn't output a decryption key.
cat << EOF
{
"keys":{
"encryption_key":"AQIDBAUGBwgJCgsMDQ4PEA=="
},
"meta":{
"external_data":{}
}
}
EOF
else
# We have metadata and should output a decryption key. In our simplified case
# it is the same as the encryption key.
cat << EOF
{
"keys":{
"encryption_key":"AQIDBAUGBwgJCgsMDQ4PEA==",
"decryption_key":"AQIDBAUGBwgJCgsMDQ4PEA=="
},
"meta":{
"external_data":{}
}
}
EOF
fiМетоды
AES-GCM
На данный момент поддерживается только метод шифрования AES-GCM. Его можно настроить следующим образом:
terraform {
encryption {
# Key provider configuration here
method "aes_gcm" "yourname" {
keys = key_provider.your_key_provider_type.your_key_provider_name
}
}
}Для метода AES-GCM нужны ключи длиной 16, 24 или 32 байта. Настройте поставщик ключей так, чтобы он предоставлял ключи именно такой длины.
AES-GCM — это безопасный алгоритм шифрования, соответствующий отраслевым стандартам, однако он подвержен «насыщению ключа». Чтобы настроить безопасную систему, следует использовать либо поставщик ключей для их выработки (например, PBKDF2) с длинной и сложной парольной фразой, либо систему управления ключами, которая регулярно и автоматически меняет ключи. Использование коротких неизменяемых ключей снижает надёжность шифрования.
Внешний (экспериментальный)
Метод с внешней командой позволяет запускать внешние команды для выполнения шифрования и расшифрования. Эти программы должны быть специально написаны для работы с OpenTofu. У этого поставщика ключей есть следующие поля:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
encrypt_command |
Внешняя команда для шифрования, задаваемая в виде массива, где каждый параметр является отдельным элементом массива. | 1 | |
decrypt_command |
Внешняя команда для расшифрования, задаваемая в виде массива, где каждый параметр является отдельным элементом массива. | 1 | |
keys |
Ссылка на поставщика ключей, если внешней команде требуются ключи. |
Например, внешнюю программу можно настроить следующим образом:
terraform {
encryption {
key_provider "external" "foo" {
encrypt_command = ["./some_program", "--encrypt"]
decrypt_command = ["./some_program", "--decrypt"]
# Optional:
keys = key_provider.some.provider
}
}
}Написание внешнего метода
Внешний метод может быть любым, если его можно запустить как приложение. Протокол состоит из 3 шагов:
- Внешняя программа записывает заголовок в стандартный поток вывода.
- OpenTofu передаёт внешней программе через стандартный поток ввода материал ключа и данные для шифрования или расшифрования.
- Внешняя программа записывает зашифрованные или расшифрованные данные в стандартный поток вывода.
- Шаг 1: Запись заголовка
- Шаг 2: Чтение входных данных
- Шаг 3: Запись выходных данных
- Пример: Go
- Пример: Python
На первом шаге внешняя программа должна вывести заголовок в стандартный поток вывода, чтобы OpenTofu распознал в ней допустимый внешний метод. Заголовок всегда должен состоять из одной строки и содержать следующее:
{"magic":"OpenTofu-External-Encryption-Method","version":1}Открыть файл схемы JSON
После записи заголовка OpenTofu записывает материал ключа и обрабатываемые данные в стандартный поток ввода внешней программы. Материал ключа может отсутствовать, если поставщик ключей не настроен. Входные данные всегда имеют следующий формат:
{
"payload": "base64-encoded payload to encrypt or decrypt",
"key": "base64-encoded key for encryption or decryption (optional)"
}Открыть файл схемы JSON
Получив входные данные, внешняя программа может сформировать выходные данные.
{
"payload": "base64-encoded encrypted or decrypted payload"
}Открыть файл схемы JSON
package main
import (
"encoding/json"
"io"
"log"
"os"
)
// Header is the initial line that needs to be written as JSON when the program starts.
type Header struct {
Magic string `json:"magic"`
Version int `json:"version"`
}
// Input is the input data received from OpenTofu in response to the header as JSON.
type Input struct {
// Key is the encryption or decryption key, if present.
Key []byte `json:"key,omitempty"`
// Payload is the data to encrypt/decrypt.
Payload []byte `json:"payload"`
}
// Output is the data structure that should be written to the output.
type Output struct {
// Payload is the payload that has been encrypted/decrypted by the external method.
Payload []byte `json:"payload"`
}
func main() {
// Write logs to stderr
log.Default().SetOutput(os.Stderr)
// Write header:
header := Header{
"OpenTofu-External-Encryption-Method",
1,
}
marshalledHeader, err := json.Marshal(header)
if err != nil {
log.Fatalf("%v", err)
}
_, err = os.Stdout.Write(append(marshalledHeader, []byte("\n")...))
if err != nil {
log.Fatalf("Failed to write output: %v", err)
}
// Read input:
input, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("Failed to read stdin: %v", err)
}
var inputData Input
if err = json.Unmarshal(input, &inputData); err != nil {
log.Fatalf("Failed to parse stdin: %v", err)
}
// Encrypt/decrypt the input and produce the output here.
var outputPayload []byte
if len(os.Args) != 2 {
log.Fatalf("Expected --encrypt or --decrypt")
}
switch os.Args[1] {
case "--encrypt":
// Encrypt the payload
case "--decrypt":
// Decrypt the payload
default:
log.Fatalf("Expected --encrypt or --decrypt")
}
// Write output
output := Output{
Payload: outputPayload,
}
outputData, err := json.Marshal(output)
if err != nil {
log.Fatalf("Failed to stringify output: %v", err)
}
_, err = os.Stdout.Write(outputData)
if err != nil {
log.Fatalf("Failed to write output: %v", err)
}
}#!/usr/bin/python
import argparse
import base64
import json
import sys
if __name__ == "__main__":
# Make sure that this program isn't running interactively:
if sys.stdout.isatty():
sys.stderr.write("This is an OpenTofu encryption method and is not meant to be run interactively. "
"Please configure this program in your OpenTofu encryption block to use it.\n")
sys.exit(1)
parser = argparse.ArgumentParser(prog='My External Encryption Method')
parser.add_argument('--encrypt', action='store_true')
parser.add_argument('--decrypt', action='store_true')
args = parser.parse_args()
# Write the header:
sys.stdout.write((json.dumps({"magic": "OpenTofu-External-Encryption-Method", "version": 1}) + "\n"))
sys.stdout.flush()
# Read the input:
inputData = sys.stdin.read()
data = json.loads(inputData)
key = base64.b64decode(data["key"])
payload = base64.b64decode(data["payload"])
# Produce the output payload here.
if args.encrypt:
outputPayload = b''
elif args.decrypt:
outputPayload = b''
else:
raise "Expected --encrypt or --decrypt."
# Write the output:
sys.stdout.write(json.dumps({
"payload": base64.b64encode(outputPayload).decode('ascii'),
}))Незашифрованный
Метод unencrypted используется для явного перехода к шифрованию и обратно. Он не требует настройки и показан выше в блоке Начальная настройка.
Copyright (c) The OpenTofu Authors
Copyright (c) 2014 HashiCorp, Inc.
Mozilla Public License, version 2.0
https://opentofu.org/docs/v1.10/language/state/encryption/