Как я искал ПДн в 300 базах данных [и сохранил рассудок]
Пришли как-то ко мне парни из службы безопасности и говорят: «Надо обойти все БД и собрать с них персональные данные». Потому что в России изменилось законодательство и теперь их нужно хранить в особо защищённых хранилищах.
Если этого не сделать, то рано или поздно данные могут утечь и ещё можно нарваться на высокие штрафы при утечке. Задача безопасников (и основная выгода от их наличия в компании) — минимизация таких рисков.
Вот только у нас несколько сотен баз данных, где-то около трёхсот. Даже если просто заглянуть в них и попытаться сделать выборку — это займёт весьма продолжительное время. И никто не имеет полной картины, где что хранится.
Скорее всего, вам скоро предстоит такое же, поэтому сейчас покажу артефакты, которые я нашёл в процессе.
В общем, задача состояла в том, чтобы собрать персональные данные со всех БД для их переноса в защищённое хранилище. Наша команда 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:
- получили негативный фидбэк — крутим колёсико дальше;
- нет негативного фидбэка — задача выполнена.
Но на обратную связь требуется время. То есть в моменте нельзя понять, выполнена задача или нет. Только спустя время можно оглянуться назад: да, получилось неплохо.
Я уже говорил, работа с разными хостами и БД шла параллельно, они обрабатывались отдельными пачками, чтобы сократить время выполнения задачи. Когда это было сделано, я понял, что задача выполнена на 70%. По сути, она состояла из простых операций:
- посмотреть, какие есть столбцы у таблицы;
- отфильтровать столбцы нужного типа;
- создать SQL-запрос для выбранных столбцов и запустить его.
Результат всей этой задачи — это количество найденных поиском строк. Да, было примерно три доработки, связанные с ненужными нам данными вроде логов, цифровыми идентификаторами, похожими на телефонные номера, да с ПДн, помещёнными в нестроковые столбцы. Но мы это быстро пофиксили, и в целом алгоритм работает уже около года в изначальном виде и не приносит никаких проблем. Это и есть основной показатель эффективности моей работы. Допустим, если бы я запустил алгоритм один раз — в результатах были бы возможны какие-то значительные ошибки. Но он проработал около 50 раз за год по всем БД, и, если проблем за всё это время не было выявлено, то с высокой долей вероятности можно заключить, что их не будет и дальше.
Я сделал вещь, которая работает целый год, и никаких жалоб на неё не поступало. Да, вначале был фидбэк, что там того-то нет, здесь другого не хватает, а в какие-то БД вообще лезть не надо.
Но это быстро доработали — и всё, фидбэк (вообще любой) закончился. Я вижу, что безопасники ходят то к одним, то к другим командам разработчиков с результатами моего поиска, говорят удалить ПДн то там, то здесь. А ко мне никаких пожеланий или предложений не приходит. Значит, я всё сделал круто.