Перейти к содержимому

17 Занятие 17: Безопасность / Анонимизация данных

Исходная страница в нативном Markdown: Безопасность / Анонимизация данных
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: pgpro_17_anonymize.html Archive: PGPRO-handouts-16.zip Страниц PDF: 23

Page 1: Безопасность / Анонимизация данных

Авторские права

© Postgres Professional, 2023–2026 Авторы: Алексей Береснев, Илья Баштанов, Павел Толмачев, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Анонимизация
  • Стратегии анонимизации
  • Расширение pgpro_anonymizer
  • Правила маскирования
  • Метки безопасности
  • Динамическое изменение данных
  • Статическое изменение по правилам
  • Безопасность

Page 3: Анонимизация

Source slide page 3: Анонимизация{#source-figure-003 caption=“Source slide page 3: Анонимизация”}

Статическое Динамическое изменение изменение

Комментарии преподавателя

Защитить данные от несанкционированного чтения с помощью разделения доступа между ролями и назначения привилегий не всегда возможно: иногда пользователю необходимо показать какую-то информацию.

Дополнительным средством защиты данных в таких случаях может стать анонимизация — сохранение конфиденциальности путем изменения исходных данных или их представления в измененном виде:

● Статическое изменение — сохранение анонимизированных данных и дальнейшее их предоставление в уже измененном виде.

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

● Динамическое изменение (маскирование) — исходные данные не изменяются, а анонимизация и представление в измененном виде выполняются «на лету», непосредственно при обращении к ним.

Такой вариант может применяться, например, для защиты данных от просмотра недоверенными пользователями в приложениях.

Изменения данных, выполняемые при анонимизации, требуют затрат процессорного времени, места в памяти и, возможно, увеличения числа операций ввода-вывода. Статическое изменение перезаписывает данные на диске и затраты на него единовременны. При динамическом маскировании ресурсы расходуются при каждом обращении.

Page 4: Стратегии анонимизации

Удаление

  • Замена
  • Искажение
  • Рандомизация и подмена
  • Псевдонимизация
  • Обобщение
  • Перестановка
  • Пользовательские правила

Комментарии преподавателя

Для анонимизации данных могут использоваться разные стратегии:

● Удаление (сокрытие) полностью исключает доступ к данным.

● Замена — все значения заменяются одним. Возможна частичная замена, когда часть данных остается нетронутой.

● Искажение (отклонение) «сдвигает» даты и числовые значения.

● Рандомизация и подмена (фальсификация) заменяют конфиденциальные данные случайными, но правдоподобными значениями.

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

● Обобщение заменяет данные диапазоном значений.

● Перестановка перемешивает значения в рамках столбца.

● Пользовательские правила предназначены для анонимизации данных в соответствии с особыми требованиями — например, при необходимости логически согласованных изменений в столбцах.

Выбор стратегии анонимизации зависит от типа данных и задач обеспечения конфиденциальности. Например, для имен и различных идентификаторов удобно использовать замену, рандомизацию или псевдонимизацию, а отклонение и обобщение подходят для чисел и значений даты-времени.

Page 5: pgpro_anonymizer

Инструменты внутри СУБД

  • для статического и динамического изменения
  • Набор функций
    • для разных стратегий анонимизации
  • Декларативные конструкции

Комментарии преподавателя

Расширение pgpro_anonymizer позволяет анонимизировать данные внутри экземпляра СУБД без использования внешних инструментов, что уменьшает риск утечки данных. Расширение поставляется в виде отдельного пакета pgpro-anonymizer-ent-16; за его основу был взят проект PostgreSQL Anonymizer.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#PGPRO-ANON YMIZER

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

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#MASKING-FUN CTIONS

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

Page 6: Статическое изменение с помощью функций

Для работы расширения требуется загрузить разделяемую библиотеку.

=> ALTER SYSTEM SET shared_preload_libraries = 'anon';
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service

Будем работать с демобазой. Подключимся к ней как суперпользователь:

student$ psql demo

Создадим расширение:

=> CREATE EXTENSION anon;
CREATE EXTENSION

Появилась одноименная схема, в которой расширение хранит свои объекты:

=> \dn anon
List of schemas
 Name |  Owner
------+---------
 anon | student
(1 row)

Создадим схему, в которой будем размещать анонимизированные данные:

=> CREATE SCHEMA m_schema;
CREATE SCHEMA

Для удобства временно добавим эту схему в путь поиска:

=> SELECT set_config('search_path', 'm_schema, ' || current_setting('search_path'), false);
         set_config
----------------------------
 m_schema, bookings, public
(1 row)

Считаем, что нам необходимо создать небольшой набор данных для тестировщиков, в котором нужно скрыть имена и фамилии пассажиров и исказить даты и суммы бронирований. Создадим в схеме m_schema таблицы m_tickets и m_bookings, в которые скопируем часть данных из таблиц tickets и bookings схемы bookings:

=> CREATE TABLE m_tickets AS
  SELECT * FROM tickets ORDER BY book_ref LIMIT 5;
SELECT 5
=> CREATE TABLE m_bookings AS
  SELECT * FROM bookings WHERE book_ref IN (SELECT book_ref from m_tickets);
SELECT 4

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

=> SELECT proname FROM pg_proc
WHERE pronamespace = 'anon'::regnamespace AND proname ~ 'fake|random';

Page 7: Page 7

        proname
-----------------------
 random
 random_date_between
 random_date
 random_int_between
 random_bigint_between
 random_string
 random_zip
 random_in_daterange
 random_in_int4range
 random_in_int8range
 random_in_numrange
 random_in_tsrange
 random_in_tstzrange
 random_phone
 random_hash
 random_in
 random_in_enum
 fake_first_name
 fake_last_name
 fake_email
 fake_address
 fake_city
 fake_company
 fake_country
 fake_iban
 fake_postcode
 fake_siret
(27 rows)

Функции подмены случайным образом выбирают значения из таблиц-словарей, также расположенных в схеме anon:

=> \dt anon.*
                List of relations
 Schema |         Name         | Type  |  Owner
--------+----------------------+-------+---------
 anon   | address              | table | student
 anon   | city                 | table | student
 anon   | company              | table | student
 anon   | country              | table | student
 anon   | email                | table | student
 anon   | first_name           | table | student
 anon   | iban                 | table | student
 anon   | identifier           | table | student
 anon   | identifiers_category | table | student
 anon   | last_name            | table | student
 anon   | lorem_ipsum          | table | student
 anon   | postcode             | table | student
 anon   | siret                | table | student
(13 rows)

Функция init импортирует в эти таблицы наборы случайных данных. Они невелики — 1000 значений для каждой категории, на английском языке.

=> SELECT anon.init();
 init
------
 t
(1 row)

Изменим данные в столбце passenger_name, используя значения, возвращаемые функциями fake_first_name и fake_last_name:

=> UPDATE m_tickets
SET passenger_name = anon.concat(anon.fake_first_name(), ' ', anon.fake_last_name());
UPDATE 5

С помощью функции shuffle_column перемешаем данные в столбце бронирований. В качестве последнего параметра вызова требуется указать столбец, выполняющий функцию первичного ключа:

=> SELECT anon.shuffle_column('m_tickets', 'book_ref', 'ticket_no');

Page 8: Page 8

 shuffle_column
----------------
 t
(1 row)

Дополнительно исказим суммы и даты бронирований:

=> SELECT anon.add_noise_on_datetime_column('m_bookings', 'book_date', '3 days 1 hour');
 add_noise_on_datetime_column
------------------------------
 t
(1 row)
=> SELECT anon.add_noise_on_numeric_column('m_bookings', 'total_amount', '0.2');
 add_noise_on_numeric_column
-----------------------------
 t
(1 row)
=> UPDATE m_bookings SET total_amount = round(total_amount, -2); -- уберем лишние разряды
UPDATE 4
=> UPDATE m_bookings SET book_date = date_trunc('minute', book_date); -- обрежем до минут
UPDATE 4

Исходные данные — бронирования и билеты:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM bookings
  JOIN tickets USING (book_ref)
ORDER BY book_ref LIMIT 5;
 book_ref |       book_date        |  passenger_name  | total_amount
----------+------------------------+------------------+--------------
 00000F   | 2017-07-05 03:12:00+03 | ANNA ANTONOVA    |    265700.00
 000012   | 2017-07-14 09:02:00+03 | TAMARA ZAYCEVA   |     37900.00
 000068   | 2017-08-15 14:27:00+03 | TATYANA PETROVA  |     18100.00
 000181   | 2017-08-10 13:28:00+03 | EVGENIYA KARPOVA |    131800.00
 000181   | 2017-08-10 13:28:00+03 | ALEKSANDR ZHUKOV |    131800.00
(5 rows)

Измененные данные:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM m_bookings
  JOIN m_tickets USING (book_ref)
ORDER BY book_ref;
 book_ref |       book_date        | passenger_name | total_amount
----------+------------------------+----------------+--------------
 00000F   | 2017-07-06 09:59:00+03 | Connor Fritz   |    248100.00
 000012   | 2017-07-15 17:13:00+03 | Joan Stein     |     45400.00
 000068   | 2017-08-17 14:35:00+03 | Colton Gilmore |     20900.00
 000181   | 2017-08-12 03:47:00+03 | Monique Norton |    156000.00
 000181   | 2017-08-12 03:47:00+03 | Karl Valdez    |    156000.00
(5 rows)

Данные бронирований и билетов анонимизированы.

Page 9: Метки и правила

Метки безопасности

  • штатный универсальный механизм хранения атрибутов объектов
  • назначаются администратором
  • интерпретируются модулем — провайдером меток
  • Правила изменения данных
    • описываются меткой безопасности для провайдера anon
    • на уровне столбцов таблиц
    • определяют преобразование: замену на константу или значение
    • функции

Комментарии преподавателя

Штатный механизм меток безопасности PostgreSQL позволяет ассоциировать метки с отдельными объектами, в том числе и со столбцами таблиц.

Метка — это атрибут объекта (текстовая строка), который устанавливается администратором, а интерпретируется при обращении к объекту загружаемым модулем — провайдером. Провайдер также определяет, допустима ли метка и разрешено ли применять ее к указанному объекту. PostgreSQL не накладывает ограничений на то, как провайдер должен интерпретировать метки безопасности; он лишь обеспечивает механизм их хранения и поддержку работы модулей-провайдеров. На базе этого механизма может быть реализована, например, система мандатного управления доступом (MAC).

https://postgrespro.ru/docs/enterprise/16/sql-security-label

Для расширения pgpro_anonymizer провайдером меток безопасности выступает модуль anon.

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

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#DECLARE-MA SKING-RULES

Page 10: Динамическое изменение

Работает для ролей, помеченных как недоверенные

  • Обращение к таблице подменяется на обращение к представлению
    • anon.sourceschema — схема исходных данных
    • anon.maskschema — схема анонимизирующих представлений
  • Ограничения и особенности
    • поддержка только одной схемы
    • необходимость каскадного удаления таблиц
    • запрет прямого изменения типа данных
    • возможное снижение производительности

Комментарии преподавателя

Динамическое маскирование работает для ролей с установленной меткой безопасности MASKED (недоверенных ролей).

В схеме, указанной в параметре anon.maskschema, создаются представления, одноименные с анонимизируемыми таблицами из схемы anon.sourceschema и изменяющие данные согласно установленным правилам. При этом для недоверенных пользователей путь поиска изменяется таким образом, что при указании целевой таблицы запрос перенаправляется к маскирующему представлению.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#DYNAMIC-MA SKING

У динамического маскирования есть ряд особенностей и ограничений:

● Чтобы удалить исходную замаскированную таблицу, для нее нужно выполнять DROP … CASCADE, что позволяет сразу удалить и зависящее от нее маскирующее представление.

● Система работает только с одной схемой, указанной в параметре anon.sourceschema (по умолчанию — public).

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

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

Page 11: Динамическое изменение

Создадим пользователя, для которого будем ограничивать доступ к данным, и пометим его как недоверенного:

=> CREATE USER looser;
CREATE ROLE
=> SECURITY LABEL FOR anon ON ROLE looser IS 'MASKED';
SECURITY LABEL

Установленные метки безопасности объектов можно увидеть в системном каталоге:

=> SELECT classoid::regclass, objoid::regrole, provider, label FROM pg_shseclabel;
 classoid  | objoid | provider | label
-----------+--------+----------+--------
 pg_authid | looser | anon     | MASKED
(1 row)

Предположим, что нужно предоставить минимально необходимый доступ к данным пассажиров вспомогательным службам аэропорта. В этом случае номер билета и часть персональных данных пассажира можно скрыть или подменить. Определим правило, скрывающее данные документа, идентифицирующего пассажира — они будут частично маскированы:

=> SECURITY LABEL FOR anon ON COLUMN tickets.passenger_id
IS 'MASKED WITH FUNCTION anon.partial(passenger_id, 2, $$** ***$$, 3)';
SECURITY LABEL

Столбец contact_data представлен типом jsonb, для работы с его элементами определим функцию. При включенном параметре anon.restrict_to_trusted_schemas она должна принадлежать одной из доверенных схем:

=> SELECT objoid::regnamespace, provider, label FROM pg_seclabel
WHERE classoid = 'pg_namespace'::regclass;
   objoid   | provider |  label
------------+----------+---------
 anon       | anon     | TRUSTED
 pg_catalog | anon     | TRUSTED
(2 rows)
=> SHOW anon.restrict_to_trusted_schemas;
 anon.restrict_to_trusted_schemas
----------------------------------
 on
(1 row)

Из соображений безопасности в правилах маскирования надо явно указывать имя схемы — это не позволит пользователю-злоумышленнику подменить функцию на собственную:

=> CREATE FUNCTION anon.fake_contact_data() RETURNS jsonb
  RETURN pg_catalog.jsonb_build_object('email', anon.fake_email(), 'phone', anon.random_phone());
CREATE FUNCTION

В правиле для столбца укажем вызов этой функции:

=> SECURITY LABEL FOR anon ON COLUMN tickets.contact_data
IS 'MASKED WITH FUNCTION anon.fake_contact_data()';
SECURITY LABEL

Вот созданные нами метки для столбцов:

=> SELECT objoid::regclass,
  (SELECT attname FROM pg_attribute WHERE attrelid = objoid AND attnum = objsubid),
  provider, label
FROM pg_seclabel
WHERE classoid = 'pg_class'::regclass;

Page 12: Page 12

 objoid  |   attname    | provider |                               label
---------+--------------+----------+------------------------------------------------------
-------------
 tickets | passenger_id | anon     | MASKED WITH FUNCTION anon.partial(passenger_id, 2,
$$** ***$$, 3)
 tickets | contact_data | anon     | MASKED WITH FUNCTION anon.fake_contact_data()
(2 rows)

Более удобный способ получения информации о правилах изменения — представление anon.pg_masking_rules:

=> SELECT relnamespace, relname, attname, format_type, col_description FROM anon.pg_masking_rules \gx
-[ RECORD 1 ]---+------------------------------------------------------------------
relnamespace    | bookings
relname         | tickets
attname         | passenger_id
format_type     | character varying(20)
col_description | MASKED WITH FUNCTION anon.partial(passenger_id, 2, $$** ***$$, 3)
-[ RECORD 2 ]---+------------------------------------------------------------------
relnamespace    | bookings
relname         | tickets
attname         | contact_data
format_type     | jsonb
col_description | MASKED WITH FUNCTION anon.fake_contact_data()

Далее нам необходимо установить исходную (bookings) и анонимизирующую (mask по умолчанию) схемы для базы данных:

=> ALTER DATABASE demo SET anon.sourceschema TO 'bookings';
ALTER DATABASE
=> SHOW anon.maskschema;
 anon.maskschema
-----------------
 mask
(1 row)

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

=> \c
You are now connected to database "demo" as user "student".
=> SELECT anon.start_dynamic_masking();
 start_dynamic_masking
-----------------------
 t
(1 row)

Вызов этой функции: проверит состояние расширения и при необходимости вызовет anon.init; при необходимости создаст схему, указанную в параметре anon.maskschema; для таблиц в sourceschema создаст в maskschema одноименные представления, через которые недоверенные пользователи будут видеть маскированные данные; отзовет все права на sourceschema у всех недоверенных ролей; назначит недоверенным ролям необходимые права на схемы anon и anon.maskschema, а также на объекты в них; установит недоверенным ролям путь поиска к схеме anon.maskschema. Вот список созданных маскирующих представлений:

=> \dv mask.*

Page 13: Page 13

             List of relations
 Schema |      Name       | Type |  Owner
--------+-----------------+------+---------
 mask   | aircrafts_data  | view | student
 mask   | airports_data   | view | student
 mask   | boarding_passes | view | student
 mask   | bookings        | view | student
 mask   | flights         | view | student
 mask   | seats           | view | student
 mask   | ticket_flights  | view | student
 mask   | tickets         | view | student
(8 rows)

Подключаемся к базе от имени недоверенного пользователя и выполняем запрос:

student$ psql -d demo -U looser
looser=> SELECT ticket_no, passenger_id, passenger_name, contact_data FROM tickets LIMIT 5;
   ticket_no   | passenger_id |   passenger_name   |
contact_data
---------------+--------------+--------------------+--------------------------------------
-----------------------------
 0005432000987 | 81** ***011  | VALERIY TIKHONOV   | {"email": "zjohnson@example.net",
"phone": "0129939052"}
 0005432000988 | 84** ***203  | EVGENIYA ALEKSEEVA | {"email": "alexander90@example.org",
"phone": "0360226263"}
 0005432000989 | 10** ***484  | ARTUR GERASIMOV    | {"email":
"christophernorris@example.net", "phone": "0999795800"}
 0005432000990 | 48** ***049  | ALINA VOLKOVA      | {"email": "iharmon@example.org",
"phone": "0412376527"}
 0005432000991 | 66** ***589  | MAKSIM ZHUKOV      | {"email":
"joshuahodges@example.com", "phone": "0534237840"}
(5 rows)

В полученных данных видно, что такая анонимизация никак не учитывает смысловой взаимосвязи столбцов, содержащих имя пассажира и его контактные данные (адрес электронной почты). Для получения более правдоподобных данных можно использовать пользовательские подменяющие функции. Путь поиска и права для недоверенного пользователя изменены таким образом, что вместо схемы bookings ему видны объекты из схемы mask:

looser=> SELECT current_schemas(true);
  current_schemas
-------------------
 {pg_catalog,mask}
(1 row)

Доступа к исходным данным у пользователя looser нет:

looser=> SELECT * FROM bookings.tickets;
ERROR:  permission denied for schema bookings
LINE 1: SELECT * FROM bookings.tickets;
                      ^

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

looser=> \d+ tickets

Page 14: Page 14

                                       View "mask.tickets"
     Column     |         Type          | Collation | Nullable | Default | Storage  |
Description
----------------+-----------------------+-----------+----------+---------+----------+-----
--------
 ticket_no      | character(13)         |           |          |         | extended |
 book_ref       | character(6)          |           |          |         | extended |
 passenger_id   | character varying(20) |           |          |         | extended |
 passenger_name | text                  |           |          |         | extended |
 contact_data   | jsonb                 |           |          |         | extended |
View definition:
 SELECT ticket_no,
    book_ref,
    anon.partial(passenger_id::text, 2, '** ***'::text, 3)::character varying(20) AS
passenger_id,
    passenger_name,
    anon.fake_contact_data() AS contact_data
   FROM bookings.tickets;

Изменим правила маскирования для столбцов. Номер билета будем считать конфиденциальным и скроем его, заменив на константу:

=> SECURITY LABEL FOR anon ON COLUMN tickets.ticket_no
IS 'MASKED WITH VALUE $$PRIVATE$$';
SECURITY LABEL

А для документа, идентифицирующего пассажира, маскирование отменим:

=> SECURITY LABEL FOR anon ON COLUMN tickets.passenger_id
IS NULL;
SECURITY LABEL
looser=> SELECT ticket_no, passenger_id, passenger_name, contact_data FROM tickets LIMIT 5;
   ticket_no   | passenger_id |   passenger_name   |
contact_data
---------------+--------------+--------------------+--------------------------------------
---------------------------
 PRIVATE       | 8149 604011  | VALERIY TIKHONOV   | {"email":
"harmonmatthew@example.com", "phone": "0888938751"}
 PRIVATE       | 8499 420203  | EVGENIYA ALEKSEEVA | {"email": "john90@example.org",
"phone": "0853715519"}
 PRIVATE       | 1011 752484  | ARTUR GERASIMOV    | {"email": "ashley83@example.com",
"phone": "0739380089"}
 PRIVATE       | 4849 400049  | ALINA VOLKOVA      | {"email":
"kimberlyjohnson@example.org", "phone": "0394258303"}
 PRIVATE       | 6615 976589  | MAKSIM ZHUKOV      | {"email": "ydavidson@example.com",
"phone": "0409638085"}
(5 rows)

Page 15: Статическое изменение

Данные в таблицах изменяются по заданным правилам

  • anon.anonymize_database() — все данные в базе
  • anon.anonymize_table() — отдельная таблица
  • anon.anonymize_column() — отдельный столбец таблицы

Комментарии преподавателя

Статическое изменение данных также может выполняться на основе определенных правил маскирования.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#APPLYING-MA SKING-RULES

Функции anon.anonymize_database, anon.anonymize_table и anon.anonymize_column позволяют преобразовать исходные таблицы в соответствии с заранее определенными правилами.

Поскольку при этом перезаписываются все строки таблицы, даже если изменяется только один столбец, такая операция может занять продолжительное время.

Иногда может оказаться быстрее экспортировать анонимизированные данные и повторно загрузить их в базу данных. Если нужно экспортировать базу вместе с анонимизируемыми данными, следует воспользоваться скриптом pg_dump_anon.sh.

https://postgrespro.ru/docs/enterprise/16/pgpro-anonymizer#ANONYMOUS-DUMPS

Page 16: Статическое изменение по правилам

Сейчас данные в схеме m_schema выглядят следующим образом:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM m_schema.m_bookings
  JOIN m_schema.m_tickets USING (book_ref)
ORDER BY passenger_name;
 book_ref |       book_date        | passenger_name | total_amount
----------+------------------------+----------------+--------------
 000068   | 2017-08-17 14:35:00+03 | Colton Gilmore |     20900.00
 00000F   | 2017-07-06 09:59:00+03 | Connor Fritz   |    248100.00
 000012   | 2017-07-15 17:13:00+03 | Joan Stein     |     45400.00
 000181   | 2017-08-12 03:47:00+03 | Karl Valdez    |    156000.00
 000181   | 2017-08-12 03:47:00+03 | Monique Norton |    156000.00
(5 rows)

Определим правила, которые скроют номер бронирования, не нарушив взаимосвязь таблиц по столбцу book_ref. Применим хеширование: это преобразование является детерминированным. Вычисленное значение хеша будет слишком длинным…

=> SELECT anon.hash('000181');
                               hash
------------------------------------------------------------------
 2b4c7a7391af921b2049fc14539989e56cb5003c11dd2ce863c06ed1acbc1df9
(1 row)

…поэтому мы идем на то, чтобы сохранить только его часть. Конечно, такое сокращение повысит риск коллизии значений.

=> SECURITY LABEL FOR anon ON COLUMN m_schema.m_tickets.book_ref
IS 'MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)';
SECURITY LABEL
=> SECURITY LABEL FOR anon ON COLUMN  m_schema.m_bookings.book_ref
IS 'MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)';
SECURITY LABEL

Для хеширования применяются алгоритм и соль, указанные в параметрах. Обычные пользователи не имеют к ним доступа даже на чтение:

looser=> SHOW anon.algorithm;
ERROR:  permission denied to examine "anon.algorithm"
DETAIL:  Only roles with privileges of the "pg_read_all_settings" role may examine this
parameter.
looser=> SHOW anon.salt;
ERROR:  permission denied to examine "anon.salt"
DETAIL:  Only roles with privileges of the "pg_read_all_settings" role may examine this
parameter.

Проверим правила:

=> SELECT relnamespace, relname, attname, format_type, col_description FROM anon.pg_masks
WHERE relnamespace = 'm_schema'::regnamespace \gx
-[ RECORD 1 ]---+-------------------------------------------------------------
relnamespace    | m_schema
relname         | m_tickets
attname         | book_ref
format_type     | character(6)
col_description | MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)
-[ RECORD 2 ]---+-------------------------------------------------------------
relnamespace    | m_schema
relname         | m_bookings
attname         | book_ref
format_type     | character(6)
col_description | MASKED WITH FUNCTION pg_catalog.left(anon.hash(book_ref), 6)

