Spec-Zone.ru › OpenTofu 1.11

Шифрование состояния и плана

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
    }
  }
}

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​

Этот поставщик ключей использует механизм секретов OpenBao Transit для создания ключей данных. Его можно настроить следующим образом:

Параметр Описание Мин. По умолчанию
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

Например, внешнюю программу можно настроить следующим образом:

Code Block
terraform {
  encryption {
    key_provider "external" "foo" {
      command = ["./some_program", "some_parameter"]
    }
  }
}
Примечание

Этот провайдер можно использовать вместе с параметром chain провайдера ключей PBKDF2, чтобы передавать парольную фразу из внешней программы.

Написание внешнего провайдера ключей​

Внешним провайдером может быть любая программа, которую можно запустить как приложение. Протокол состоит из 3 шагов:

  1. Внешняя программа записывает заголовок в стандартный поток вывода.
  2. OpenTofu отправляет метаданные внешней программе через стандартный поток ввода.
  3. Внешняя программа записывает информацию о ключе в стандартный поток вывода.
  • Шаг 1: запись заголовка
  • Шаг 2: чтение входных данных
  • Шаг 3: запись выходных данных
  • Пример: Go
  • Пример: Python
  • Пример: оболочка POSIX

Сначала внешняя программа должна вывести заголовок в стандартный поток вывода, чтобы OpenTofu мог определить, что это действительный внешний провайдер ключей. Заголовок всегда должен состоять из одной строки и содержать следующее:

Code Block
{"magic":"OpenTofu-External-Key-Provider","version":1}

Открыть файл схемы JSON

После записи заголовка OpenTofu записывает входные данные в стандартный поток ввода внешней программы. Если OpenTofu нужно только зашифровать данные, это будет null. Если OpenTofu нужно расшифровать данные, в стандартный поток ввода будут записаны метаданные, ранее сохраненные вместе с зашифрованными данными:

Code Block
{
  "external_data": {
    "key1": "value1",
    "key2": "value2"
  }
}

Открыть файл схемы JSON

Получив входные данные, внешняя программа может сформировать выходные данные. Если входных данных нет, внешней программе нужно только предоставить ключ шифрования. Если входные данные есть, она также должна предоставить ключ расшифрования. При необходимости выходные данные могут содержать метаданные, которые будут сохранены вместе с зашифрованными данными и переданы в качестве входных при следующем запуске.

Code Block
{
  "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

Code Block
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)
}
Code Block
#!/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
        }))
Code Block
#!/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. Его можно настроить следующим образом:

Code Block
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 Ссылка на провайдер ключей, если внешней команде нужны ключи.

Например, внешнюю программу можно настроить следующим образом:

Code Block
terraform {
  encryption {
    method "external" "foo" {
      encrypt_command = ["./some_program", "--encrypt"]
      decrypt_command = ["./some_program", "--decrypt"]
      # Optional:
      keys = key_provider.some.provider
    }
  }
}

Написание внешнего метода​

Внешним методом может быть любая программа, которую можно запустить как приложение. Протокол состоит из 3 шагов:

  1. Внешняя программа записывает заголовок в стандартный поток вывода.
  2. OpenTofu отправляет ключевой материал и данные для шифрования/расшифрования внешней программе через стандартный поток ввода.
  3. Внешняя программа записывает зашифрованные/расшифрованные данные в стандартный поток вывода.
  • Шаг 1: запись заголовка
  • Шаг 2: чтение входных данных
  • Шаг 3: запись выходных данных
  • Пример: Go
  • Пример: Python

Сначала внешняя программа должна вывести заголовок в стандартный поток вывода, чтобы OpenTofu мог определить, что это действительный внешний метод. Заголовок всегда должен состоять из одной строки и содержать следующее:

Code Block
{"magic":"OpenTofu-External-Encryption-Method","version":1}

Открыть файл схемы JSON

После записи заголовка OpenTofu записывает ключевой материал и данные для обработки в стандартный поток ввода внешней программы. Ключевой материал может отсутствовать, если провайдер ключей не настроен. Входные данные всегда имеют следующий формат:

Code Block
{
  "payload": "base64-encoded payload to encrypt or decrypt",
  "key": "base64-encoded key for encryption or decryption (optional)"
}

Открыть файл схемы JSON

Получив входные данные, внешняя программа может сформировать выходные данные.

Code Block
{
  "payload": "base64-encoded encrypted or decrypted payload"
}

Открыть файл схемы JSON

Code Block
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)
	}
}
Code Block
#!/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.11/language/state/encryption/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API