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

Data Engineering Design Patterns, глава 7: Паттерны безопасности данных

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

Законы о приватности, например GDPR и CCPA, задают правила между владельцем данных и пользователем. Одно из них - право на удаление. Поэтому инженер данных думает не только о пайплайнах, но и о безопасности.

ПаттернКогда применять
Vertical PartitionerНужно удалять данные пользователя дёшево. Вы строите систему с нуля
In-Place OverwriterДанные уже лежат в старой системе. Переделать хранение нельзя
Fine-Grained Accessor for TablesПользователь видит таблицу, но не все её колонки и строки
Fine-Grained Accessor for ResourcesНужно дать задаче доступ только к своим облачным ресурсам
EncryptorНужно защитить данные на диске и в сети
AnonymizerНужно отдать данные наружу без возможности найти человека
Pseudo-AnonymizerНужно скрыть значения, но оставить пользу для аналитики
Secrets PointerВ коде не должно быть логинов и паролей
Secretless ConnectorМожно обойтись совсем без паролей

Пользователь просит удалить его данные. Нужно найти их и стереть. Есть два подхода.

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

Как работает:

  • При загрузке строка делится на две части по колонкам.
  • Неизменные и персональные атрибуты (PII) идут в одно хранилище. Каждый пишется один раз.
  • Остальные атрибуты (события, действия) идут в другое.
  • Общий ключ, например user_id, позволяет потом объединить части.

Пример на Spark: в foreachBatch делают два select и пишут результат в два топика Kafka. В Delta Lake удаление - это DELETE, а потом VACUUM, иначе старые файлы останутся. В Kafka для удаления шлют tombstone message: ключ и null. Работает при включённой compaction.

Компромиссы:

  • Данных для удаления мало, поэтому удаление дешёвое.
  • Читателю нужен join. Запросы сложнее и медленнее.
  • Помогает единая точка входа (view), каталог данных и lineage.
  • Сырые данные до разделения нужно хранить недолго. Иначе они нарушат удаление.

Проблема: Старая система хранит терабайты данных по временным партициям. Стратегии для персональных данных нет. Переделать всё нет ресурсов.

Как работает:

  • Если хранилище поддерживает DELETE, выполняют его с условием WHERE.
  • В Delta Lake и Iceberg после этого чистят старые файлы. Иначе работает time travel, и данные можно прочитать.
  • Для JSON и CSV читают весь набор, фильтруют записи пользователя и пишут результат.
  • Результат пишут сначала в staging area. Потом отдельная задача переносит его на место. Так сбой не ломает данные.
  • В Kafka читают топик и шлют tombstone для нужных ключей.

Компромиссы:

  • Много I/O. Нужно читать и переписывать много файлов.
  • Дороже, чем Vertical Partitioner. У пользователя 2000 событий: нужно переписать 2000 строк. В Vertical Partitioner хватило бы одной.
  • Объединяйте запросы на удаление в один запуск.
  • Откат сложен: исходные данные перезаписаны. Нужно версионирование хранилища.
  • Если срок хранения короче срока ответа на запрос, можно положиться на retention. Проверьте это с юристами.

Проблема: Доступа к таблице целиком недостаточно. Пользователь не должен видеть все колонки и все строки.

Как работает:

  • Доступ к колонкам: GRANT SELECT(col_a, col_b) ON table TO user (PostgreSQL, Redshift).
  • Доступ через теги в каталоге данных. Так работает BigQuery с policy tags.
  • Маскирование колонок: пользователь видит колонку, но значение скрыто (Databricks Unity Catalog, Snowflake).
  • Доступ к строкам: row-level security, row filter или row access policy. К запросу добавляется условие WHERE.
  • Если БД не умеет row-level, делают view с условием по current_user.
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
CREATE POLICY own_rows ON users USING (login = current_user);

Компромиссы:

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

Проблема: Аудит нашёл слишком широкие права. Одна задача может перезаписать все наборы данных в object store. Нужен принцип least privilege.

Как работает:

  • Подход через ресурс: политика привязана к ресурсу, например к бакету GCS.
  • Подход через identity: политика привязана к роли или пользователю (IAM role в AWS).
  • Права можно ограничить ресурсом, префиксом или условием, например по тегам.
  • Политики описывают через IaC, например Terraform.

Компромиссы:

  • Много мелких политик трудно поддерживать.
  • Wildcard (visits*) упрощает работу, но нарушает least privilege. Обсудите это с безопасниками.
  • Смесь двух подходов усложняет систему. Выберите один.
  • У облака есть квоты на число политик и ролей.

Проблема: Данные физически где-то лежат. Их могут украсть с серверов или перехватить в сети.

Как работает:

  • Data at rest: данные на диске шифруются. Клиентское шифрование: ключом управляет производитель. Серверное: ключами управляет сервис (AWS KMS, GCP KMS, Azure Key Vault).
  • При чтении хранилище просит ключ у хранилища ключей. Нет прав на ключ - нет данных.
  • Data in transit: безопасное соединение (TLS). Задают минимальную версию протокола, например 1.2.