Page 17: Page 17

Преобразуем таблицы с применением этих правил:

=> SELECT anon.anonymize_table('m_schema.m_tickets'), anon.anonymize_table('m_schema.m_bookings');
 anonymize_table | anonymize_table
-----------------+-----------------
 t               | t
(1 row)

Результат:

=> SELECT book_ref, book_date, passenger_name, total_amount
FROM m_schema.m_bookings
  JOIN m_schema.m_tickets USING (book_ref)
ORDER BY passenger_name;
 book_ref |       book_date        | passenger_name | total_amount
----------+------------------------+----------------+--------------
 5561d8   | 2017-08-17 14:35:00+03 | Colton Gilmore |     20900.00
 bcc517   | 2017-07-06 09:59:00+03 | Connor Fritz   |    248100.00
 b2836e   | 2017-07-15 17:13:00+03 | Joan Stein     |     45400.00
 2b4c7a   | 2017-08-12 03:47:00+03 | Karl Valdez    |    156000.00
 2b4c7a   | 2017-08-12 03:47:00+03 | Monique Norton |    156000.00
(5 rows)

Обратите внимание, что для безопасности преобразующие функции работают с правами вызывающего пользователя:

=> SELECT proname, CASE WHEN prosecdef THEN 'DEFINER' ELSE 'INVOKER' END security
FROM pg_proc
WHERE pronamespace = 'anon'::regnamespace AND proname LIKE 'anonymize%';
      proname       | security
