Spec-Zone.ru › Ansible

Защита секретов Ansible с помощью SOPS

CNCF SOPS позволяет шифровать и расшифровывать файлы с помощью различных источников ключей (GPG, AWS KMS, GCP KMS, …). Для структурированных данных, таких как файлы YAML, JSON, INI и ENV, он шифрует значения, но не ключи отображения. В файлах YAML он также шифрует комментарии. Это делает SOPS отличным инструментом для шифрования учетных данных с помощью Ansible: вы можете легко увидеть, в каких файлах содержится та или иная переменная, но сами переменные будут зашифрованы.

Возможность использовать различные источники ключей упрощает работу в сложных средах по сравнению с Ansible Vault.

  • Установка SOPS
  • Установка community.sops в среде выполнения
  • Настройка SOPS
  • Работа с зашифрованными файлами
  • Работа с зашифрованными данными из других источников
  • Работа с зашифрованными переменными

Установка SOPS

Бинарные файлы и пакеты можно найти на странице выпусков проекта. В зависимости от вашей операционной системы, возможно, его также можно установить с помощью системного менеджера пакетов.

Эта коллекция предоставляет роль community.sops.install, которая позволяет установить SOPS и GNU Privacy Guard (GPG). Роль позволяет установить SOPS из системного менеджера пакетов или с GitHub; подробности см. в sops_source. SOPS и GPG можно установить на удаленных узлах или на контроллере Ansible; подробности см. в sops_install_on_localhost.

- name: Playbook to install SOPS
  hosts: all
  tasks:
    # To use the sops_encrypt module on a remote host, you need to install SOPS on it:
    - name: Install SOPS on remote hosts
      ansible.builtin.include_role:
        name: community.sops.install
      vars:
        sops_version: 2.7.0  # per default installs the latest version

    # To use the lookup plugin, filter plugin, vars plugin, or the load_vars action,
    # you need SOPS installed on localhost:
    - name: Install SOPS on localhost
      ansible.builtin.include_role:
        name: community.sops.install
      vars:
        sops_install_on_localhost: true

При использовании ansible-core 2.11 или более поздней версии можно также воспользоваться двумя вспомогательными плейбуками:

# Install SOPS on Ansible controller
$ ansible-playbook community.sops.install_localhost

# Install SOPS on remote servers
$ ansible-playbook community.sops.install --inventory /path/to/inventory

Установка community.sops в среде выполнения

При сборке среды выполнения, содержащей community.sops, обратите внимание: по умолчанию SOPS не устанавливается автоматически. Это связано с ограничением системы описания зависимостей для сред выполнения. Если вы собираете среду выполнения, содержащую community.sops, убедитесь, что SOPS установлен в ней.

Самый простой способ обеспечить это — использовать плейбук community.sops.install_localhost. При описании среды выполнения можно добавить RUN дополнительный шаг сборки в execution-environment.yml:

---
version: 3
dependencies:
  galaxy: requirements.yml
additional_build_steps:
  append_final:
    # Ensure that SOPS is installed in the EE, assuming the EE is for ansible-core 2.11 or newer
    - RUN ansible-playbook -v community.sops.install_localhost

Обратите внимание, что это работает только в том случае, если среда выполнения собрана с ansible-core 2.11 или новее. При использовании среды выполнения с Ansible 2.9 необходимо вручную применить роль community.sops.install. Также обратите внимание, что необходимо убедиться, что Ansible 2.9 использует правильный интерпретатор Python для установки системных пакетов; в приведенном ниже примере предполагается, что базовый образ среды выполнения основан на RHEL/CentOS:

---
version: 3
dependencies:
  galaxy: requirements.yml
additional_build_steps:
  append_final:
    # Special step needed for Ansible 2.9 based EEs
    - >-
      RUN ansible localhost -m include_role -a name=community.sops.install
          -e sops_install_on_localhost=true
          -e ansible_python_interpreter=/usr/libexec/platform-python

После выполнения этого шага вы сможете использовать все плагины и модули community.sops в среде выполнения (на localhost).

Настройка SOPS

Далее в этом руководстве предполагается, что SOPS уже установлен.

Для простоты можно использовать ключи GPG. Если у вас нет такого ключа или вы не хотите использовать свой, выполните gpg --quick-generate-key me@example.com, чтобы создать ключ GPG для идентификатора пользователя me@example.com. Вам понадобится его 40-значный шестнадцатеричный идентификатор ключа, который будет напечатан в конце. Сначала создайте файл .sops.yaml в дереве каталогов, с которым вы работаете:

