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):
-
root- Назначение: глобальные (базовые) Pillars.
- Где используются: как «база» при слиянии Pillars для миньона.
-
collection(не используется в текущей версии!)- Назначение: Pillars для коллекций клиентов (minion collections), с поддержкой наследования по дереву коллекций.
- Где используются: при слиянии Pillars, если в запросе
pillarenvпередан ключ со значениемcollection: ....
-
minion- Назначение: Pillars, специфичные для конкретного клиента.
- Где используются: при слиянии Pillars для клиента, как переопределение (override) значений окружения
base.
Имеют самый высокий приоритет при базовом слиянии Pillars.
-
task_tpl- Назначение: значения по умолчанию (defaults) для шаблонов задач.
- Где используются: эндпоинт
GET /tasks/template/{tpl_id}/with-defaultsподмешивает эти значения в JSON Schema (какdefault).
-
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
-
Root (
tgt_type=root)pillarenv = "base"
-
Minion (
tgt_type=minion)pillarenv = "minion_id:{minion_id};master:{master}"
-
Collection (
tgt_type=collection)pillarenv = "collection:{collection_object_id};user:{user_sub}"
-
Task (
tgt_type=task)pillarenv = "task:{task_id}"
-
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
pillarenvразбивается по разделителю,(запятой) на списокenvs.merge_result— обычный словарь (dict):{ name: value }.- Для каждого
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(...).
- Если
- После обработки всех
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 выполняет расшифрование значения перед возвратом.
(см. доку по шифрованию)
Примеры
-
pillarenv = "base"- берутся данные Pillars из окружения
base - затем производится переопределение данными Pillars из окружения
minion_id:{minion_id};master:{master}
- берутся данные Pillars из окружения
-
pillarenv = "base,collection:<cid>;user:<sub>"- берутся данные Pillars из окружения
base - затем производится переопределение данными Pillars из набора окружений коллекций по ветке (родительские (ancestors) + собственные (self))
- затем pillars из
collection:<cid>;user:<sub>(как указан вenv) - затем производится переопределение, специфичное для клиента (minion-specific override), т. к. в запросе указано окружение
base.
- берутся данные Pillars из окружения
Секретные 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):
- На каждый секретный Pillar генерируется случайный DEK (Data Encryption Key) длиной 32 байта.
- DEK шифрует данные (алгоритм
AES-256-GCM). - 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_b64— ciphertext для данных (base64).
В реализацииcryptographyэтоciphertext || tag. dek_wrap_nonce_b64— 12-байтный nonce для обёртки DEK (base64).dek_wrapped_b64— зашифрованный DEK (base64).
validate=True).Процесс зашифрования (создание секретного Pillar)
- Формируется
aadпо контексту Pillar. - Генерируется DEK
dek = os.urandom(32). - Данные зашифровываются
AESGCM(dek)сdata_nonce = os.urandom(12)и AAD=aad. - DEK зашифровываtтся KEK
AESGCM(kek)сwrap_nonce = os.urandom(12)и AAD=pillars-dek:{kid}:{aad}. - Результат сохраняется как
$encJSON-payload (см. Формат хранения Pillar).
Процесс расшифрования
- Проверяется, что значение имеет структуру
{ "$enc": { ... } }. - Проверяются
vиkid:- если
kidне совпадает с текущимpillars_kid, расшифрование отклоняется.
- если
- DEK извлекается расшифрованием
dek_wrapped_b64через KEK и wrap-AAD. - Полезные данные расшифровываются через DEK и AAD=
aad. - Plaintext распознаётся как JSON и возвращается как
JsonValue.