--------------------+----------
 anonymize_column   | INVOKER
 anonymize_database | INVOKER
 anonymize_table    | INVOKER
(3 rows)

Page 18: Итоги

Анонимизация позволяет защитить конфиденциальные данные, когда разграничение ролей неприменимо

  • Правила изменения данных определяются декларативно с помощью меток безопасности
  • Поддерживается статическое и динамическое изменение
  • Анонимизация данных влечет накладные расходы

Page 19: Практика

  1. Настройте правило маскирования столбца с контактными данными таблицы m_tickets так, чтобы почтовые адреса содержали имена и фамилии пассажиров. Экспортируйте данные таблицы в анонимизированном виде от лица недоверенного пользователя. 2. Включите динамическое маскирование. Объявите правило для подмены имени пассажира в таблице m_tickets. Выполните несколько запросов к одной из строк таблицы. Затем перепишите правило так, чтобы использовалась псевдонимизация и снова выполните запросы. Сравните результаты.

Комментарии преподавателя

  1. Установите расширение и скопируйте небольшую часть данных в таблицу m_tickets из bookings.tickets так, как это сделано в демонстрации.

Напишите функцию, формирующую контактные данные, в которых почтовый адрес имеет вид имя.фамилия@example.com. Поместите эту функцию в доверенную схему и используйте ее в правиле изменения.

