Как я искал ПДн в 300 базах данных [и сохранил рассудок]

image

Пришли как-то ко мне парни из службы безопасности и говорят: «Надо обойти все БД и собрать с них персональные данные». Потому что в России изменилось законодательство и теперь их нужно хранить в особо защищённых хранилищах.

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

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

Скорее всего, вам скоро предстоит такое же, поэтому сейчас покажу артефакты, которые я нашёл в процессе.

В общем, задача состояла в том, чтобы собрать персональные данные со всех БД для их переноса в защищённое хранилище. Наша команда Infra Operations выполняет регулярные операции, связанные с инфраструктурой. Есть простые операции, вроде предоставления прав доступа к БД или, напротив, их отчуждения у сотрудника при его переводе или увольнении. Но есть и сложные — например, создание приложения на Symfony.

Безопасники со своей проблемой пришли сначала к руководителю инфраструктуры. А он уже указал на меня — вот, мол, тот человек, который вам нужен. Потому что я 5 лет в Skyeng занимаюсь поддержкой со стороны инфраструктуры, в том числе автоматизацией рутинных операций. Вдобавок я люблю задачи, к которым не понятно как подойти. Все наши 300 баз данных нужно прошерстить — как это сделать? Непонятно.

А я сделал. Да ещё так, чтобы программа не перегружала прод и не теряла данные.

Декомпозиция задачи


Чтобы была понятна логика моей работы, сделаю небольшое отступление. Есть такие люди — полиглоты. Знающие, допустим, по 10 языков. Как выучить 10 языков? Ответ простой: зная 9 языков, освоить ещё один. Как выучить 9 языков? Зная 8 языков, освоить ещё 1. Рекурсивно до самого первого, родного языка.

То же самое и с базами данных. У нас 300 БД, в которых суммарно 10–10,5 тысячи таблиц.

Какие-то небольшие, какие-то просто огромные, на терабайт. Как просмотреть их все и найти персональные данные? Нужно найти их в одной БД. Как это сделать?

В каждой базе данных есть таблицы. В PostgreSQL или MySQL можно сделать запрос и получить список этих таблиц, а по каждой из них — информацию о количестве и типе столбцов, которые она содержит. Если это столбец типа integer — я искать в нём ничего не буду. Если он строковый — например char или varchar — это то, что нужно.

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

  • Нужно обработать каждую из 300 баз данных.
  • В каждой БД нужно пройти по всем таблицам, посмотреть, какие там есть поля.
  • Динамически собрать SQL-запрос типа SELECT COUNT (*) FROM *название конкретной таблицы* WHERE и далее список подходящих полей с фильтром. Подходящие — это строковые, например, varchar, text, в зависимости от движка БД.
  • Далее по регулярным выражениям с like или regexp выбираем строки, содержащие адрес электронной почты или номер телефона.


В России фиксированный телефонный план, то есть номер состоит из кода страны (8 или +7) и следующих за ним 10 цифр. Это как раз подходящий шаблон для автоматизированного поиска.

Можно было решить задачу и по-иному. Например, прийти к разработчикам и сказать: «Вот, у вашей команды есть 5 баз данных. Покажите, что вы в них храните, дайте описание или структуру данных».

Но это 5 баз данных, а у нас их в общей сложности 300 с лишним. Подходить к каждой команде разработчиков, а у нас их почти 40, и спрашивать, что у них хранится — долго и муторно. Да, я мог бы подойти к тимлиду с таким вопросом, и он либо ответил бы сам, либо поручил это какому-то подчинённому. То есть за меня бы эту задачу выполняли другие люди. Но всё равно на мне бы осталась эта масса коммуникаций. Это же с каждой командой надо встретиться — кто-то придёт на встречу, а за кем-то придётся бегать самому. Далеко не лучший вариант.

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

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

Алгоритм поиска


На бумаге решение выглядело довольно простым:

  • По сути, мне нужно было организовать цикл по каждой базе данных.
  • Внутри БД нужен цикл по каждой таблице.
  • Внутри таблицы нужен цикл по всем столбцам.
  • Далее отфильтровать столбцы подходящего типа и запустить по ним SQL-запрос.


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

