G.1. pgpro_anonymizer — маскирование или замена конфиденциальных данных #

pgpro_anonymizer — это расширение для маскирования или замены конфиденциальных коммерческих данных или информации, позволяющей установить личность (PII, персональные данные), в БД Postgres Pro.

В проекте используется декларативный подход к анонимизации. Это означает, что вы можете объявлять правила маскирования, используя язык описания данных (DDL), и задавать свою стратегию анонимизации внутри самого определения таблицы.

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

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

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

G.1.1. Термины и определения #

Используются следующие основные стратегии:

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

  • Статическое маскирование полностью заменяет конфиденциальную информацию несвязанными данными. После такой обработки исходные данные не могут быть восстановлены.

Данные могут быть изменены несколькими способами:

  • Удаление просто удаляет данные.

  • Статическая замена последовательно заменяет данные одним и тем же значением. Например: замена всех значений столбца типа text на значение «КОНФИДЕНЦИАЛЬНО».

  • Отклонение «сдвигает» даты и числовые значения. Например, после применения отклонения +/- 10% к столбцу зарплаты набор данных не потеряет смысл.

  • Обобщение снижает точность данных, заменяя их диапазоном значений. Вместо «Бобу 28 лет» можно сказать «Бобу от 20 до 30 лет». Этот метод полезен для аналитики, так как данные остаются верными.

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

  • Рандомизация заменяет конфиденциальные данные случайными, но правдоподобными значениями. Цель состоит в том, чтобы исключить возможность какой-либо идентификации по записи данных, оставляя её пригодной для тестирования, анализа и обработки данных.

  • Частичное скрытие аналогично статической подстановке, но оставляет часть данных нетронутыми. Например: номер кредитной карты может быть заменён на «40XX XXXX XXXX XX96».

  • Пользовательские правила предназначены для изменения данных в соответствии с особыми требованиями (например, заменить одновременно почтовый индекс и название города случайными значениями, чтобы они оставались согласованными).

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

G.1.2. Пример анонимизации #

Предположим, что необходимо замаскировать адреса электронной почты и телефонные номера:

SELECT * FROM people;
 id | firstname | lastname |   phone
----+-----------+----------+------------
 T1 | Sarah     | Connor    | 0609110911
  1. Активируйте механизм динамического маскирования:

    SELECT anon.start_dynamic_masking();
  2. Объявите недоверенного пользователя:

    CREATE ROLE skynet LOGIN;
    SECURITY LABEL FOR anon ON ROLE skynet IS 'MASKED';
  3. Объявите правила маскирования:

    SECURITY LABEL FOR anon ON COLUMN people.lastname
    IS 'MASKED WITH FUNCTION anon.fake_last_name()';
    
    SECURITY LABEL FOR anon ON COLUMN people.phone
    IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)';
  4. Подключитесь к серверу от имени недоверенного пользователя:

    \connect - skynet
    SELECT * FROM people;
     id | firstname | lastname  |   phone
    ----+-----------+-----------+------------
     T1 | Sarah     | Stranahan | 06******11

G.1.3. Установка и подготовка #

Расширение pgpro_anonymizer поставляется вместе с Postgres Pro Enterprise в виде отдельного пакета pgpro-anonymizer-ent-17 (подробные инструкции по установке приведены в Главе 17). Чтобы включить pgpro_anonymizer, выполните следующие действия:

  1. Добавьте имя библиотеки в переменную shared_preload_libraries в файле postgresql.conf:

    shared_preload_libraries = 'anon'
  2. Перезагрузите сервер баз данных, чтобы изменения вступили в силу.

    Примечание

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

    SHOW shared_preload_libraries;
  3. Создайте расширение, выполнив следующий запрос:

    CREATE EXTENSION anon CASCADE;
  4. Инициализируйте расширение:

    SELECT anon.init();

    Функция init() импортирует набор случайных данных по умолчанию (IBAN (международный номер банковского счёта), имена, города и т. д.). Это очень маленький набор данных на английском языке (1000 значений для каждой категории).

G.1.4. Конфигурирование #

У расширения есть несколько параметров, которые можно задать на уровне всего экземпляра БД (в postgresql.conf или используя команду ALTER SYSTEM).

Их также можно (и зачастую лучше) задать на уровне базы данных следующим образом:

ALTER DATABASE customers SET anon.algorithm = sha512;

Следующие параметры может изменить только суперпользователь:

anon.algorithm #

Метод хеширования, используемый функциями псевдонимизации. Используйте документацию pgcrypto для ознакомления со списком доступных параметров.

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

Тип: text

По умолчанию: sha256

Видимость: только для суперпользователей

anon.maskschema #

Схема (то есть пространство имён), в которой будут храниться представления динамического маскирования.

Тип: text

По умолчанию: mask

Видимость: для всех пользователей

anon.restrict_to_trusted_schemas #

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

Это повышает безопасность, не позволяя пользователям объявлять собственные фильтры маскирования.

Это также означает, что схема должна указываться явно в правилах маскирования. Например, приведённые ниже правила не сработают, потому что схема функции lower не объявлена.