Создайте недоверенного пользователя и сделайте выгрузку данных с помощью скрипта pg_dump_anon.sh от его имени.

  1. Для подмены имен используйте изменчивую функцию anon.fake_first_name. Для псевдонимизации замените ее на anon.pseudo_first_name.

Page 20: 1. Пользовательские правила анонимизации

Установим расширение pgpro_anonymizer в демобазе и инициализируем его.

=> ALTER SYSTEM SET shared_preload_libraries = 'anon';
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service
student$ psql demo
=> CREATE EXTENSION anon;
CREATE EXTENSION
=> SELECT anon.init();
 init
------
 t
(1 row)

Создадим в схеме public таблицу m_tickets, в которую скопируем часть данных из таблицы tickets схемы bookings:

=> CREATE TABLE public.m_tickets AS SELECT * FROM tickets LIMIT 5;
SELECT 5

Используем функцию, показанную в демонстрации, но добавим подзапрос, формирующий почтовый адрес по переданному аргументу:

=> CREATE FUNCTION anon.fake_contact_data(name text) RETURNS jsonb
  RETURN pg_catalog.jsonb_build_object(
    'email', (SELECT pg_catalog.concat(nm[1], '.', nm[2], '@example.com')
              FROM pg_catalog.regexp_split_to_array(pg_catalog.lower(name), '\s+') nm),
    'phone', anon.random_phone()
  );
