Перейти к основному содержимому
Версия: 0.2.0

Pillars

Термины

  • Конвертное шифрование (англ. Envelope Encryption) — это метод защиты данных, при котором информация шифруется уникальным симметричным ключом данных (DEK), а сам этот ключ затем шифруется другим корневым ключом (KEK или CMK). Это позволяет безопасно хранить зашифрованные данные рядом с зашифрованным ключом, обеспечивая возможность ротации корневого ключа без перешифрования самих данных, высокую производительность шифрования данных и централизованное управление безопасностью.
  • Окружение pillarenv — строковый идентификатор окружения/области применения pillar. В коде это не отдельная сущность, а ключ, по которому делается выборка из БД MongoDB.
  • Слияние Pillars — это механизм объединения данных Pillar из разных SLS-файлов, окружений и источников в одну итоговую структуру, с применением заданной стратегии разрешения конфликтов ключей.
  • Шифрование данных at rest — шифрование данных «в покое» — это метод защиты информации, когда она физически хранится на носителях: дисках, SSD, томах виртуальных машин, объектных или блочных хранилищах. Основная цель — исключить возможность несанкционированного доступа к данным, даже если злоумышленник получит прямой доступ к устройствам хранения.
  • Эндпоинт (от англ. endpoint — точка подключения) — конкретный сетевой адрес, URL, который определяет точку доступа к ресурсу или услуге на сервере.
  • AAD (англ. Additional Authenticated Data) — аутентифицированный контекст — это открытые (незашифрованные) данные, передаваемые вместе с зашифрованным сообщением, которые аутентифицируются вместе с ним для обеспечения целостности.
  • Ciphertext(также Шифротекст) — результат преобразования обычных данных (открытого текста) с помощью алгоритма шифрования и ключа.
  • Nonce (сокращение от англ. "number used once" — число, используемое один раз) — это случайное или псевдослучайное число, применяемое в криптографии только один раз для обеспечения безопасности.
  • Pillar (также данные Pillar, мн. ч. Pillars) — запись с именем (name) и значением (value), привязанная к некоторой цели (tgt_type/tgt_id) и окружению pillarenv.

Хранилище Pillars — БД MongoDB, коллекция pillars (уникальность обеспечивается составным индексом по полям (tgt_type, tgt_id, name, pillarenv)).

Типы целей (tgt_type)

В исходном коде Программного комплекса определены 5 типов целей (PillarTgtType):

  1. root

    • Назначение: глобальные (базовые) Pillars.
    • Где используются: как «база» при слиянии Pillars для миньона.
  2. collection (не используется в текущей версии!)

    • Назначение: Pillars для коллекций клиентов (minion collections), с поддержкой наследования по дереву коллекций.
    • Где используются: при слиянии Pillars, если в запросе pillarenv передан ключ со значением collection: ....
  3. minion

    • Назначение: Pillars, специфичные для конкретного клиента.
    • Где используются: при слиянии Pillars для клиента, как переопределение (override) значений окружения base.
      Имеют самый высокий приоритет при базовом слиянии Pillars.
  4. task_tpl

    • Назначение: значения по умолчанию (defaults) для шаблонов задач.
    • Где используются: эндпоинт GET /tasks/template/{tpl_id}/with-defaults подмешивает эти значения в JSON Schema (как default).
  5. task

    • Назначение: Pillars, привязанные к конкретной задаче (runtime-параметры).
    • Где используются: создаются при создании/запуске задачи Salt.Box (см. PillarService.create_for_task).

Где и как создаются Pillars

HTTP API

  • Эндпоинт POST /pillars создаёт Pillar. Внутри Core:

    • валидирует created_by,
    • вычисляет tgt_id и перезаписывает pillarenv согласно tgt_type (см. ниже),
    • при is_secret=true производит зашифрование значения «на запись» (Encryption at rest).
  • Эндпоинт PUT /pillars/{pid} обновляет Pillar с учётом важного ограничения:

    • если Pillar секретный и уже хранится в зашифрованном виде, обновление значения ЗАПРЕЩЕНО.
  • Эндпоинт POST /pillars/list возвращает только tgt_type in {minion, root} (такой фильтр стоит прямо в роутере):
    • секретные значения в списках маскируются как "*******".

Автоматическое создание Pillars для задач

Метод PillarService.create_for_task() создаёт набор Pillars типа task и (опционально) сохраняет их как значения по умолчанию для task_tpl.

Как формируется окружение pillarenv