Соответственно, для первых критерием отбора служат комбинации цифр. В России телефонный номер состоит из 10 цифр. Они бывают типов ABC и DEF. Первый — это привязанные к регионам. Другой — мобильные операторы. Мобильные телефонные номера начинаются на 9 (после кода страны). У региональных вариантов побольше — после девятки может стоять 2, 3, 4, 5 и 8. 6 и 7 гарантированно исключаются, потому что это казахстанские номера, а нам нужны были только российские.

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

К слову, некоторые коллеги сомневались в том, что задачу можно решить на Ansible. Мол, надо использовать «настоящий» язык программирования, а Ansible слишком медленный для обработки таких объёмов данных. Но я это отмёл сразу, потому что задача Ansible была только в создании SQL-запроса. А основная работа проходит внутри самой БД.

Что могло пойти (и пошло) не так


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

Как я уже говорил, у нас 300 баз данных, которые распределены по 40 хостам. Сами хосты также разбиты на отдельные группы по СУБД, которые на них используются. То есть хосты на PostgreSQL — это одна группа, на MySQL — другая и т.д.

Так вот, запустить Ansible playbook против всех 40 хостов сразу я не мог: мой рабочий комп просто это не вытягивал. Соответственно, нужно было обрабатывать хосты отдельными группами. В итоге я разделил все 40 хостов на пакеты по 10 случайных хостов в каждом. И запускал playbook последовательно по каждому пакету.

Второй нюанс — базы данных имеют разный размер. Соответственно, для каждой большой БД выделяется один хост, а если базы небольшие, то на одном сервере их могло храниться несколько штук. Чтобы обрабатывать их более равномерно, я использовал в Ansible такую штуку, как динамический инвентарь. В нём каждую БД представил отдельным хостом.

Мне удалось сделать так, чтобы обработка всех 300 баз данных не грузила прод. Вариантов здесь было два:

  • Обрабатывать все БД последовательно. То есть я сначала иду на 1 хост, смотрю, какие там есть базы данных, последовательно обрабатываю каждую из них. Это простое решение, но очень долгое. На все БД ушли бы недели, а то и месяцы.
  • Другой вариант — распараллелить. Я одновременно иду на каждый сервер и запускаю запросы для каждой базы данных, а точнее — для каждой таблицы в этих базах данных.


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

Очевидно, что второй вариант более быстрый, но вызывает неравномерную загрузку серверов.

Поэтому я поступил следующим образом:

  • Допустим, на сервере лежит 10 баз данных. Я прихожу на него и запускаю последовательно 10 запросов по одному на каждую БД. Я собираю со всех БД этого сервера все таблицы — и так для каждого сервера.
  • В результате у меня получается чуть более 10 тысяч таблиц. Каждую таблицу динамически добавляю в инвентарь как хост с параметрами её родной БД и родного хоста этой БД. Далее их перемешиваю и рандомно выбираю по 10 штук для пакетной обработки. У Ansible это штатная функциональность.


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

Если обрабатывать без перемешивания, то в какой-то момент может случиться так, что запрос всё же загрузит прод. Мой поиск срабатывает ночью раз в неделю в выходные и длится несколько часов — всё же 10 тысяч таблиц нужно обработать. У нас учатся студенты из разных уголков мира, кто-то из них живёт по московскому времени, другие находятся, допустим, в часовом поясе Нью-Йорка или Владивостока. И вот у кого-то в три часа ночи по Москве по случайному совпадению проходят уроки. В результате мой поиск накладывается на эти уроки — и прод начинает тормозить. Природа случая. Если не перемешивать таблицы перед поиском, то такая ситуация будет случаться постоянно. Если перемешивать — даже если один раз кто-то пострадает на своём уроке от моего поиска, то в следующий раз у него уже такой проблемы не будет.

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

Другая проблема, с которой мы столкнулись, — отсутствие результатов поиска персональных данных по БД, в которых они точно были. Мы изначально искали адреса электронной почты и телефоны в текстовых полях. После того как мы запустили поиск, пришёл фидбэк от разработчиков — у нас в таких-то БД точно есть персональные данные, но поиск их не нашёл.