CREATE FUNCTION

Проверим:

=> SELECT anon.fake_contact_data('Vasily Bulkin');
                       fake_contact_data
---------------------------------------------------------------
 {"email": "vasily.bulkin@example.com", "phone": "0828806024"}
(1 row)

В правиле маскирования укажем вызов этой функции:

=> SECURITY LABEL FOR anon ON COLUMN m_tickets.contact_data
IS 'MASKED WITH FUNCTION anon.fake_contact_data(passenger_name)';
SECURITY LABEL

Взглянем на созданное правило:

=> SELECT * FROM anon.pg_masking_rules \gx
-[ RECORD 1 ]----+------------------------------------------------------------
attrelid         | 16913
attnum           | 5
relnamespace     | public
relname          | m_tickets
attname          | contact_data
format_type      | jsonb
col_description  | MASKED WITH FUNCTION anon.fake_contact_data(passenger_name)
masking_function | anon.fake_contact_data(passenger_name)
masking_value    |
priority         | 100
masking_filter   | anon.fake_contact_data(passenger_name)
trusted_schema   | t

И на исходные данные (динамическое маскирование не включено):

=> SELECT * FROM m_tickets;

Page 21: Page 21

   ticket_no   | book_ref | passenger_id |   passenger_name   |
     contact_data