Окружение pillarenv может:

  • создаваться как одиночное значение (например, base),
  • при запросе с мастер-сервера может быть списком окружений, разделённых запятыми: env1,env2,... — это важно для слияния Pillars.

Canonical-форматы окружений pillarenv, которые создаёт модуль Core

  1. Root (tgt_type=root)

    • pillarenv = "base"
  2. Minion (tgt_type=minion)

    • pillarenv = "minion_id:{minion_id};master:{master}"
  3. Collection (tgt_type=collection)

    • pillarenv = "collection:{collection_object_id};user:{user_sub}"
  4. Task (tgt_type=task)

    • pillarenv = "task:{task_id}"
  5. Task template (tgt_type=task_tpl)

    • pillarenv = "task_tpl:{task_template_id};user:{user_sub}"

Также есть утилитарный метод чтения get_for_target(tgt_type, tgt_id, user_sub), который читает ровно один pillarenv вида:

  • "{tgt_type}:{tgt_id};user:{user_sub}" (если user_sub задан)
  • "{tgt_type}:{tgt_id}" (если user_sub не задан)

Слияние Pillars при запросе от мастер-сервера

Откуда приходит запрос

Мастер-сервер запрашивает Pillars для клиента через Redis bus:

  • Модуль Core слушает событие master_get_pillar_data и получает BridgePillarDataRequest.
  • В обработчике вызывается метод PillarService.get_merged(master, minion_id, pillarenv).

Входные данные

  • Окружение pillarenv имеет строковый тип (может являться списком через запятую). Примеры: "base", "base,collection:<id>;user:<sub>".

Алгоритм слияния Pillars

  1. pillarenv разбивается по разделителю , (запятой) на список envs.
  2. merge_result — обычный словарь (dict): { name: value }.
  3. Для каждого env по порядку выполняется алгоритм:
    • Если env начинается с "collection:", то модуль Core:
      • пытается выполнить парсинг collection_id и (опционально) user_sub из строки,
      • получает список branch_ids = get_ancestors_ids(target=collection_id, include_self=True),
      • для каждого collection_id из branch_ids читает Pillars по pillarenv:
        • "collection:{collection_id};user:{user_sub}" (если user_sub присутствует)
        • иначе "collection:{collection_id}"
      • и выполняет слияние merge_result.update(...).
    • Затем (в любом случае) Core читает pillars по pillarenv == env и снова делает merge_result.update(...).
  4. После обработки всех env применяется специальное правило приоритета:
    • если среди env присутствует "base", то модуль Core дополнительно читает Pillars для клиента:
      • pillarenv = "minion_id:{minion_id};master:{master}"
    • и применяет их последними (то есть они имеют самый высокий приоритет).

Правила приоритета

  • merge_result.update() означает: если два Pillar имеют одинаковый name, то значение, применённое позже, перезапишет предыдущее.
  • Итоговый приоритет задаётся порядком env в строке pillarenv и порядком branch_ids, который возвращает репозиторий коллекций.
  • Переопределение, специфичное для клиента (minion-specific override) применяется последним только в случае, когда в запросе указано окружение base.

Что возвращается мастеру

  • Возвращается словарь (dict) { pillar_name: pillar_value }.
  • Если pillar помечен is_secret=true, модуль Core выполняет расшифрование значения перед возвратом.
    (см. доку по шифрованию)

Примеры

  1. pillarenv = "base"

    • берутся данные Pillars из окружения base
    • затем производится переопределение данными Pillars из окружения minion_id:{minion_id};master:{master}
  2. pillarenv = "base,collection:<cid>;user:<sub>"

    • берутся данные Pillars из окружения base
    • затем производится переопределение данными Pillars из набора окружений коллекций по ветке (родительские (ancestors) + собственные (self))
    • затем pillars из collection:<cid>;user:<sub> (как указан в env)
    • затем производится переопределение, специфичное для клиента (minion-specific override), т. к. в запросе указано окружение base.

Секретные Pillars

Секретный статус данных Pillar устанавливается флагом is_secret.

Секретные данные Pillar:

  • зашифровываются «на запись» (Encryption at rest);
  • в списковых ответах маскируются как "*******";
  • при слиянии Pillars для мастер-сервера/клиента расшифровываются перед возвратом.

Когда применяется шифрование

  • Шифрование выполняется при создании Pillar, если is_secret=true.
  • Если значение уже имеет формат зашифрованного JSON-payload (маркер $enc), повторное шифрование не производится.