SECURITY LABEL FOR anon ON COLUMN people.name
IS 'MASKED WITH FUNCTION lower(people.name)';

Правильный способ объявить эту функцию:

SECURITY LABEL FOR anon ON COLUMN people.name
IS 'MASKED WITH FUNCTION pg_catalog.lower(people.name)';

Тип: boolean

По умолчанию: on (вкл.)

Видимость: для всех пользователей

anon.salt #

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

ALTER DATABASE foo SET anon.salt = 'This_Is_A_Very_Secret_Salt';

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

  • Псевдонимизированные данные

  • Алгоритм хеширования (см. anon.algorithm)

  • Значение соли

Соль и имя алгоритма хеширования должны быть защищены тем же уровнем безопасности, что и сами данные. Вот почему значение соли следует хранить непосредственно в базе данных, используя команду ALTER DATABASE.

Тип: text

По умолчанию: (не задан)

Видимость: только для суперпользователей

anon.sourceshema #

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

Измените это значение перед запуском динамического маскирования.

ALTER DATABASE foo SET anon.sourceschema TO 'my_app';

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

SELECT anon.start_dynamic_masking();

Тип: text

По умолчанию: public

Видимость: для всех пользователей

G.1.5. Объявление правил маскирования #

Основная идея данного расширения — предложить встроенную анонимность.

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

Расширение позволяет маскировать данные непосредственно внутри экземпляра Postgres Pro без использования внешнего инструмента и таким образом ограничивает раскрытие информации и уменьшает риск утечки данных.

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

CREATE TABLE player (id SERIAL, name TEXT, points INT);

INSERT INTO player VALUES
  ( 1, 'Kareem Abdul-Jabbar', 38387),
  ( 5, 'Michael Jordan', 32292 );

SECURITY LABEL FOR anon ON COLUMN player.name
  IS 'MASKED WITH FUNCTION anon.fake_last_name()';

SECURITY LABEL FOR anon ON COLUMN player.id
  IS 'MASKED WITH VALUE NULL';

Важно

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

G.1.5.1. Экранирование строковых литералов (констант) #

Как вы могли заметить, определения правил маскирования заключаются в одинарные кавычки. Поэтому, если в правиле маскирования нужно записать строку, экранируйте её символами доллара:

SECURITY LABEL FOR anon ON COLUMN player.name
  IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';

G.1.5.2. Использование выражений #

Вы можете использовать более сложные выражения с синтаксисом MASKED WITH VALUE:

SECURITY LABEL FOR anon ON COLUMN player.name
  IS 'MASKED WITH VALUE CASE WHEN name IS NULL
                             THEN $$John$$
                             ELSE anon.random_string(LENGTH(name))
                             END';

G.1.5.3. Удаление правила маскирования #

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

SECURITY LABEL FOR anon ON COLUMN player.name IS NULL;

Чтобы удалить все правила сразу, используйте оператор:

SELECT anon.remove_masks_for_all_columns();

G.1.6. Функции маскирования #

Данное расширение предоставляет функции для реализации следующих основных стратегий анонимизации:

В зависимости от ваших данных может потребоваться использовать разные стратегии для разных столбцов:

G.1.6.1. Удаление #

Самый быстрый и безопасный способ анонимизировать данные — удалить их.

Во многих случаях лучший способ скрыть содержимое столбца — заменить все значения одним статическим значением.

Например, вы можете заменить весь столбец словом CONFIDENTIAL, как указано ниже:

SECURITY LABEL FOR anon
  ON COLUMN users.address
  IS 'MASKED WITH VALUE $$CONFIDENTIAL$$';

G.1.6.2. Добавление искажения #

Этот способ также называется Отклонение. В его основе лежит «сдвиг» дат и числовых значений. Например, после применения отклонения +/- 10% к столбцу зарплаты набор данных не потеряет смысл.

anon.noise(noise_value anyelement, ratio double precision) #

Если параметр ratio = 0.33, все значения столбца будут случайным образом смещены с коэффициентом +/- 33%.

anon.dnoise(noise_value anyelement, noise_range interval) #

Если интервал = «2 дня», возвращаемое значение будет исходным значением, сдвинутым случайным образом на +/- 2 дня.

Важно

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

G.1.6.3. Рандомизация #

Данное расширение предоставляет большой выбор функций для генерации абсолютно случайных данных:

G.1.6.3.1. Основные случайные значения #
anon.random_date() #

Возвращает значение date.

random_date_between(d1 date, d2 date) #

Возвращает значение date между d1 и d2.

random_int_between(i1 integer, i2 integer) #

Возвращает значение integer между i1 и i2.

random_bigint_between(b1 bigint, b2 bigint) #

Возвращает значение bigint между b1 и b2.

anon.random_string(n integer) #

Возвращает значение типа text, содержащее n букв.

anon.random_zip() #

Возвращает 5-значный код.

anon.random_phone(phone_prefix text) #

Возвращает 8-значный номер телефона с префиксом phone_prefix.