---------------+----------+--------------+--------------------+---------------------------
--------------------------------------------------
 0005432000987 | 06B046   | 8149 604011  | VALERIY TIKHONOV   | {"phone": "+70127117011"}
 0005432000988 | 06B046   | 8499 420203  | EVGENIYA ALEKSEEVA | {"phone": "+70378089255"}
 0005432000989 | E170C3   | 1011 752484  | ARTUR GERASIMOV    | {"phone": "+70760429203"}
 0005432000990 | E170C3   | 4849 400049  | ALINA VOLKOVA      | {"email":
"volkova.alina_03101973@postgrespro.ru", "phone": "+70582584031"}
 0005432000991 | F313DD   | 6615 976589  | MAKSIM ZHUKOV      | {"email":
"m-zhukov061972@postgrespro.ru", "phone": "+70149562185"}
(5 rows)

Создадим недоверенного пользователя и выполним выгрузку данных из таблицы под его аккаунтом.

=> CREATE USER novice;
CREATE ROLE
=> SECURITY LABEL FOR anon ON ROLE novice IS 'MASKED';
SECURITY LABEL
=> GRANT SELECT ON m_tickets TO novice;
GRANT
student$ /opt/pgpro/ent-16/bin/pg_dump_anon.sh \
-U novice -t m_tickets demo \
2>/dev/null | grep -A 5 COPY
COPY public.m_tickets FROM STDIN WITH CSV;
0005432000987,06B046,8149 604011,VALERIY TIKHONOV,"{""email"":
""valeriy.tikhonov@example.com"", ""phone"": ""0804160992""}"
0005432000988,06B046,8499 420203,EVGENIYA ALEKSEEVA,"{""email"":
""evgeniya.alekseeva@example.com"", ""phone"": ""0322424618""}"
0005432000989,E170C3,1011 752484,ARTUR GERASIMOV,"{""email"":
""artur.gerasimov@example.com"", ""phone"": ""0329808084""}"
0005432000990,E170C3,4849 400049,ALINA VOLKOVA,"{""email"":
""alina.volkova@example.com"", ""phone"": ""0584090983""}"
0005432000991,F313DD,6615 976589,MAKSIM ZHUKOV,"{""email"":
""maksim.zhukov@example.com"", ""phone"": ""0787065850""}"