creation_rules:
  - pgp: 'FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4'

Здесь FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4 — это 40-значный шестнадцатеричный идентификатор ключа. С помощью этого файла можно создать зашифрованный SOPS файл, выполнив следующую команду в каталоге, где находится .sops.yaml, или в его подкаталоге:

$ sops test.sops.yaml

Откроется окно редактора с примером файла YAML. Вставьте следующее содержимое:

# This is a comment
hello: world
foo:
  - bar
  - baz

После закрытия редактора SOPS создаст test.sops.yaml с зашифрованным содержимым:

#ENC[AES256_GCM,data:r6Ok05DzzHBO4tonlz2t49CF,iv:Y0P39iXwaGYU9NG5oRC3NuaGVL40uruSze0CxbDTpTk=,tag:EzoG+X+BJAHbxE0asSyGlQ==,type:comment]
hello: ENC[AES256_GCM,data:onBZqWk=,iv:bwj4bwaeh3vpVDYqY2AnYo1thF955i5vbFpCC1DwJtM=,tag:4qbVzuHTaPrXm64r2Rqz1Q==,type:str]
foo:
    - ENC[AES256_GCM,data:UsY8,iv:USv71rKfvbTF+3a5T2WO56wGVu609/0uigqkO0pa6U4=,tag:s8NdqLp+8OOQg4xDfE78oA==,type:str]
    - ENC[AES256_GCM,data:Dhmo,iv:qWs5gN2SCXYq0EfGelZhODsdViKB9w2taQMhsqy0D2g=,tag:I+ZFvuxnsvQmywqz+a/M9w==,type:str]
sops:
    kms: []
    gcp_kms: []
    azure_kv: []
    hc_vault: []
    age: []
    lastmodified: "2021-06-15T19:36:34Z"
    mac: ENC[AES256_GCM,data:HAvLeOvt7xWI7B5TCeDEsL6sOSzGGeTbgBSJaZkwadmoAm3Ny4IZPF8JAbFaPPLmN8FJVAt4D61aIWa6Xwi3xMj1g6DmxFfgK6JFJqWqW122UlMhqZ/WuMWFV6yVxpTLDXgemndgGDJqUTUi14FMh/MzPDg4f6kFP64kA9fpLrY=,iv:LdhswnMymZG8J9na/jnF3WYnX0DvzvoBlvjUCu4nI6c=,tag:Qt4d7L3FXsgfmg9iOs8P4A==,type:str]
    pgp:
        - created_at: "2021-06-15T19:36:01Z"
          enc: |-
            -----BEGIN PGP MESSAGE-----

            wcBMAyUpShfNkFB/AQgAT8OAKnWLBQRG3kT5lZCmyoPzK6RwF0zRkwCzJkLNl6xg
            nQjUjpD03ZD4FtiRidspXEj7NvCLDghJ0UETtDjmrwsTeJ5YAK/JxouWmoNhVVdF
            p0qOlj/THXIV+ypVaqrisZGZiTqeWjUNFuayknvjm3XduOOPZA1MIJ14pQxcgca4
            NWmKwPwXTWEy3RJ0ZsnjjjYvKHjHyvbHdbDgARu8R1jEgdNPKPBRVpEY6RNeafXI
            gFBVRfrhPKD6HmnmNvjHwUc/K+wOa1ciIYVrT4mPXoyBsFkyV0egh/QRf0JO8+X7
            Ut/jEtCrl9BXJCNYGmC5EU3PPiFlAu1MRxlCiPNWltLmASn2w62wMpgih6f+OpI/
            zyEOdz0qx80LEfhv3+jBbDfBwz4GqpAHUr0fCXDzeDiKfzlU6isagoIAhJfwX6oG
            NeQ47ktk1XhPmgIwxxuvonG14iQoU2cA
            =GoXQ
            -----END PGP MESSAGE-----
          fp: FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4
    unencrypted_suffix: _unencrypted
    version: 3.7.1

Первая строка содержит зашифрованное содержимое. Вторая строка содержит незашифрованный ключ hello: world и зашифрованное строковое значение world. В следующих нескольких строках содержится незашифрованный ключ foo с зашифрованными элементами списка.

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

Работа с зашифрованными файлами

Расшифровывать файлы, зашифрованные с помощью SOPS, можно с помощью плагина поиска community.sops.sops, а динамически шифровать данные — с помощью модуля community.sops.sops_encrypt. Возможность шифровать данные полезна при создании или обновлении секретов в плейбуках Ansible.