anon.random_in(ARRAY[1,2,3]) #

Возвращает целое число между 1 и 3.

anon.random_in(ARRAY['red','green','blue']) #

Возвращает случайное текстовое значение из массива ['red', 'green', 'blue'].

G.1.6.4. Фальсификация #

Идея фальсификации заключается в замене конфиденциальных данных случайными, но правдоподобными значениями. Цель состоит в том, чтобы избежать какой-либо идентификации по записи данных, оставляя её при этом пригодной для тестирования, анализа и обработки данных.

Предоставляются следующие функции фальсификации:

anon.fake_address() #

Возвращает полный почтовый адрес.

anon.fake_city() #

Возвращает название существующего города.

anon.fake_country() #

Возвращает название существующей страны.

anon.fake_company() #

Возвращает случайное название компании.

anon.fake_email() #

Возвращает действительный адрес электронной почты.

anon.fake_first_name() #

Возвращает случайное имя.

anon.fake_iban() #

Возвращает случайный действительный номер IBAN (международный номер банковского счёта).

anon.fake_last_name() #

Возвращает случайную фамилию.

anon.fake_postcode() #

Возвращает случайный действительный почтовый индекс.

Для столбцов типа text и varchar вы можете использовать классический генератор Lorem Ipsum:

anon.lorem_ipsum() #

Возвращает пять абзацев.

anon.lorem_ipsum(2) #

Возвращает два абзаца.

anon.lorem_ipsum( paragraphs := 4 ) #

Возвращает четыре абзаца.

anon.lorem_ipsum( words := 20 ) #

Возвращает 20 слов.

anon.lorem_ipsum( characters := LENGTH(table.column) ) #

Возвращает то же количество символов, что и в исходной строке.

G.1.6.5. Расширенная фальсификация #

Генерация фальсифицированных данных — сложная тема. Представленные здесь функции ограничены базовым вариантом использования. Чтобы узнать о более продвинутых методах фальсификации, в частности, если вам нужны локализованные фальсифицированные данные, взгляните на PostgreSQL Faker, расширение, основанное на известной библиотеке Faker языка Python.

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

Например:

CREATE SCHEMA faker;
CREATE EXTENSION faker SCHEMA faker;
SELECT faker.faker('de_DE');
SELECT faker.first_name_female();
 first_name_female
-------------------
 Mirja

G.1.6.6. Псевдонимизация #

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

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

anon.pseudo_first_name(seed anyelement, salt text) #

Возвращает случайное имя.

anon.pseudo_last_name(seed anyelement, salt text) #

Возвращает случайную фамилию.

anon.pseudo_email(seed anyelement, salt text) #

Возвращает действительный адрес электронной почты.

anon.pseudo_city(seed anyelement, salt text) #

Возвращает название существующего города.

anon.pseudo_country(seed anyelement, salt text) #

Возвращает название существующей страны.

anon.pseudo_company(seed anyelement, salt text) #

Возвращает случайное название компании.

anon.pseudo_iban(seed anyelement, salt text) #

Возвращает случайный действительный номер IBAN (международный номер банковского счёта).

Второй аргумент (salt) является необязательным. Вы можете вызывать каждую функцию с затравкой вида anon.pseudo_city('bob'), без указания соли. Соль нужна, чтобы увеличить сложность и избежать атак по словарю и полным перебором. Если соль не указана, вместо неё используется случайное секретное значение соли (за подробным описанием обратитесь к разделу Универсальное хеширование).

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

SECURITY LABEL FOR anon
  ON COLUMN users.emailaddress
  IS 'MASKED WITH FUNCTION anon.pseudo_email(users.login)';

Примечание

Вы можете создавать уникальные значения, используя функцию псевдонимизации. Например, если вы хотите замаскировать столбец email, объявленный как UNIQUE, вам нужно инициализировать расширение с фальсифицированным набором данных, который намного больше, чем количество строк в таблице. В противном случае вы можете столкнуться с «коллизиями», то есть ситуациями, когда два разных исходных значения создают одно и то же псевдозначение.

Важно

Псевдонимизацию часто путают с анонимизацией, но на самом деле они служат двум разным целям: псевдонимизация — это способ защитить личную информацию, но псевдонимизированные данные по-прежнему «связаны» с реальными данными.

G.1.6.7. Общее хеширование #

Теоретически хеширование не является надёжным методом анонимизации, однако на практике иногда необходимо создать детерминированный хеш оригинальных данных.

Например когда пара первичный/внешний ключ является «естественным ключом», она может содержать актуальную информацию (номер клиента, содержащий дату рождения или что-то подобное).

Хеширование таких столбцов позволяет сохранить ссылочную целостность даже для относительно необычных исходных данных.

anon.hash(value text) #

Возвращает текстовый хеш значения, используя секретную соль и алгоритм хеширования.

anon.digest(value text, salt text, algorithm text) #

Позволяет выбрать соль и алгоритм хеширования. Поддерживаемые алгоритмы: md5, sha1, sha224, sha256, sha384 и sha512.

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