Данные таблицы выгрузились в анонимизированном виде.

2. Подмена и псевдонимизация

Активируем механизм динамического маскирования:

=> SELECT anon.start_dynamic_masking();
 start_dynamic_masking
-----------------------
 t
(1 row)

Сформируем правило, подменяющее имя пассажира. Для этого воспользуемся функцией anon.fake_last_name:

=> SECURITY LABEL FOR anon ON COLUMN m_tickets.passenger_name
IS 'MASKED WITH FUNCTION anon.fake_first_name()';
SECURITY LABEL

Подключаемся как недоверенный пользователь и выполняем несколько одинаковых запросов.

=> \c - novice
You are now connected to database "demo" as user "novice".
=> SELECT passenger_name FROM m_tickets WHERE passenger_id = '8149 604011';
 passenger_name
----------------
 Janet
(1 row)
=> \g

Page 22: Page 22

 passenger_name
----------------
 Richard
(1 row)
=> \g
 passenger_name
----------------
 Tyrone
(1 row)

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

=> \c - student
You are now connected to database "demo" as user "student".
=> \df anon.pseudo_*
                                          List of functions
 Schema |       Name        | Result data type |              Argument data types
     | Type
--------+-------------------+------------------+------------------------------------------
-----+------
 anon   | pseudo_city       | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_company    | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_country    | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_email      | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_first_name | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_iban       | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_last_name  | text             | seed anyelement, salt text DEFAULT
