Как мы построили систему анализа утечек паролей с хранением в ScyllaDB

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

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

  • почта + пароль — между ними какой-то разделитель, то есть вариации на тему csv;

  • домен + почта + пароль — чаще всего так выглядят попавшие в открытый доступ файлы со стилеров либо с фишинговых ресурсов:

  • табличное представление дампов таблиц — экспортированные в csv записи целых таблиц пользователей, чаще всего в таком случае имеется не сам пароль, а его хэш

  • sql-дампы — появляются когда базу экспортировали в формате SQL, тогда искомые данные будут либо в опреациях INSERT, либо в COPY;

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

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

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

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

Архитектура системы

Общая архитектура системы представлена схеме:

a26ba25a40ae50769a7e9da758d4817e.png

Немного о технологиях. В качестве системы контроля задач и процессов обработки выбрана PrefectHQ. Позволяет в веб-интерфейсе увидеть статистику по запущенным flow по обработке и загрузке данных. Здесь же можно посмотреть, какие процессы завершены с ошибками, увидеть логи процессов. На нее переходили с Celery, проблем в переносе кода особо не было, зато наглядность заметно выросла. В Prefect можно задавать лимиты на конкурентное выполнение процессов, чем мы активно пользуется для контроля нагрузки на БД.

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

Доступ к файлам утечек выполняется с помощью Minio, который предоставляет S3 протокол для доступа к файлам. При любой обработке утечки воркер получает id утечки в качестве параметра запуска flow, далее выполняется получение записи утечки. Из записи получаем путь до файла утечки.

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

Для клиентов реализован легковесный API на Go, в котором собран только функционал, доступный нашим пользователям. К нему подключается фронтенд личного кабинета пользователя.

Обработанные данные хранятся в ScyllaDB. В ней используется один keyspace с таблицами для каждого индекса. В базу выполняется запись из воркера в PrefectHQ. Чтение данных для пользователей выполняет клиент на Go.

Разбор полученных файлов

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

  1. Определение, есть ли интересующая информация в файле

  2. Определение кодировки и разделителя (для sql-утечек преобразование в требуемый формат)

  3. Анализ содержимого столбцов для определения вида данных

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

Определение разделителей

Первый шаг в начале работы с валидным файлом утечки — понять, какой это тип файла. Если работа ведется с SQL-файлом, то задача сводится к разделению файла на таблицы, далее по каждой таблице из INSERT-выражений получаем значения для столбцов, складываем в удобный для разбора формат с разделителем между значениями. Наличие название столбцов может подсказать системе, какой вид данных в столбце.

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

Анализ содержимого столбцов

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

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

Загрузка и индексация данных

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

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

Следующей попыткой было использование RocksDB. Удобная встраиваемая база (скорее даже key-value хранилище). Есть поддержка для Go, работает в контексте сервера. В процессе реализации сервера для записи и чтения, проектировании логики индексации зародился последний подход с индексами. Оказалось не очень удобно для масштабирования системы, нужно было слишком много всего разрабатывать вокруг хотя бы для того, чтобы запустить второй инстанс. Было много сложностей в плане поддежки кода для понимания методов кодирования значений и логики индексов.

Сейчас система работает на ScyllaDB. СУБД запускается как отдельный процесс, есть клиентские драйверы для Python, Go, Java, C++, Rust и другие.

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

Проектирование индексов

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

fdedb3e821e2d83128a327148db05296.png

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

  • Partition Key — ключ раздела, по нему будет выполняться распределение записей между партициями (и соответственно по нодам)

  • Clustering Key — следующая часть ключа, по ней выполняется сортировка записей в пределах партиции

  • Static columns — остальная часть ключа, которая делает запись уникальной, при этом не изменяется

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

В PassLeak реализованы следующие таблицы для хранения утечек:

  • idx_email — записи для идентификатором является значение почты

  • idx_phone — записи где идентификатором является номер телефона

  • idx_username — записи для идентификатором является логин

  • leak_records — метаданные по утечкам, используются для обновления записей

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

Например, таблица индеса по email задана следующим образом:

CREATE TABLE idx_email (
  domain VARCHAR,
  value VARCHAR,
  password VARCHAR,
  raw_value VARCHAR,
  leak_time int,
  PRIMARY KEY(domain, value, password)
);

В столбце value хранится часть почты до домена

Такая структура таблицы позволяет выполнять поиск для следующих сценариев:

SELECT * FROM idx_email WHERE domain='test_domain.com';
SELECT * FROM idx_email WHERE domain='test_domain.com' AMD value='user_name';

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

Процесс записи

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

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

  • чтение файла утечки, разбивка строк по столбцам, валидация каждой строки;

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

  • добавление запросов в очередь;

  • при достижении необходимого количества запросов выполняется асинхронная операция записи.

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

В ScyllaDB при создании записи есть инструкция IF NOT EXISTS, которая позволяет избежать перезаписи данных если они уже есть. Удобно применять если есть необходимость сохранения первоначальной записи. В нашем случае данная инструкция применяется для контроля, что мы не перетираем более свежей утечкой данные из более старой (нужно понимать, когда связка логина/пароля встретилась впервые). По-умолчанию при записи данные будут перезаписаны

В итоге пришли к следующим выводам:

  • грузить нужно батчами, для себя подобрали размер в 100 тыс записей за один запрос;

  • выполнять загрузку асинхронными запросами, тогда во время ожидания операции можно готовить следующую пачку данных;

  • использовать prepared statements, позволило ускорить процесс загрузки.

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

Выводы

В результате у нас получилась стабильно работающая система, которая устраивает нас по скорости записи и чтения, а также по стабильности работы без сбоев. Процесс тюнинга записи оказался самым сложным и интересным, без погружения в структуру ScyllaDB, алгоритмов сжатия, устройства SSTable и прочего не обойтись.

© Habrahabr.ru