Привет, Хабр! Меня зовут Владимир Харчиков, я развиваю и сопровождаю Platform V Pangolin в СберТехе. Pangolin ― реляционная СУБД, созданная нами для хранения и обработки данных в высоконагруженных приложениях.
В статье я расскажу, как объединить высокую скорость обработки транзакций и безопасность хранения данных, а именно о реализации функции прозрачного шифрования данных внутри нашей СУБД. Кому эта тема интересна ― прошу под кат.
Немного про Platform V Pangolin
Когда Сбер несколько лет назад начал процесс импортозамещения и постепенного отказа от решений западных вендоров, перед нами стояла задача создать собственное ПО, которое будет соответствовать требованиям крупнейшего банка страны к функциональности СУБД, её производительности, безопасности, отказоустойчивости.
Так появилась Platform V Pangolin, реляционная СУБД, основанная на наших собственных инновационных разработках и хорошо зарекомендовавшем себя открытом ПО (PostgreSQL).
Что такое TDE и зачем оно понадобилось
Transparent Data Encryption (TDE) – тип шифрования данных на диске, при котором данные не шифруются ни при передаче, ни в памяти.
TDE — важная фича, особенно для финансовых организаций, которым приходится иметь дело с различными стандартами безопасности.
Прозрачное шифрование защищает данные от раскрытия при хищении дисков или файлов с данными СУБД, в том числе резервных копий. А ещё ограждает от подмены данных через их изменение в файлах: правильного шифрования данных без ключа всё равно не получится, одна только порча.
В общем, стало понятно, что для Pangolin просто необходимо прозрачное шифрование, которое бы отвечало высоким требованиям корпораций, а по скорости и надёжности не уступало бы аналогичным промышленным базам данных.
Какие есть альтернативы
Конечно, TDE — не единственный способ шифрования данных. Есть и другие варианты. Например, шифрование отдельных колонок на стороне пользователя, где все сопутствующие процедуры должны быть реализованы в приложении.
Но такой подход осложняет работу: пользовательская сторона сама должна выполнять операции, связанные с шифрованием и расшифровкой данных, управлением ключами шифрования, их ротацией. А ещё при таком шифровании нарушается порядок сортировки, что делает невозможным RANGE SCAN по шифрованным индексам.
Ещё один способ — шифрование файловой системы. Минус в том, что при монтировании зашифрованной системы нужно вручную вводить секрет с помощью внешних средств, так как экземпляр базы данных к моменту монтирования ещё не запущен.
Требования к TDE для промышленной базы данных
Если коротко, то мы сформулировали требования к прозрачному шифрованию так:
минимальное влияние на производительность;
минимальный риск расшифровки данных с использованием знаний об их специфике — например, структуре страниц PostgreSQL;
минимум дополнительных ручных действий при сопровождении БД: в частности, при запуске экземпляра не должно быть ручного ввода секретов;
ограничение «зоны поражения» при краже ключа. То есть ключи должны ротироваться;
отсутствие риска недоступности данных из-за ротации ключей;
поддержка возможности восстановления из резервной копии, сделанной до ротации ключей;
возможность добавлять алгоритмы шифрования без перевыпуска всего продукта.
Реализация: особенности, сложности, подходы
С самого начала было понятно, что реализовать шифрование как расширение PostgreSQL не получится: нужно менять поведение системы в точках, не имеющих необходимых хуков.
Чтобы повысить надёжность защиты, помимо шифрования страниц с данными решили реализовать шифрование в следующих объектах:
файлы данных отношений, индексов, материализованных представлений. Шифруются только main- и init-форки, а вот visibility map и free space map не шифруются;
файлы данных временных таблиц;
временные файлы, создаваемые операциями сортировок и объединений при выходе за пределы, заданные параметром work_mem;
временные файлы, создаваемые при логическом декодировании WAL-записей во время логической репликации;
временные файлы хранения данных изменений для транзакций (update, insert, delete, truncate);
файлы WAL;
данные, передаваемые в рамках потоковой репликации;
данные, передаваемые в рамках логической репликации;
файлы данных в составе резервных копий.
Если данные реплицируются между экземплярами БД, то все экземпляры, участвующие в репликации, должны уметь шифровать данные и иметь доступ к одним и тем же ключам шифрования. Это позволит исключить сценарий кражи данных через репликацию на неавторизованный экземпляр БД. Сценарий выглядит немного параноидально, но он необходим, так как TDE участвует в решении более глобальной проблемы — защите пользовательских данных от привилегированных пользователей, включая администраторов СУБД.
Также некоторые утилиты PostgreSQL работают с данными в WAL напрямую, без обращения к API самой СУБД. Все эти утилиты пришлось доработать:
pg_probackup — утилита резервного копирования, проверяет журнал на корректность и составляет карту затронутых изменениями блоков для инкрементального копирования;
pg_rewind — утилита синхронизации «убежавшей» реплики с основной базой данных, анализирует журнал и находит точку расхождения между синхронизируемыми базами;
pg_waldump — разбирает журнал для отображения в читаемом виде;
pg_recvlogical — получает данные для логической репликации и требует доступ к пакетам CopyData в открытом виде.
Все эти утилиты мы «обучили» расшифровке журналов при чтении. Утилита pg_resetwal изменяет журнал, поэтому её пришлось «обучать» и расшифровке, и шифрованию. Утилиты pg_basebackup и pg_receivewal не модифицировали, так как им не нужен доступ к структуре получаемых данных.
При шифровании используются симметричные алгоритмы, поскольку они быстрее, а асимметрия в данном случае просто не нужна. На первом этапе в качестве провайдера шифрования выступают движки библиотеки OpenSSL и алгоритмы AES CBC/CTR/GCM с длиной ключа 128/192/256 бит (движок builtin, алгоритмы SN_aes_128_*, SN_aes_192_*, SN_aes_256_*).
В тестовых целях сделали интеграцию шифрования по ГОСТ из состава OpenSSL (движок gost, алгоритмы SN_id_Gost28147_89/NID_gost89_cbc, SN_gost89_cnt/NID_gost89_cnt_12). Но использовать её по умолчанию не получилось из-за низкой скорости. В отличие от AES-NI/PadLock, для шифрования по ГОСТ отсутствует аппаратная поддержка.
Поддержка алгоритмов шифрования реализована через механизм динамически подгружаемых библиотек-обёрток над провайдерами шифрования.
Инициализирующие векторы уникальны для каждого куска данных, а алгоритм формирования вектора зависит от типа шифруемых данных. Например, для страниц данных используется информация из заголовка страницы и номер страницы, а для временных данных генерируется случайный вектор, сохраняемый в общей памяти до тех пор, пока есть необходимость доступа к этим данным.
Непосредственно шифрование/дешифрование происходит в точках передачи данных из памяти на диск или в сеть и наоборот.
Для данных отношений.
Основные изменения были сделаны в buffer manager (bufmgr.c) в функциях, выполняющих чтение с диска в буферный кэш (ReadBuffer_common) и запись из буферного кэша на диск (FlushBuffer, FlushRelationBuffers). Модификация именно в этих функциях позволила сократить изменения в коде, так как функциональность, требующая работы с буферным кэшем, использует в конечном счёте именно эти функции для чтения страниц с диска или записи страниц на диск.
Для данных временных таблиц.
Всё почти аналогично данным постоянных отношений. Пришлось изменить только функцию выделения новой страницы буферного кэша LocalBufferAlloc для local buffer manager’а (localbuf.c), так как в ней было реализовано собственное сохранение dirty-страниц при необходимости освобождения памяти в буферном кэше.
Для временных данных, выгружаемых на диск при сортировках и join'ах.
TDE поддерживали в функциях buffile.c, служащих для записи и чтения буферизируемых временных файлов.
Для записей WAL.
Реализовали шифрование в xlog.c в создании нового сегмента WAL и в записи WAL. В процессе walreceiver перешифрование реализовано только при смене временной линии, где меняется инициализирующий вектор шифрования. В остальных случаях приходящие записи складываются в WAL как есть, так как они поступают уже в зашифрованном виде.
Для потоковой репликации.
Изменений нет, записи WAL передаются как есть, так как они уже лежат в зашифрованном виде.
Для логической репликации.
Реализовано шифрование и расшифровка передаваемых данных через заполнение добавленного callback page_cipher в XLogReaderRoutine в slotfuncs.c, walsender.c и logicalfuncs.c.
Управление ключами
Структура ключей сделана двухуровневой. Для каждого объекта — таблицы, индекса, материализованного представления — генерируется новый ключ, который помещается в хранилище ключей (keyring). Хранилище размещается в общей памяти PostgreSQL.
По умолчанию ключи лежат в памяти в открытом виде, чтобы обеспечить требование по производительности. Впрочем, в любом случае нужно контролировать доступ к общей памяти, так как в буферном кэше данные также будут лежать в расшифрованном виде, доступном как расширению pageinspect, так и инструментам типа shmcat. Отмечу, что ключи объектов и данные в shared buffers можно хранить в памяти и зашифрованными, но тогда придётся пожертвовать производительностью.
Ключи шифрования объектов перманентно хранятся в директории global БД в виде файла. Ключи в файле зашифрованы мастер-ключом, содержат указание на версию мастер-ключа и контрольный блок. Он позволяет понять, что для расшифровки объекта использован правильный мастер-ключ. В противном случае при ротации мастер-ключа вместо ключа шифрования объекта можно получить мусор.
Файл с ключами включается в резервную копию БД. Мастер-ключ хранится в хранилище секретов. Там же хранится история изменения мастер-ключей, указание на метку активного мастер-ключа и ключ шифрования реплицируемых данных для потоковой и логической репликации.
Двухуровневая система решает две задачи:
уменьшает объём данных в хранилище секретов;
позволяет не перешифровывать все данные при смене мастер-ключа.
В качестве хранилища секретов используется Hashicorp Vault. Интеграция реализована через поддержку механизма плагинов интеграции с хранилищем секретов, что позволяет при необходимости реализовывать и включать поддержку и других типов хранилищ без необходимости выпускать новую версию продукта.
На скриншоте ниже — пример структуры хранения ключей в Hashicorp Vault:
Здесь:
master_key_value_20220525_113326_194 — параметр, содержащий фактическое значение мастер-ключа: при ротации мастер-ключа создаётся новый параметр с мастер-ключом, но предыдущий ключ не удаляется, так как он может использоваться при восстановлении из резервной копии;
actual_master_key — параметр, содержащий имя параметра с актуальным мастер-ключом;
wal_key — параметр, содержащий ключ шифрования WAL, общий для серверов БД, между которыми выполняется репликация.
Поддерживается ротация мастер-ключей как через функции БД, так и на стороне хранилища секретов.
При ротации на стороне хранилища секретов нужно соблюдать структуру хранения параметров, как показано выше. При этом на стороне СУБД работает фоновый процесс, периодически проверяющий изменение мастер-ключа и выполняющий перешифрование ключей в локальном keyring БД.
Функции для поддержки ротации и изменения мастер-ключей на стороне БД следующие:
block_rotate_master_key() захватывает блокировку изменения мастер-ключа, служит для исключения ротации мастер-ключа TDE на кластере при работе таких утилит, как pg_rewind;
unblock_rotate_master_key() снимает блокировку изменения мастер-ключа на кластере;
set_master_key(new_master_key TEXT) устанавливает новое значение мастер-ключа;
rotate_master_key() генерирует и устанавливает новое значение мастер-ключа;
reencrypt_keys() перешифровывает keyring актуальным мастер-ключом, опираясь на информацию о предыдущем использованном мастер-ключе;
restore_keys() перешифровывает keyring актуальным мастер-ключом, в том числе поддерживает нахождение мастер-ключа из истории в хранилище секретов, которым был зашифрован конкретный ключ шифрования объекта. Используется в том числе в восстановлении резервных копий, снятых во время действия предыдущих мастер-ключей.
В процессе ротации самым сложным было исключить ситуации, при которых изменение мастер-ключей и перешифрование keyring будет конкурировать с использующими их процессами и утилитами.
Задачу решали через добавление rw lock (под капотом — LWLock в shared- или exclusive-режимах, в зависимости от того, кто и что делает с файлом keyring) на чтение и изменение файла keyring. Чтобы предоставить утилите pg_rewind возможность управлять этой блокировкой, установка и снятие были обёрнуты в функции, указанные выше. Блокировка, установленная через функцию block_rotate_master_key(), будет действовать, пока её не снимут или пока будет жив процесс сессии подключения.
Для ключей в keyring ротация пока не реализована, так как смена ключа шифрования тривиальным способом вынуждает провести единовременное перешифрование большого объёма данных с блокировкой доступа к данным на это время. Планируем реализовать функциональность в ближайшее время и рассматриваем два варианта:
Ввести ещё один форк для файлов отношений, лежащий рядом с файлами main-форка. Форк будет предназначен для хранения идентификатора ключа шифрования из keyring, которым зашифрован конкретный блок main-форка отношения.
Использовать свободные биты в поле pd_flags заголовка страницы для указания на какой-либо из N последних ключей из истории ключей в keyring для отношения.
Любой из этих подходов позволит нам проводить отложенное перешифрование отдельных страниц, а не единовременно менять ключ шифрования для всего отношения с полным перешифрованием:
при их подъёме/вытеснении в/из shared buffers;
или фоновым процессом специально для перешифрования отношений в процессе смены ключа шифрования.
Управление шифрованием
Шифруется объект или нет, зависит от того, в каком табличном пространстве он находится. Для создания шифрованных табличных пространств добавлена опция в команду CREATE TABLESPACE:
CREATE TABLESPACE a_table_space_on LOCATION '<директория табличного пространства>' WITH (is_encrypted = on);
Смена значения опции is_encrypted запрещена, так как это потребует шифрования или расшифровки всех данных, хранимых в табличном пространстве;
Создание ключа для шифрованного объекта отражается в WAL для обеспечения целостности и репликации ключа на резервные БД. При логической репликации передача ключей шифрования не поддерживается;
При переносе объекта из шифрованного в нешифрованное табличное пространство выполняется расшифровка данных;
При удалении отношения его ключ помечается на удаление из keyring при коммите соответствующей транзакции. При последующем checkpoint'е происходит удаление помеченных на удаление ключей из keyring, так как размер keyring ограничен.
Для настройки TDE введены дополнительные параметры в конфигурации PostgreSQL:
is_tde_on (on/off, по умолчанию off) — включение шифрования необратимо, так как приводит к шифрованию имеющегося на момент включения и формируемых далее WAL, и обратной процедуры не предусмотрено;
pg_encryption_keys_capacity (32-битное целое, по умолчанию 50000) — размер карты keyring в общей памяти, сюда же входят ключи для toast и WAL.
Интеграция с хранилищем секретов настраивается в 2 этапа:
Выбор плагина для интеграции с соответствующим задаче интерфейсом и создание из libdi-инсталляции символической ссылки с фиксированным именем на библиотеку плагина. Плагин предоставляет функции инициализации плагина, проверки работоспособности, чтения и установки значений параметров, а также функцию для check-and-set-установки значения параметра, если такая возможность поддерживается соответствующим хранилищем. В зоне ответственности плагина находятся все вопросы, касающиеся особенностей взаимодействия с хранилищем секретов, включая аутентификацию, авторизацию и протоколы взаимодействия.
Настройка параметров, специфичных для плагина. Способ настройки зависит от конкретной реализации плагина.
Настройка используемого провайдера шифрования выполняется через указание динамически подгружаемой библиотеки шифрования путём создания символической ссылки с фиксированным именем из lib-директории инсталляции PostgreSQL на требуемую библиотеку. Криптографические плагины предоставляют функции получения размера ключа и инициализирующего вектора, генерации ключа, шифрования и расшифровки заданного блока данных с заданным ключом и вектором инициализации.
Резервное копирование и восстановление
При резервном копировании файлы данных передаются в том виде, в котором хранятся. В резервную копию включается файл keyring, зашифрованный мастер-ключом, актуальным на момент снятия резервной копии. Поднять базу из резервной копии на сервере, не имеющем настроенного доступа к хранилищу секретов с валидными для БД ключами шифрования, не получится.
При запуске экземпляра после восстановления БД из резервной копии будет проведена проверка совпадения метки мастер-ключа, которым был зашифрован keyring на момент снятия резервной копии, с меткой актуального мастер-ключа. При несовпадении меток будет выполняться процедура, реализованная в функции restore_keys (см. выше): она выполняет перешифрование keyring актуальным мастер-ключом. После успешного перешифрования база данных будет открыта.
TDE — это ещё не вся защита данных
Чтобы обеспечить защиту пользовательских данных, в СУБД должна быть реализована возможность разделять привилегии и действия, доступные пользователям с различным уровнем доступа.
Рассмотрим роли привилегированных пользователей, имеющих доступ к данным, и тактику защиты от злоумышленников, обладающих каждой из этих ролей:
Администратор ОС (root) имеет полный доступ к файловой системе сервера, его настройкам и запущенным процессам. Шифрование позволяет спрятать от него содержимое файлов, но полная защита возможна только на уровне операционной системы.
Пользователь ОС с доступом к файловой системе, на которой размещаются файлы БД. Шифрование не даёт ему возможности просматривать данные в файлах.
Администратор базы данных имеет доступ к учётной записи ОС, под которой работает СУБД. Шифрование не позволяет просматривать данные в файлах, но администратор может выполнить массу различных действий с настройками СУБД, сертификатами SSL, настройками методов аутентификации и т. д. В целях безопасности администратора нужно лишить возможности выполнять запросы к защищаемым данным. Это делается при помощи защиты от привилегированных пользователей.
Офицер безопасности распоряжается полномочиями пользователей, ключами шифрования и прочими параметрами, относящимися к безопасности, но не имеет доступа к данным. Так как у офицера безопасности есть доступ к ключам шифрования, он не должен работать с файлами, содержащими данные, которые зашифрованы этими ключами.
Очевидно, что для полной защиты пользовательских данных одного TDE недостаточно. Шифрование не препятствует выполнению авторизованных запросов и не закрывает риски, связанные с тем, что расширения PostgreSQL могут получить доступ к незашифрованным данным в памяти.
Чтобы обеспечить комплексную безопасность данных, в Platform V Pangolin реализована защита от привилегированных пользователей и контроль расширений PostgreSQL, в рамках которого каждое расширение проверяется нашей командой, а двоичный образ подписывается.
Вот, в целом, и всё. Если у вас есть вопросы ― задавайте в комментариях, постараюсь на все ответить. И если есть варианты решения озвученных проблем ― поделитесь собственными кейсами, пожалуйста.