NULL::text | func
 anon   | pseudo_siret      | text             | seed anyelement, salt text DEFAULT
NULL::text | func
(8 rows)

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

=> SECURITY LABEL FOR anon ON COLUMN m_tickets.passenger_name
IS 'MASKED WITH FUNCTION anon.pseudo_first_name(passenger_id)';
SECURITY LABEL
=> \c - novice
You are now connected to database "demo" as user "novice".
novice=> SELECT passenger_name FROM m_tickets WHERE passenger_id = '8149 604011';
 passenger_name
----------------
 Eddie
(1 row)
novice=> \g
 passenger_name
----------------
 Eddie
(1 row)
novice=> \g

Page 23: Уборка

 passenger_name
----------------
 Eddie
(1 row)

Удалим расширение и объекты, созданные в базе demo, удалим созданную роль и очистим список загружаемых библиотек.

novice=> \c - student
You are now connected to database "demo" as user "student".
student=# DROP EXTENSION anon CASCADE;
NOTICE:  drop cascades to 2 other objects
DETAIL:  drop cascades to function anon.fake_contact_data(text)
drop cascades to view mask.m_tickets
DROP EXTENSION
student=# DROP SCHEMA mask;
DROP SCHEMA
student=# DROP TABLE m_tickets;
DROP TABLE
student=# DROP USER novice;
DROP ROLE
student=# ALTER SYSTEM RESET shared_preload_libraries;
ALTER SYSTEM
student$ sudo systemctl restart postgrespro-ent-16.service