Компромиссы:

  • Шифрование и расшифровка нагружают CPU.
  • Если потерять ключ, данные потеряны. Облака дают soft delete ключей с периодом восстановления.
  • Старые версии TLS нужно обновлять.

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

Как работает:

  • Удаление: колонки убирают (drop).
  • Perturbation: к значению добавляют шум.
  • Синтетическая замена: значение заменяют похожим случайным, например через библиотеку Faker.
  • Результат нельзя связать с человеком, даже при объединении с другими данными.

Компромисс: Информация теряется. Аналитика и ML-модели могут дать неверные выводы.

Проблема: После Anonymizer аналитики не могут отвечать на бизнес-вопросы. Им нужны значения в более удобной форме.

Как работает:

МетодЧто делает
MaskingЗаменяет часть символов, например XXX-XX-1000
TokenizationЗаменяет значение токеном. Связь хранится в защищённом vault
HashingНеобратимо заменяет значение хэшем
EncryptionШифрует значение. Владелец ключа может вернуть оригинал
GeneralizationЗаменяет значение диапазоном, например страна на регион

Компромиссы:

  • Защита слабее, чем у Anonymizer. Это ложное чувство безопасности.
  • Если объединить две таблицы, замаскированные поля могут вместе назвать человека.
  • Теряется информация и тип данных. Число становится диапазоном, то есть строкой.
  • Разные значения могут стать одинаковыми после маскирования.

Проблема: Логин и пароль лежат в коде или в репозитории. Они уже утекали раньше.

Как работает:

  • Секреты хранятся в secrets manager (AWS Secrets Manager, Google Secret Manager).
  • Код хранит только имя секрета, а не значение.
  • Во время выполнения потребитель запрашивает значение у сервиса.
  • Доступ защищён дважды: правами на secrets manager и самим секретом.
  • Имена секретов одинаковы во всех средах. Значения создаёт IaC.
client = boto3.client("secretsmanager")
password = client.get_secret_value(SecretId="pwd")["SecretString"]

Компромиссы:

  • Кэш секретов может устареть. Простое решение: пусть задача упадёт и перечитает секрет при перезапуске. Для этого нужна idempotency.
  • Секреты всё равно могут попасть в логи.
  • Секрет всё равно существует. Кто-то должен его безопасно создать.

Проблема: Команда не хочет управлять паролями и ключами API совсем.

Как работает:

  • Подход через IAM: сервис проверяет права вызывающего в IAM. Паролей нет.
  • Подход через сертификаты: центр сертификации (CA) проверяет сертификат клиента.
  • В AWS задача получает временные credentials через STS и роль. В GCP для задач Dataflow используют Service Account.

Компромиссы:

  • Настройка всё равно нужна. Это не “без работы”.
  • Сертификаты нужно ротировать. Во время ротации поддерживайте старые и новые, пока все клиенты не перейдут.
ТерминЗначение
PIIПерсональные данные, по которым можно найти человека
Tombstone messageЗапись с ключом и null для удаления в Kafka
Staging areaВременное место для результата до публикации
Least privilegeМинимум прав, нужных для работы
Row-level securityОграничение строк, которые видит пользователь
Column maskingСкрытие значения колонки для части пользователей
KMSСервис управления ключами шифрования
Secrets managerЦентральное хранилище паролей и ключей
IAMУправление доступом пользователей и сервисов в облаке
  • Разделяйте PII и события при загрузке. Тогда запрос на удаление затронет мало данных.
  • После DELETE в Delta Lake или Iceberg запускайте VACUUM или аналог. Иначе time travel вернёт данные.
  • Объединяйте запросы на удаление в один запуск. Это экономит I/O.
  • Давайте задачам отдельные роли с least privilege. Не используйте один общий ключ.
  • Для колонок с PII включайте маскирование или GRANT по колонкам. В dbt и в warehouse храните это как код.
  • Включайте шифрование по умолчанию на бакетах и задавайте минимальную версию TLS.
  • Не кладите пароли в Git. Используйте secrets manager или доступ через роль.
  • Проверьте, что секреты не попадают в логи.
  • Перед отдачей данных наружу решите: нужна полная анонимизация или хватит псевдонимизации. Проверьте риск объединения таблиц.
  1. Чем Vertical Partitioner дешевле In-Place Overwriter при удалении данных пользователя?
  2. Зачем после DELETE в Delta Lake нужен VACUUM?
  3. Чем Anonymizer отличается от Pseudo-Anonymizer? Почему псевдонимизация даёт ложное чувство безопасности?
  4. Что остаётся проблемой при Secrets Pointer, а что убирает Secretless Connector?

Предыдущая: Паттерны потока данных. Следующая: Паттерны хранения данных.