anon.set_secret_salt(value text) #

Задать собственное значение соли.

anon.set_algorithm(value text) #

Выбрать другой алгоритм хеширования. Список поддерживаемых алгоритмов представлен в описании функции anon.digest.

Имейте в виду, что хеширование — это форма псевдонимизации. Это означает, что данные могут быть «деанонимизированы» с помощью хешированного значения и функции маскирования. Если злоумышленники получат доступ к этим двум элементам, они смогут идентифицировать некоторых людей, используя методы атаки brute force (полным перебором) или dictionary (по словарю). Поэтому соль и алгоритм, используемые для хеширования данных, нужно защищать так же надёжно, как и исходный набор данных.

В двух словах, мы рекомендуем вам использовать функцию anon.hash(), а не anon.digest(), потому что тогда соль не будет отображаться в правиле маскирования открытым текстом.

Кроме того, на практике хеш-функция вернёт длинную строку символов, например:

SELECT anon.hash('bob');
                                  hash
----------------------------------------------------------------------------------------------------------------------------------
95b6accef02c5a725a8c9abf19ab5575f99ca3d9997984181e4b3f81d96cbca4d0977d694ac490350e01d0d213639909987ef52de8e44d6258d536c55e427397

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

SECURITY LABEL FOR anon ON COLUMN people.phone_number
IS 'MASKED WITH FUNCTION pg_catalog.left(anon.hash(phone_number),12)';

SECURITY LABEL FOR anon ON COLUMN call_history.fk_phone_number
IS 'MASKED WITH FUNCTION pg_catalog.left(anon.hash(fk_phone_number),12)';

Конечно, сокращение хеш-значения до 12 символов повысит риск «коллизии» (два разных значения имеют один и тот же фальсифицированный хеш). Решение о принятии такого риска остаётся за вами.

G.1.6.8. Частичное скрытие #

Частичное скрытие оставляет часть данных нетронутыми. Например: номер кредитной карты может быть заменён на «40XX XXXX XXXX XX96».

anon.partial(input text, prefix int, padding text, suffix int) #

Частично заменяет заданный текст. Например, anon.partial('abcdefgh',1,'xxxx',3) возвращает axxxxfgh.

anon.partial_email(email text) #

Частично заменяет указанный адрес электронной почты. Например, anon.partial_email('daamien@gmail.com') возвращает da******@gm******.com.

G.1.6.9. Обобщение #

Обобщение — это принцип замены исходного значения диапазоном, содержащим это значение. Например, вместо «Полу 42 года», можно сказать «Полу от 40 до 50 лет».

Примечание

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

Рассмотрим следующую таблицу, содержащую медицинские данные:

SELECT * FROM patient;
 id |   name   |  zipcode |   birth    |    disease
----+----------+----------+------------+---------------
  1 | Alice    |    47678 | 1979-12-29 | Heart Disease
  2 | Bob      |    47678 | 1959-03-22 | Heart Disease
  3 | Caroline |    47678 | 1988-07-22 | Heart Disease
  4 | David    |    47905 | 1997-03-04 | Flu
  5 | Eleanor  |    47909 | 1999-12-15 | Heart Disease
  6 | Frank    |    47906 | 1968-07-04 | Cancer
  7 | Geri     |    47605 | 1977-10-30 | Heart Disease
  8 | Harry    |    47673 | 1978-06-13 | Cancer
  9 | Ingrid   |    47607 | 1991-12-12 | Cancer

Мы можем построить представление на основании этой таблицы, в котором будут исключены некоторые столбцы (SSN и name) и обобщить почтовый индекс и дату рождения следующим образом:

CREATE VIEW anonymized_patient AS
SELECT
    'REDACTED' AS lastname,
    anon.generalize_int4range(zipcode,100) AS zipcode,
    anon.generalize_tsrange(birth,'decade') AS birth,
    disease
FROM patient;

Анонимизированная таблица теперь выглядит так:

SELECT * FROM anonymized_patient;
 lastname |   zipcode     |           birth             |    disease
----------+---------------+-----------------------------+---------------
 REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Heart Disease
 REDACTED | [47600,47700) | ["1950-01-01","1960-01-01") | Heart Disease
 REDACTED | [47600,47700) | ["1980-01-01","1990-01-01") | Heart Disease
 REDACTED | [47900,48000) | ["1990-01-01","2000-01-01") | Flu
 REDACTED | [47900,48000) | ["1990-01-01","2000-01-01") | Heart Disease
 REDACTED | [47900,48000) | ["1960-01-01","1970-01-01") | Cancer
 REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Heart Disease
 REDACTED | [47600,47700) | ["1970-01-01","1980-01-01") | Cancer
 REDACTED | [47600,47700) | ["1990-01-01","2000-01-01") | Cancer

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

Postgres Pro предлагает несколько диапазонных типов данных, которые идеально подходят для дат и числовых значений.

Для числовых значений доступны следующие функции:

generalize_int4range(value, step)
generalize_int8range(value, step)
generalize_numrange(value, step) #