Оказалось, что ПДн у них лежат не в текстовых столбцах, а JSON и JSONB. Я просто добавил эти типы в поддержку поиска и решил проблему.

Были ещё случаи ложноположительного срабатывания поиска по телефонным номерам. Я изначально его настроил так, чтобы он искал десятизначные комбинации цифр. Но такие сочетания могут быть не только телефонными номерами. Чтобы избежать ложных срабатываний, я добавил чёрный список таблиц, где персональных данных точно нет, там искать не надо. Аналогично можно исключить из поиска большие таблицы, где тоже ПДн не содержатся, — например, те, где хранятся логи AWX или Kubernetes. Заглядывать в них бессмысленно. Но таких исключений на все 300 баз данных было по пальцам сосчитать.

Количество ложных срабатываний можно уменьшить путём тонкой настройки регэкспов.

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

Соответственно, всё равно остаётся вероятность того, что персональные данные утекут и компания нарвётся на штраф. Его размер зависит от количества утёкших персональных данных.

А именно снижение таких рисков — конечная цель всего проекта.

Так вот, поиск показал несколько мест вроде баз данных биллинга, которые резко выделяются по количеству найденных персональных данных. У нас есть CMDB — configuration management database. В ней мы аккумулируем все данные, найденные во всех БД, и распределяем их по командам разработки, от которых получаем обратную связь. Вот в таком табличном виде БД можно легко отсортировать по количеству персональных данных. А дальше берём топ-10 БД по этому критерию и работаем с ними. По закону Парето 20% подобных найденных мест будут содержать 80% всех проблем.

Потом безопасники с этими 20% «проблемных» БД пошли к разработчикам и сказали им, что вот из них нужно удалить персональные данные. А вот в других базах персональных данных слишком мало, можно просто забить. Допустим, есть БД на 10 тысяч записей, какие-то из них могут быть номерами телефонов. Но их мало — даже если произойдёт утечка, то штраф будет небольшой.

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

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

С учётом того, что такой алгоритм подразумевает вероятность утечки какой-то части ПДн, назвать его совершенным нельзя. С другой стороны — от него совершенство и не нужно. Требовалось найти самые проблемные БД, так сказать, иголки в стоге сена.

Как я понял, что сделал всё круто


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

Нужно было найти то, не знаю что, и не знаю, где и как. Но найти надо обязательно, иначе компании будет больно в финансовом плане. Как понять, что задача выполнена?

Мне в этом плане нравится Agile. Design —> Develop —> Deploy —> Get feedback:

  • получили негативный фидбэк — крутим колёсико дальше;
  • нет негативного фидбэка — задача выполнена.


image

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

Я уже говорил, работа с разными хостами и БД шла параллельно, они обрабатывались отдельными пачками, чтобы сократить время выполнения задачи. Когда это было сделано, я понял, что задача выполнена на 70%. По сути, она состояла из простых операций:

  • посмотреть, какие есть столбцы у таблицы;
  • отфильтровать столбцы нужного типа;
  • создать SQL-запрос для выбранных столбцов и запустить его.


Результат всей этой задачи — это количество найденных поиском строк. Да, было примерно три доработки, связанные с ненужными нам данными вроде логов, цифровыми идентификаторами, похожими на телефонные номера, да с ПДн, помещёнными в нестроковые столбцы. Но мы это быстро пофиксили, и в целом алгоритм работает уже около года в изначальном виде и не приносит никаких проблем. Это и есть основной показатель эффективности моей работы. Допустим, если бы я запустил алгоритм один раз — в результатах были бы возможны какие-то значительные ошибки. Но он проработал около 50 раз за год по всем БД, и, если проблем за всё это время не было выявлено, то с высокой долей вероятности можно заключить, что их не будет и дальше.

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

Но это быстро доработали — и всё, фидбэк (вообще любой) закончился. Я вижу, что безопасники ходят то к одним, то к другим командам разработчиков с результатами моего поиска, говорят удалить ПДн то там, то здесь. А ко мне никаких пожеланий или предложений не приходит. Значит, я всё сделал круто.

© Habrahabr.ru