Конфигурация ключей

Шифрование зависит от настроек приложения:

  • pillars_kek_b64 — base64-строка, содержащая ровно 32 байта ключа (KEK) для AES-256-GCM.
  • pillars_kid — строковый идентификатор ключа (Key ID) для контроля/ротации (по умолчанию core-kek-v1).

Если pillars_kek_b64 не задан, любые попытки зашифровать/расшифровать секретный pillar приводят к ошибке PillarEncryptionKeyNotConfiguredException (HTTP 500).

Рекомендованный способ сгенерировать ключ (пример):

python -c "import os,base64; print(base64.b64encode(os.urandom(32)).decode())"

Описание процесса шифрования

Используется «конвертное» шифрование (Envelope Encryption):

  1. На каждый секретный Pillar генерируется случайный DEK (Data Encryption Key) длиной 32 байта.
  2. DEK шифрует данные (алгоритм AES-256-GCM).
  3. DEK «оборачивается» (wrap) постоянным KEK (Key Encryption Key) также через AES-256-GCM.

Таким образом, KEK не шифрует полезные данные напрямую — он шифрует только DEK.

Канонизация данных

Перед шифрованием данные Pillar приводятся к унифицированному представлению:

  • производится сериализация JSON с параметрами:
    • sort_keys=true (принудительная сортировка ключей в JSON-объектах по алфавиту),
    • separators=(",", ":") (явное определение символов-разделителей между элементами (запятая) и между ключами/значениями (двоеточие), позволяющее компактифицировать JSON-строки),
    • ensure_ascii=false (сохранение символов Unicode (например, кириллицы) в оригинальном виде, без их экранирования);
  • производится перевод в кодировку UTF-8.

Канонизация данных важна для тождественности Pillars с одинаковыми полями и для корректности расшифрования.

AAD: привязка к контексту Pillar

Для AES-GCM используется AAD (Additional Authenticated Data) — аутентифицированный контекст, который не шифруется, но влияет на проверку целостности.

AAD для данных строится так:

  • "{tgt_type}:{tgt_id}:{pillarenv}:{name}",
    где pillarenv и tgt_id берутся уже после разрешения цели
    (см. PillarService._resolve_target).

Если любой из этих атрибутов изменится (например, переименование name или изменение pillarenv), расшифрование не пройдёт из-за неуспешной GCM-аутентификации.

Для «обёртки» DEK используется отдельное пространство AAD:

  • "pillars-dek:{kid}:{data_aad}"

Формат хранения Pillar в базе данных

Зашифрованное значение хранится как JSON-объект с маркером $enc:

{
"$enc": {
"v": 1,
"alg": "A256GCM",
"kid": "core-kek-v1",
"nonce_b64": "...",
"ct_b64": "...",
"dek_wrap_alg": "A256GCM",
"dek_wrap_nonce_b64": "...",
"dek_wrapped_b64": "..."
}
}

Поля:

  • v — версия формата (сейчас 1).
  • kid — идентификатор ключа (должен совпадать с текущим pillars_kid).
  • nonce_b64 — 12-байтный nonce для шифрования данных (base64).
  • ct_b64ciphertext для данных (base64).
    В реализации cryptography это ciphertext || tag.
  • dek_wrap_nonce_b64 — 12-байтный nonce для обёртки DEK (base64).
  • dek_wrapped_b64 — зашифрованный DEK (base64).
Все base64 — стандартный Base64 (RFC 4648) с валидацией (validate=True).

Процесс зашифрования (создание секретного Pillar)

  1. Формируется aad по контексту Pillar.
  2. Генерируется DEK dek = os.urandom(32).
  3. Данные зашифровываются AESGCM(dek) с data_nonce = os.urandom(12) и AAD=aad.
  4. DEK зашифровываtтся KEK AESGCM(kek) с wrap_nonce = os.urandom(12) и AAD=pillars-dek:{kid}:{aad}.
  5. Результат сохраняется как $enc JSON-payload (см. Формат хранения Pillar).

Процесс расшифрования

  1. Проверяется, что значение имеет структуру { "$enc": { ... } }.
  2. Проверяются v и kid:
    • если kid не совпадает с текущим pillars_kid, расшифрование отклоняется.
  3. DEK извлекается расшифрованием dek_wrapped_b64 через KEK и wrap-AAD.
  4. Полезные данные расшифровываются через DEK и AAD=aad.
  5. Plaintext распознаётся как JSON и возвращается как JsonValue.