где value — данные, которые будут обобщены, а step — размер каждого диапазона.

G.1.6.10. Создание собственных масок #

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

Например, если вы написали функцию foo() внутри схемы bar, вы можете применить её следующим образом:

SECURITY LABEL FOR anon ON SCHEMA bar IS 'TRUSTED';

SECURITY LABEL FOR anon ON COLUMN player.score
IS 'MASKED WITH FUNCTION bar.foo()';

Примечание

Схема bar должна быть объявлена суперпользователем как TRUSTED.

G.1.6.10.1. Создание функции маскирования для столбца JSONB #

Для сложных типов данных вам, возможно, придётся создать собственную функцию. Этим приёмом придётся часто пользоваться, если вам нужно скрыть определённые части поля JSON.

Например:

CREATE TABLE company (
  business_name TEXT,
  info JSONB
)

Поле info содержит неструктурированные данные, как показано ниже:

SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';
           jsonb_pretty
----------------------------------
 {
     "employees": [
         {
             "lastName": "Doe",
             "firstName": "John"
         },
         {
             "lastName": "Smith",
             "firstName": "Anna"
         },
         {
             "lastName": "Jones",
             "firstName": "Peter"
         }
     ]
 }
(1 row)

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

SECURITY LABEL FOR anon ON SCHEMA custom_masks IS 'TRUSTED';

CREATE FUNCTION custom_masks.remove_last_name(j JSONB)
RETURNS JSONB
VOLATILE
LANGUAGE SQL
AS $func$
SELECT
  json_build_object(
    'employees' ,
    array_agg(
      jsonb_set(e ,'{lastName}', to_jsonb(anon.fake_last_name()))
    )
  )::JSONB
FROM jsonb_array_elements( j->'employees') e
$func$;

Затем проверьте правильность работы функции:

SELECT custom_masks.remove_last_name(info) FROM company;

Если всё в порядке, вы можете объявить эту функцию как маску поля info:

SECURITY LABEL FOR anon ON COLUMN company.info
IS 'MASKED WITH FUNCTION custom_masks.remove_last_name(info)';

И использовать её:

SELECT anonymize_table('company');
SELECT jsonb_pretty(info) FROM company WHERE business_name = 'Soylent Green';
            jsonb_pretty
-------------------------------------
 {
     "employees": [                 +
         {                          +
             "lastName": "Prawdzik",+
             "firstName": "John"    +
         },                         +
         {                          +
             "lastName": "Baltazor",+
             "firstName": "Anna"    +
         },                         +
         {                          +
             "lastName": "Taylan",  +
             "firstName": "Peter"   +
         }                          +
     ]                              +
 }
(1 row)

Это самый простой пример. Как видите, управлять сложной структурой JSON с помощью языка SQL возможно, но поначалу это может быть сложно. Существует несколько способов обхода ключей и обновления значений. Вам, вероятно, придётся попробовать разные подходы в зависимости от ваших реальных данных JSON и производительности, которую вы хотите достичь.

G.1.7. Статическое маскирование #

Иногда полезно напрямую преобразовать исходный набор данных. Сделать это можно разными способами:

Все эти способы уничтожают исходные данные. Используйте их с осторожностью.

G.1.7.1. Применение правил маскирования #

Вы можете навсегда применить правила маскирования базы данных, используя функцию anon.anonymize database().

Рассмотрим простой пример:

CREATE TABLE customer (
  id SERIAL,
  full_name TEXT,
  birth DATE,
  employer TEXT,
  zipcode TEXT,
  fk_shop INTEGER
);

INSERT INTO customer
VALUES
(911,'Chuck Norris','1940-03-10','Texas Rangers', '75001',12),
(312,'David Hasselhoff','1952-07-17','Baywatch', '90001',423)
;

SELECT * FROM customer;

 id  |   full_name      |   birth    |    employer   | zipcode | fk_shop
-----+------------------+------------+---------------+---------+---------
 911 | Chuck Norris     | 1940-03-10 | Texas Rangers | 75001   | 12
 112 | David Hasselhoff | 1952-07-17 | Baywatch      | 90001   | 423
  1. Объявите правила маскирования:

    SECURITY LABEL FOR anon ON COLUMN customer.full_name
    IS 'MASKED WITH FUNCTION anon.fake_first_name() || '' '' || anon.fake_last_name()';
    
    SECURITY LABEL FOR anon ON COLUMN customer.employer
    IS 'MASKED WITH FUNCTION anon.fake_company()';
    
    SECURITY LABEL FOR anon ON COLUMN customer.zipcode
    IS 'MASKED WITH FUNCTION anon.random_zip()';
  2. Замените исходные данные в замаскированных столбцах:

    SELECT anon.anonymize_database();
    
    SELECT * FROM customer;
    
     id  |  full_name  |   birth    |      employer       | zipcode | fk_shop
    -----+-------------+------------+---------------------+---------+---------
     911 | Jesse Kosel | 1940-03-10 | Marigold Properties | 62172   |      12
     312 | Leolin Bose | 1952-07-17 | Inventure           | 20026   |     423