Предположим, у вас есть зашифрованный закрытый ключ keys/private_key.pem.sops, который до шифрования с помощью SOPS был в формате PEM:

$ openssl genrsa -out keys/private_key.pem 2048
$ sops --encrypt keys/private_key.pem > keys/private_key.pem.sops
$ wipe keys/private_key.pem

Чтобы использовать его в плейбуке, например передать модулю community.crypto.openssl_csr для создания запроса на подпись сертификата (CSR), можно загрузить его с помощью плагина поиска community.sops.sops:

---
- name: Load SOPS-encrypted private key
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Create CSR with encrypted private key
      community.crypto.openssl_csr:
        # The private key is provided with SOPS:
        privatekey_content: "{{ lookup('community.sops.sops', 'keys/private_key.pem.sops') }}"
        # Store the CSR on disk unencrypted:
        path: ansible.com.csr
        # This is going to be a CSR for ansible.com and www.ansible.com
        subject_alt_name:
          - DNS:ansible.com
          - DNS:www.ansible.com
        use_common_name_for_san: false

В результате получится следующий вывод:

PLAY [Load SOPS-encrypted private key] ***************************************************************************

TASK [Create CSR with encrypted private key] *********************************************************************
ok: [localhost]

PLAY RECAP *******************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

После этого у вас будет CSR ansible.com.csr для зашифрованного закрытого ключа keys/private_key.pem.sops.

Если вы хотите использовать Ansible для создания (или обновления) зашифрованного закрытого ключа, воспользуйтесь модулем community.crypto.openssl_privatekey_pipe для создания (или обновления) закрытого ключа, а затем модулем community.sops.sops_encrypt, чтобы записать его на диск в зашифрованном виде:

---
- name: Create SOPS-encrypted private key
  hosts: localhost
  gather_facts: false
  tasks:
    - block:
        - name: Create private key
          community.crypto.openssl_privatekey_pipe:
            size: 2048
          no_log: true  # Always use this with openssl_privatekey_pipe!
          register: private_key

        - name: Write encrypted key to disk
          community.sops.sops_encrypt:
            path: keys/private_key.pem.sops
            content_text: "{{ private_key.privatekey }}"

      always:
        - name: Wipe private key from Ansible's facts
          # This is particularly important if the playbook doesn't end here!
          set_fact:
            private_key: ''

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

---
- name: Create SOPS-encrypted private key
  hosts: localhost
  gather_facts: false
  tasks:
    - block:
        - name: Create private key
          community.crypto.openssl_privatekey_pipe:
            size: 2048
            content: >-
              {{ lookup(
                    'community.sops.sops',
                    'keys/private_key.pem.sops',
                    empty_on_not_exist=true
                 ) }}
          no_log: true  # Always use this with openssl_privatekey_pipe!
          register: private_key

        - name: Write encrypted key to disk
          community.sops.sops_encrypt:
            path: keys/private_key.pem.sops
            content_text: "{{ private_key.privatekey }}"
          when: private_key is changed

      always:
        - name: Wipe private key from Ansible's facts
          # This is particularly important if the playbook doesn't end here!
          set_fact:
            private_key: ''

Флаг empty_on_not_exist=true нужен, чтобы предотвратить ошибку поиска, если ключ еще не существует. При повторном запуске этого плейбука вывод будет следующим:

PLAY [Create SOPS-encrypted private key] *************************************************************************

TASK [Create private key] ****************************************************************************************
ok: [localhost]

TASK [Write encrypted key to disk] *******************************************************************************
skipping: [localhost]

