Шифрование состояния и плана
OpenTofu поддерживает шифрование файлов состояния и плана при хранении — как для локального хранилища, так и при использовании бэкенда. Кроме того, шифрование можно использовать с источником данных terraform_remote_state. На этой странице объясняется, как настроить шифрование и какой метод подходит для того или иного случая.
Общие рекомендации и возможные проблемы (прочитайте)
При включении шифрования файлы состояния и плана невозможно будет восстановить без соответствующего ключа шифрования. Внимательно прочитайте этот раздел, прежде чем включать шифрование.
От чего защищает шифрование?
При включении шифрования OpenTofu будет шифровать данные состояния при хранении. Если злоумышленник получит доступ к файлу состояния, он не должен иметь возможности прочитать его и воспользоваться конфиденциальными значениями (например, ключами доступа), содержащимися в файле состояния.
Однако шифрование не защищает от потери данных (повреждения файла состояния), а также от атак с повторным воспроизведением (когда злоумышленник использует старый файл состояния или плана и обманом заставляет вас запустить его). Кроме того, OpenTofu не защищает и не может защитить конфиденциальные значения в файле состояния от человека, выполняющего команду tofu.
Какие меры предосторожности необходимо принять?
При включении шифрования определите, кому нужен непосредственный доступ к файлу состояния. Если такой доступ нужен более чем небольшому числу людей, рассмотрите возможность выполнять рабочие запуски plan и apply в производственной среде из системы непрерывной интеграции, чтобы защитить и ключ шифрования, и конфиденциальные значения в состоянии.
Вам также необходимо выбрать тип ключа в соответствии с требованиями безопасности. Можно использовать статическую парольную фразу или выбрать систему управления ключами. При выборе системы управления ключами для некоторых методов шифрования обязательно настройте автоматическую ротацию ключей. Это особенно важно, если выбранный алгоритм шифрования может достичь «насыщения ключа», когда приближается максимальный безопасный предел его использования, например AES-GCM. Подробнее об этом можно узнать в разделе методы шифрования ниже.
Наконец, прежде чем включать шифрование, проверьте план аварийного восстановления и создайте временную резервную копию незашифрованного файла состояния. Также убедитесь, что у вас есть резервные копии ключей. После включения шифрования OpenTofu не сможет прочитать файл состояния без правильного ключа.
Переход с незашифрованного состояния/плана
Если у вас уже есть файл состояния и вы хотите включить шифрование, простого включения шифрования недостаточно: OpenTofu откажется читать данные в открытом виде. Это защитный механизм, не позволяющий OpenTofu читать изменённые незашифрованные данные. Подробные инструкции по миграции см. в разделе первоначальная настройка ниже.
Гарантия совместимости
Исследования в области криптографии могут быстро менять современные стандарты. Мы будем поддерживать все поставщики ключей и методы, описанные в документации, в течение одной дополнительной минорной версии, но можем добавлять новые версии тех же поставщиков ключей и методов (например, 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 Servers для создания ключей. Параметры аутентификации идентичны параметрам бэкенда 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
}
}
}Azure Vault
Этот поставщик ключей использует Azure Key Vault для создания ключей. Параметры аутентификации в основном идентичны параметрам бэкенда Azure, за исключением устаревших параметров и параметров, относящихся к хранилищу. Обратите внимание: в отличие от бэкенда состояния, этот поставщик ключей всегда использует Entra ID. Доступны следующие параметры:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
| vault_uri (обязательный) | URI хранилища в Azure. Формат: https://{vault-name}.vault.azure.net
|
Н/Д | - |
| vault_key_name (обязательный) | Имя ключа в указанном Azure Vault. | Н/Д | - |
| key_length (обязательный) | Количество байтов для создания ключа. Должно быть не менее 1. |
1 | - |
| symmetric | Необязательный логический параметр, указывающий, что предоставленный ключ является симметричным (только HSM). | Н/Д | false |
| symmetric_key_size | Размер симметричного ключа (128, 192 или 256). Обязателен, если symmetric имеет значение true. |
Н/Д | - |
В следующем примере показана минимальная конфигурация с асимметричным ключом в Azure Key Vault:
terraform {
encryption {
key_provider "azure_vault" "asymmetric" {
vault_uri = "https://example-keys.vault.azure.net"
vault_key_name = "my-rsa-key"
key_length = 32
}
}
}В следующем примере показана минимальная конфигурация с симметричным ключом в Azure Key Vault Managed HSM:
terraform {
encryption {
key_provider "azure_vault" "symmetric" {
// symmetric keys are available in HSM only
vault_uri = "https://hardware-example.managedhsm.azure.net/"
vault_key_name = "my-aes-key"
// We only select this if this is an AES key
symmetric = true
// Keep note of how large the key is when you create it.
// This encryption key provider will only work if the size
// specified here matches the size of the AES key generated.
symmetric_key_size = 256
// This key length relates to the data-encryption key,
// NOT the key-encryption key specified above.
key_length = 32
}
}
}Обязательно укажите, симметричный ключ или асимметричный, поскольку от этого зависит используемый алгоритм шифрования.
Если используется асимметричный ключ RSA (что обычно и бывает), применяется алгоритм RSAES с оптимальным асимметричным дополнением шифрования (RSA-OAEP-256).
Если используется симметричный ключ AES, применяется алгоритм шифрования AES в режиме счётчика Галуа (AES-GCM). Внутренне выбор зависит от размера ключа, поэтому в случае симметричного ключа AES необходимо указать его размер.
Поскольку алгоритмы внутри системы различаются, при переходе между асимметричным и симметричным типами ключа (или при изменении размера симметричного ключа) в разных версиях ключа следует сохранить того же поставщика ключей и изменить его настройки на месте. Например, если вы меняете симметричный ключ AES размером 192 на асимметричный ключ RSA или EC, следует изменить конфигурацию следующим образом:
terraform {
encryption {
key_provider "azure_vault" "my_key" {
vault_uri = "https://hardware-example.managedhsm.azure.net/"
vault_key_name = "my-aes-key"
symmetric = true
symmetric_key_size = 192
key_length = 32
}
method "aes_gcm" "crypto" {
keys = key_provider.azure_vault.my_key
}
state {
method = method.aes_gcm.crypto
}
plan {
method = method.aes_gcm.crypto
}
}
}На следующую:
terraform {
encryption {
key_provider "azure_vault" "my_key" {
vault_uri = "https://hardware-example.managedhsm.azure.net/"
vault_key_name = "my-aes-key"
key_length = 32
}
method "aes_gcm" "crypto" {
keys = key_provider.azure_vault.my_key
}
state {
method = method.aes_gcm.crypto
}
plan {
method = method.aes_gcm.crypto
}
}
}OpenTofu запоминает алгоритм, использованный для ключа расшифрования, и сохраняет его в состоянии. Он по-прежнему будет знать, как расшифровать данные с помощью ранее настроенного поставщика ключей, и будет шифровать данные с новой конфигурацией. Не делайте так — это не сработает:
terraform {
encryption {
key_provider "azure_vault" "my_key" {
vault_uri = "https://hardware-example.managedhsm.azure.net/"
vault_key_name = "my-aes-key"
symmetric = true
symmetric_key_size = 192
key_length = 32
}
key_provider "azure_vault" "new_key" {
vault_uri = "https://hardware-example.managedhsm.azure.net/"
vault_key_name = "my-aes-key"
key_length = 32
}
method "aes_gcm" "crypto" {
keys = key_provider.azure_vault.my_key
}
method "aes_gcm" "new_crypto" {
keys = key_provider.azure_vault.new_key
}
state {
method = method.aes_gcm.new_crypto
fallback {
method = method.aes_gcm.crypto
}
}
plan {
method = method.aes_gcm.new_crypto
fallback {
method = method.aes_gcm.crypto
}
}
}
}OpenTofu попытается использовать резервный вариант и для шифрования, и для расшифрования. В отличие от других поставщиков, для которых рекомендуется использовать резервный вариант, здесь это приведёт к ошибке при изменении версии ключа, поскольку резервный вариант не сможет шифровать с помощью текущей версии ключа.
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 нет соответствующих отзывов, чтобы решить, следует ли выводить это из экспериментальной стадии. Поэтому в обозримом будущем, пока отзывов нет, этот key_provider останется на экспериментальной стадии.
Расскажите о своем опыте использования этого key_provider в этой задаче или на канале CNCF Slack #opentofu.
Поставщик внешних команд позволяет запускать внешние команды для получения ключей шифрования. Эти программы должны быть специально написаны для работы с OpenTofu. У этого поставщика ключей есть следующие поля:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
command |
Внешняя команда для запуска, заданная в виде массива, где каждый параметр является элементом массива. | 1 |
Например, внешнюю программу можно настроить следующим образом:
terraform {
encryption {
key_provider "external" "foo" {
command = ["./some_program", "some_parameter"]
}
}
}Этот поставщик можно использовать вместе с параметром chain поставщика ключей PBKDF2, чтобы передать парольную фразу из внешней программы.
Создание внешнего поставщика ключей
Внешний поставщик может быть любым, если его можно запустить как приложение. Протокол состоит из трех шагов:
- Внешняя программа записывает заголовок в стандартный вывод.
- OpenTofu отправляет метаданные внешней программе через стандартный ввод.
- Внешняя программа записывает сведения о ключе в стандартный вывод.
- Шаг 1: запись заголовка
- Шаг 2: чтение входных данных
- Шаг 3: запись выходных данных
- Пример: Go
- Пример: Python
- Пример: POSIX Shell
На первом шаге внешняя программа должна вывести заголовок в стандартный вывод, чтобы 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
type Keys 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"`
}
// Output describes the output data written to stdout.
type Output struct {
Keys Keys `json:"keys"`
// 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)
}
var keys Keys
keys.EncryptionKey = []byte("AQIDBAUGBwgJCgsMDQ4PEA==") // TODO produce the encryption key
if inMeta != nil {
keys.DecryptionKey = []byte("AQIDBAUGBwgJCgsMDQ4PEA==") // TODO produce the decryption key
}
output := Output{
Keys: keys,
Meta: Metadata{
// TODO: customize your metadata
},
}
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'AQIDBAUGBwgJCgsMDQ4PEA=='
# 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="$(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 нет соответствующих отзывов, чтобы решить, следует ли выводить это из экспериментальной стадии. Поэтому в обозримом будущем, пока отзывов нет, этот method останется на экспериментальной стадии.
Расскажите о своем опыте использования этого method в этой задаче или на канале CNCF Slack #opentofu.
Метод внешних команд позволяет запускать внешние команды для шифрования и расшифрования. Эти программы должны быть специально написаны для работы с OpenTofu. У этого поставщика ключей есть следующие поля:
| Параметр | Описание | Мин. | По умолчанию |
|---|---|---|---|
encrypt_command |
Внешняя команда для шифрования, заданная в виде массива, где каждый параметр является элементом массива. | 1 | |
decrypt_command |
Внешняя команда для расшифрования, заданная в виде массива, где каждый параметр является элементом массива. | 1 | |
keys |
Ссылка на поставщика ключей, если внешней команде требуются ключи. |
Например, внешнюю программу можно настроить следующим образом:
terraform {
encryption {
method "external" "foo" {
encrypt_command = ["./some_program", "--encrypt"]
decrypt_command = ["./some_program", "--decrypt"]
# Optional:
keys = key_provider.some.provider
}
}
}Создание внешнего метода
Внешний метод может быть любым, если его можно запустить как приложение. Протокол состоит из трех шагов:
- Внешняя программа записывает заголовок в стандартный вывод.
- 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.12/language/state/encryption/