Вы также можете использовать функции anonymize_table() и anonymize_column() для удаления данных из подмножества строк базы данных:

SELECT anon.anonymize_table('customer');
SELECT anon.anonymize_column('customer','zipcode');

Важно

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

G.1.7.2. Перестановка #

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

anon.shuffle_column(shuffle_table regclass, shuffle_column name, primary_key name) #

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

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

Важно

shuffle_column() не является маскирующей функцией, потому что она работает «вертикально»: она изменит все значения столбца сразу.

G.1.7.3. Добавление искажения в столбец #

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

anon.add_noise_on_numeric_column(table regclass, column text, ratio float) #

Если параметр ratio = 0.33, все значения столбца будут случайным образом смещены с коэффициентом +/- 33%.

anon.add_noise_on_datetime_column(table regclass, column text, interval interval) #

Если параметр interval = 2 дня, все значения столбца будут случайным образом сдвинуты на +/- 2 дня.

Важно

Эти функции искажения уязвимы для атак повторного воспроизведения.

G.1.8. Динамическое маскирование #

Вы можете скрыть некоторые данные от роли, объявив эту роль как «MASKED». Другие роли по-прежнему будут иметь доступ к исходным данным.

CREATE TABLE people (id TEXT, firstname TEXT, lastname TEXT, phone TEXT);
INSERT INTO people VALUES ('T1','Sarah', 'Connor','0609110911');
SELECT * FROM people;

SELECT * FROM people;
 id | firstname | lastname |   phone
----+----------+----------+------------
 T1 | Sarah    | Connor    | 0609110911
(1 row)
  1. Активируйте механизм динамического маскирования:

    SELECT anon.start_dynamic_masking();
  2. Объявите недоверенного пользователя:

    CREATE ROLE skynet LOGIN;
    SECURITY LABEL FOR anon ON ROLE skynet
    IS 'MASKED';
  3. Объявите правила маскирования:

    SECURITY LABEL FOR anon ON COLUMN people.lastname
    IS 'MASKED WITH FUNCTION anon.fake_last_name()';
    
    SECURITY LABEL FOR anon ON COLUMN people.phone
    IS 'MASKED WITH FUNCTION anon.partial(phone,2,$$******$$,2)';
  4. Подключитесь к серверу от имени недоверенного пользователя:

    \c - skynet
    SELECT * FROM people;
     id | firstname | lastname  |   phone
    ----+----------+-----------+------------
     T1 | Sarah    | Stranahan | 06******11
    (1 row)

G.1.8.1. Изменение типа замаскированного столбца #

Когда активировано динамическое маскирование, запрещено изменять тип данных столбца, если на нём есть маска.

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

BEGIN;
SELECT anon.stop_dynamic_masking();
ALTER TABLE people ALTER COLUMN phone TYPE VARCHAR(255);
SELECT anon.start_dynamic_masking();
COMMIT;

G.1.8.2. Удаление замаскированной таблицы #

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

DROP TABLE people;
psql: ERROR:  cannot drop table people because other objects depend on it
DETAIL:  view mask.company depends on table people

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

DROP TABLE people CASCADE;

G.1.8.3. Снятие маски с роли #

Просто удалить защитную метку можно следующим образом:

SECURITY LABEL FOR anon ON ROLE bob IS NULL;

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

SELECT anon.remove_masks_for_all_roles();

G.1.8.4. Ограничения #

G.1.8.4.1. Отображение списка таблиц #

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

Это связано с тем, что расширение подменяет search_path для недоверенных ролей.

Вы можете попробовать явно добавить схему, которую хотите найти, например:

\dt *.*
\dt public.*
G.1.8.4.2. Поддержка только одной схемы #

Система динамического маскирования работает только с одной схемой (по умолчанию — public). Когда вы запускаете механизм маскирования с помощью start_dynamic_masking(), вы можете указать схему, которая будет маскироваться, следующим образом:

ALTER DATABASE foo SET anon.sourceschema TO 'sales';

Затем откройте новый сеанс с базой данных и введите:

SELECT start_dynamic_masking();

Однако статическое маскирование функцией anon.anonymize() и анонимный экспорт функцией anon.dump() могут работать с несколькими схемами.

G.1.8.4.3. Производительность #

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

G.1.8.4.4. Графические инструменты #

Когда вы используете недоверенную роль с графическим интерфейсом, таким как DBeaver или pgAdmin, панель data может выдавать следующую ошибку при попытке отобразить содержимое замаскированной таблицы с именем foo:

SQL Error [42501]: ERROR: permission denied for table foo

Это связано с тем, что большинство этих инструментов будут напрямую запрашивать таблицу public.foo вместо того, чтобы «перенаправлять» механизм маскирования на представление mask.foo.

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

Открыть панель запросов SQL и ввести SELECT * FROM foo

Перейти к Database > Schemas > mask > Views > foo

G.1.9. Выгрузка анонимизированных данных #

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

G.1.9.1. pg_dump_anon.sh #