TASK [Wipe private key from Ansible's facts] *********************************************************************
ok: [localhost]

PLAY RECAP *******************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

Работа с зашифрованными данными из других источников

Для расшифровки произвольных данных можно использовать фильтр Jinja2 community.sops.decrypt. Это могут быть данные, ранее прочитанные из файла, возвращенные действием или полученные каким-либо иным способом.

Например, предположим, что вы хотите расшифровать файл, полученный с HTTPS-сервера с помощью модуля ansible.builtin.uri. Чтобы использовать поиск community.sops.sops, сначала нужно записать файл на диск. С помощью фильтра можно расшифровать его напрямую:

---
- name: Decrypt file fetched from URL
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Fetch file from URL
      ansible.builtin.uri:
        url: https://raw.githubusercontent.com/getsops/sops/master/functional-tests/res/comments.enc.yaml
        return_content: true
      register: encrypted_content

    - name: Show encrypted data
      debug:
        msg: "{{ encrypted_content.content | ansible.builtin.from_yaml }}"

    - name: Decrypt data and decode decrypted YAML
      set_fact:
        decrypted_data: "{{ encrypted_content.content | community.sops.decrypt | ansible.builtin.from_yaml }}"

    - name: Show decrypted data
      debug:
        msg: "{{ decrypted_data }}"

Результат будет следующим:

PLAY [Decrypt file fetched from URL] *****************************************************************************

TASK [Fetch file from URL] ***************************************************************************************
ok: [localhost]

TASK [Show encrypted data] ***************************************************************************************
ok: [localhost] => {
    "msg": {
        "dolor": "ENC[AES256_GCM,data:IgvT,iv:wtPNYbDTARFE810PH6ldOLzCDcAjkB/dzPsZjpgHcko=,tag:zwE8P+AwO1hrHkgF6pTbZw==,type:str]",
        "lorem": "ENC[AES256_GCM,data:PhmSdTs=,iv:J5ugEWq6RfyNx+5zDXvcTdoQ18YYZkqesDED7LNzou4=,tag:0Qrom6J6aUnZMZzGz5XCxw==,type:str]",
        "sops": {
            "age": [],
            "azure_kv": [],
            "gcp_kms": [],
            "hc_vault": [],
            "kms": [],
            "lastmodified": "2020-10-07T15:49:13Z",
            "mac": "ENC[AES256_GCM,data:2dhyKdHYSynjXPwYrn9356wA7vRKw+T5qwBenI2vZrgthpQBOCQG4M6f7eeH3VLTxB4mN4CAchb25dsNRoGr6A38VruaSSAhPco3Rh4AlvKSvXuhgRnzZvNxE/bnHX1D4K5cdTb4FsJg/Ue1l7UcWrlrv1s3H3SwLHP/nf+suD0=,iv:6xBYURjjaQzlUOKOrs2NWOChiNFZVAGPJZQZ59MwX3o=,tag:uXD5VYme+c8eHcCc5TD2YA==,type:str]",
            "pgp": [
                {
                    "created_at": "2019-08-29T21:52:32Z",
                    "enc": "-----BEGIN PGP MESSAGE-----\n\nhQEMAyUpShfNkFB/AQgAlvpTj0NYqF4mQyIeM7wX2SHLb4U07/flpqDpp2W/30Pz\nAHA7sYrgP0l8BrjT2kwtgCN0cdfoIHJudezrNjANp2P5TbP2b9kYYNxpehzB9PFj\nFixnCS7Zp8WIt1yXr1TX+ANZoXLopVcRbMaQ5OdH7CN1pNQtMR+R3FR3X/IqKxiU\nDo1YLaooRJICUC8LJw2Tb4K+lYnTSqd/HalLGym++ivFvdDB1Ya1GhT1FswXidXK\nIRjsOVbxV0q5VeNOR0zxsheOvuHyCje16c7NXJtATJVWtTFABJB8u7CY5HhZSgq+\nrXJHyLHqVLzJ8E4WqHQkMNUlVcrqAz7glZ6xbAhfI9JeAYk5SuBOQOQ4yvASqH4K\nb0N3+/abluBY7YPqKuRZBiEtmcYlZ+zIHuOTP1rD/7L5VY8CwE5U8SFlEqwM7nQJ\n6/vtl6qngOFjwt34WrhZzUfLPB/wRV/m1Qv2kr0RNA==\n=Ykiw\n-----END PGP MESSAGE-----\n",
                    "fp": "FBC7B9E2A4F9289AC0C1D4843D16CEE4A27381B4"
                }
            ],
            "unencrypted_suffix": "_unencrypted",
            "version": "3.6.1"
        }
    }
}

TASK [Decrypt data] **********************************************************************************************
ok: [localhost]

TASK [Show decrypted data] ***************************************************************************************
ok: [localhost] => {
    "msg": {
        "dolor": "sit",
        "lorem": "ipsum"
    }
}

PLAY RECAP *******************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Обратите внимание: если поместить выражение Jinja2 в переменную, оно будет вычисляться при каждом использовании. Расшифровка данных занимает некоторое время. Если выражение нужно использовать несколько раз, лучше сначала сохранить его вычисленное значение как факт с помощью ansible.bulitin.set_fact. Это может быть важно, если расшифрованные данные нужно передать роли

---
- name: Decrypt file fetched from URL
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Fetch file from URL
      ansible.builtin.uri:
        url: https://raw.githubusercontent.com/getsops/sops/master/functional-tests/res/comments.enc.yaml
        return_content: true
      register: encrypted_content

    # BAD: every time the role uses decrypted_data, the data will be decrypted!

    - name: Call role with decrypted data
      include_role:
        name: myrole
      vars:
        role_parameter: "{{ encrypted_content.content | community.sops.decrypt | ansible.builtin.from_yaml }}"

    # GOOD: the data is decrypted once before the role is called,

    - name: Store decrypted data as fact
      set_fact:
        decrypted_data: "{{ encrypted_content.content | community.sops.decrypt | ansible.builtin.from_yaml }}"

    - name: Call role with decrypted data
      include_role:
        name: myrole
      vars:
        role_parameter: "{{ decrypted_data }}"

Работа с зашифрованными переменными

Загружать зашифрованные переменные можно так же, как и с помощью плагина переменных ansible.builtin.host_group_vars, используя плагин переменных community.sops.sops. Если требуется динамически загружать переменные, как с помощью действия ansible.builtin.include_vars, можно использовать действие community.sops.load_vars.

Чтобы использовать плагин переменных, включите его в файл конфигурации Ansible (ansible.cfg):

[defaults]
vars_plugins_enabled = host_group_vars,community.sops.sops

Дополнительные сведения о включении плагинов переменных см. в разделе VARIABLE_PLUGINS_ENABLED. Затем можно поместить файлы со следующими расширениями в каталоги group_vars и host_vars:

  • .sops.yaml
  • .sops.yml
  • .sops.json

(Список расширений можно изменить с помощью valid_extensions.) Плагин переменных расшифрует эти файлы, и вы сможете напрямую использовать их незашифрованное содержимое.

Если нужно динамически загружать зашифрованные переменные, как с помощью встроенного действия ansible.builtin.include_vars, можно использовать действие community.sops.load_vars. Обратите внимание, что оно не является полноценной заменой: встроенное действие использует некоторые специальные случаи, жестко заданные в ansible-core, что позволяет загружать переменные именно как переменные (точнее, как «небезопасные» выражения Jinja2, которые автоматически вычисляются при использовании). Другие плагины действий, например community.sops.load_vars, так делать не могут и должны загружать переменные как факты.

Это имеет значение главным образом при использовании выражений Jinja2 в файле зашифрованных переменных. Когда ansible.builtin.include_vars загружает файл переменных с выражениями, эти выражения вычисляются только тогда, когда требуется вычислить определяющую их переменную (ленивое вычисление). Поскольку community.sops.load_vars возвращает факты, он должен вычислять выражения непосредственно во время загрузки. (Для этого установите параметр expressions в значение evaluate-on-load.) Это особенно важно, если нужно ссылаться на другие переменные из того же файла: это не сработает, поскольку во время вычисления первой переменной Ansible еще не знает об остальных. Ansible «узнает» о них как о фактах только после вычисления всех переменных и завершения действия.

Для следующего примера предположим, что у вас есть зашифрованный файл keys/credentials.sops.yml, который после расшифровки содержит:

encrypted_password: foo
expression: "{{ inventory_hostname }}"

Рассмотрим следующий плейбук:

---
- name: Create SOPS-encrypted private key
  hosts: localhost
  gather_facts: false
  tasks:
    - name: Load encrypted credentials
      community.sops.load_vars:
        file: keys/credentials.sops.yml
        expressions: ignore  # explicitly do not evaluate expressions
                             # on load (this is the default)

    - name: Show password
      debug:
        msg: "The password is {{ encrypted_password }}"

    - name: Show expression
      debug:
        msg: "The expression is {{ expression }}"

При запуске будет получен следующий результат:

PLAY [Create SOPS-encrypted private key] *************************************************************************

TASK [Load encrypted credentials] ********************************************************************************
ok: [localhost]

TASK [Show password] *********************************************************************************************
ok: [localhost] => {
    "msg": "The password is foo"
}

TASK [Show expression] *******************************************************************************************
ok: [localhost] => {
    "msg": "The expression is {{ inventory_hostname }}"
}

PLAY RECAP *******************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Если изменить задачу загрузки переменных следующим образом:

- name: Load encrypted credentials
  community.sops.load_vars:
    file: keys/credentials.sops.yml
    expressions: evaluate-on-load

Теперь последняя задача покажет вычисленное выражение:

TASK [Show expression] *******************************************************************************************
ok: [localhost] => {
    "msg": "The expression is localhost"
}

© 2012–2018 Michael DeHaan
© 2018–2025 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/projects/ansible/latest/collections/community/sops/docsite/guide.html

Spec-Zone.ru

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