Скрипт pg_dump_anon.sh поддерживает большинство параметров обычной команды pg_dump. Также поддерживаются переменные среды (PGHOST, PGUSER и т. д.) и файлы .pgpass.

G.1.9.2. Пример #

Пользователь с именем bob может экспортировать анонимный архив базы данных app следующим образом:

/opt/pgpro/ent-17/bin/pg_dump_anon.sh -h localhost -U bob --password --file=anonymous_dump.sql app

Важно

Имя базы данных должно быть последним параметром.

Чтобы получить дополнительные сведения о поддерживаемых параметрах, просто введите ./pg_dump_anon.sh --help.

G.1.9.3. Ограничения #

  • Пароль пользователя запрашивается автоматически. Это означает, что вы должны либо добавить параметр --password, чтобы задавать его интерактивно, либо объявить его в переменной PGPASSWORD, либо поместить его в файл .pgpass (однако в Windows переменная PGPASSFILE должна быть указана явно)

  • Единственным поддерживаемым форматом является plain. Другие форматы (custom, dir и tar) не поддерживаются.

G.1.10. Обобщение #

G.1.10.1. Снижение точности конфиденциальных данных #

Идея обобщения состоит в том, чтобы заменить данные более широкими и менее точными значениями. Например, вместо «Бобу 28 лет» можно сказать «Бобу от 20 до 30 лет». Это полезно для аналитики, потому что данные остаются верными, избегая при этом риска идентификации.

Обобщение — это способ достижения k-анонимности.

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

G.1.10.2. Пример #

Рассмотрим таблицу с медицинскими данными:

SELECT * FROM patient;
     ssn     | firstname | zipcode |   birth    |    disease
-------------+-----------+---------+------------+---------------
 253-51-6170 | Alice     |   47012 | 1989-12-29 | Heart Disease
 091-20-0543 | Bob       |   42678 | 1979-03-22 | Allergy
 565-94-1926 | Caroline  |   42678 | 1971-07-22 | Heart Disease
 510-56-7882 | Eleanor   |   47909 | 1989-12-15 | Acne
 098-24-5548 | David     |   47905 | 1997-03-04 | Flu
 118-49-5228 | Jean      |   47511 | 1993-09-14 | Flu
 263-50-7396 | Tim       |   47900 | 1981-02-25 | Heart Disease
 109-99-6362 | Bernard   |   47168 | 1992-01-03 | Asthma
 287-17-2794 | Sophie    |   42020 | 1972-07-14 | Asthma
 409-28-2014 | Arnold    |   47000 | 1999-11-20 | Diabetes
(10 rows)

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

CREATE MATERIALIZED VIEW generalized_patient AS
SELECT
  'REDACTED'::TEXT AS firstname,
  anon.generalize_int4range(zipcode,1000) AS zipcode,
  anon.generalize_daterange(birth,'decade') AS birth,
  disease
FROM patient;

Это даст нам менее точное представление данных:

SELECT * FROM generalized_patient;
 firstname |    zipcode    |          birth          |    disease
-----------+---------------+-------------------------+---------------
 REDACTED  | [47000,48000) | [1980-01-01,1990-01-01) | Heart Disease
 REDACTED  | [42000,43000) | [1970-01-01,1980-01-01) | Allergy
 REDACTED  | [42000,43000) | [1970-01-01,1980-01-01) | Heart Disease
 REDACTED  | [47000,48000) | [1980-01-01,1990-01-01) | Acne
 REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Flu
 REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Flu
 REDACTED  | [47000,48000) | [1980-01-01,1990-01-01) | Heart Disease
 REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Asthma
 REDACTED  | [42000,43000) | [1970-01-01,1980-01-01) | Asthma
 REDACTED  | [47000,48000) | [1990-01-01,2000-01-01) | Diabetes
(10 rows)

G.1.10.3. Функции обобщения #

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

Для числовых значений:

anon.generalize_int4range(value integer, step integer) #

Например, anon.generalize_int4range(42,5) возвращает диапазон [40,45).

anon.generalize_int8range(value integer, step integer) #

Например, anon.generalize_int8range(12345,1000) возвращает диапазон [12000,13000).

anon.generalize_numrange(value integer, step integer) #

Например, anon.generalize_numrange(42.32378,10) возвращает диапазон [40,50).

Для значений времени:

anon.generalize_tsrange(value integer, step integer) #

Например, anon.generalize_tsrange('1904-11-07','year') возвращает ['1904-01-01','1905-01-01').

anon.generalize_tstzrange(value integer, step integer) #

Например, anon.generalize_tstzrange('1904-11-07','week') возвращает ['1904-11-07','1904-11-14').

anon.generalize_daterange(value integer, step integer) #

Например, anon.generalize_daterange('1904-11-07','decade') возвращает [1900-01-01,1910-01-01).

Возможные значения шагов: микросекунда, миллисекунда, секунда, минута, час, день, неделя, месяц, год, десятилетие, столетие и тысячелетие.

G.1.10.4. Ограничения #

G.1.10.4.1. Выделение и крайние значения #

Выделение — это возможность изолировать человека в наборе данных на основании экстремальных или исключительных значений.

Например:

SELECT * FROM employees;

  id  |  name          | job  | salary
------+----------------+------+--------
 1578 | xkjefus3sfzd   | NULL |    1498
 2552 | cksnd2se5dfa   | NULL |    2257
 5301 | fnefckndc2xn   | NULL |   45489
 7114 | npodn5ltyp3d   | NULL |    1821

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

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

Достичь этого можно посредством реализации принципа k-анонимности.

G.1.10.4.2. Обобщение несовместимо с динамическим маскированием #

По определению, при обобщении данные остаются верными, но изменяется тип столбца.

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

G.1.10.5. k-анонимность #

k-анонимность — это стандартный отраслевой термин, используемый для описания свойства анонимизированного набора данных. Принцип k-анонимности гласит, что в заданном наборе данных любого анонимизированного человека нельзя отличить как минимум от k-1 других людей. Другими словами, k-анонимность можно описать как гарантию «укрытия в толпе». Низкое значение k увеличивает риск идентификации на основании связи с другими источниками данных.

anon.k_anonymity(id regclass) #

Вы можете оценить k-фактор анонимности таблицы следующим образом:

  1. Определите столбцы, которые являются косвенными идентификаторами (также известными как квазиидентификаторы), например:

    SECURITY LABEL FOR anon ON COLUMN patient.firstname
    IS 'INDIRECT IDENTIFIER';
    
    SECURITY LABEL FOR anon ON COLUMN patient.zipcode
    IS 'INDIRECT IDENTIFIER';
    
    SECURITY LABEL FOR anon ON COLUMN patient.birth
    IS 'INDIRECT IDENTIFIER';
  2. После определения косвенных идентификаторов:

    SELECT anon.k_anonymity('generalized_patient')

    Чем выше значение, тем лучше.

G.1.11. Производительность #

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

В двух словах, эффективность анонимизации в основном будет зависеть от следующих важных факторов:

  • Размера базы данных

  • Количества правил маскирования

G.1.11.1. Статическое маскирование #

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

Примечание

В этом случае стоимость анонимизации «оплачивается» всеми пользователями, но оплачивается раз и навсегда.

G.1.11.2. Динамическое маскирование #

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

Если вы примените к таблице три или четыре правила, время отклика для недоверенных пользователей должно быть примерно на 20-30% больше, чем для обычных.

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

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

Примечание

В этом случае стоимость анонимизации «оплачивается» только недоверенными пользователями.

G.1.11.3. Выгрузка анонимизированных данных #

Если процесс резервного копирования вашей базы данных занимает один час с использованием pg_dump, то анонимизация и экспорт всей базы данных с использованием pg_dump_anon.sh ориентировочно займёт два часа.

Примечание

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

G.1.11.4. Ускорение процесса #

G.1.11.4.1. Используйте MASKED WITH VALUE, когда это возможно #

Заменить исходные данные статическим значением всегда быстрее, чем вызывать функции маскирования.

G.1.11.4.2. Материализованные представления #

Динамическое маскирование требуется не всегда. В некоторых случаях более эффективно создавать материализованные представления.

Например:

CREATE MATERIALIZED VIEW masked_customer AS
SELECT
    id,
    anon.random_last_name() AS name,
    anon.random_date_between('1920-01-01'::DATE,now()) AS birth,
    fk_last_order,
    store_id
FROM customer;

G.1.12. Безопасность #

G.1.12.1. Разрешения #

Ниже приведён обзор возможных действий пользователей в зависимости от имеющихся у них прав:

Таблица G.1. Права

ДействиеСуперпользовательВладелецНедоверенная роль
Создать расширениеДа
Удалить расширениеДа
Инициализировать расширениеДа
Сбросить параметры расширенияДа
Конфигурировать расширениеДа
Сделать роль недовереннойДа
Начать динамическое маскированиеДа
Остановить динамическое маскированиеДа
Создать таблицуДаДа
Объявить правило маскированияДаДа
Добавить, удалить, изменить строкуДаДа
Использовать статическое маскированиеДаДа
Получить реальные данныеДаДа
Выгружать данные без анонимизацииДаДа
Выгружать анонимизированные данныеДаДа
Использовать маскирующие функцииДаДаДа
Получить замаскированные данныеДаДаДа
Посмотреть правила маскированияДаДаДа

G.1.12.2. Ограничение маскирующих фильтров доверенными схемами #

Владельцам баз данных разрешено объявлять правила маскирования. Они также могут создавать функции, содержащие произвольный код, и использовать эти функции внутри правил маскирования. В определённых обстоятельствах владелец базы данных может «обмануть» суперпользователя, заставив его запросить замаскированную таблицу и тем самым выполнить произвольный код.

Чтобы предотвратить это, суперпользователи могут настроить следующий параметр:

anon.restrict_to_trusted_schemas = on

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

G.1.12.3. Контекст безопасности функций #

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

Данное расширение содержит ещё несколько функций, объявленных с использованием тега SECURITY DEFINER.