Уроки, которые я извлек из работы на одном из самых посещаемых веб-сайтов на планете Земля.

Я консультировал большой веб-сайт (+ 200 миллионов уникальных посетителей в месяц). Такой уровень уязвимости сопряжен с большим риском различных атак, наиболее распространенной из которых является DDoS. Неназванная организация внедрила широкий спектр методов, чтобы такие атаки не повлияли на работоспособность службы.

Вышеупомянутые превентивные меры не являются чем-то необычным. Ключевым моментом является создание контента на граничных узлах (CDN, ESI) и множество уровней пассивного кеша.

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

Работая над этим проектом, я придумал решение, которое дает все те же преимущества пассивного кеша, не диктуя при этом основной дизайн сервиса.

Что такое пассивный кеш?

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

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

В этой настройке серверная часть кеша представляет собой хранилище ключей и значений (например, Redis), а источником истины является СУБД (например, Oracle Database).

Использование пассивного кеша для защиты от DDoS

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

Пример:

Http://gajus.com/blog/ - это служба блогов. Служба блога перечисляет статьи; клиент может получить доступ к отдельным статьям, используя PK статьи, например

В приведенном выше примере «1», «2», «8» и «9» - это идентификаторы ресурсов, используемые для идентификации данных в хранилище данных.

Вышеупомянутая служба использует активный кеш - когда клиент запрашивает статью с PK «1», служба просматривает хранилище кеша и возвращает результат или (если в кеше нет записи о запрошенном ресурсе) он запрашивает базу данных и сохраняет результат в кеш-хранилище в течение X времени.

Если злонамеренный пользователь создает атаку, предназначенную для выполнения HTTP-запроса на получение ресурса с PK «1», все эти запросы будут обслуживаться из бэкэнда кеша. Запрос записи из таблицы "ключ-значение" - операция с низким уровнем ресурсоемкости. Было бы дорого провести атаку в масштабе, когда поиск в хранилище кэша значений ключей становится узким местом.

Ситуация совсем иная, если атака использует произвольные значения для построения идентификатора ресурса, например. Значения ID в диапазоне от 1 до 1M. Теперь каждый запрос будет приводить к запросу к базе данных. В отличие от поиска по ключу, запросы к РСУБД дороги. Скорее всего, пакетам запроса / ответа потребуется пройти намного больше переходов, что ответ необходимо будет обработать с использованием логики приложения, результатов, сохраненных в хранилище кеша, и т. Д.

Масштабирование хранилища "ключ-значение" просто и дешево. Масштабирование СУБД может быть очень дорогим.

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

Разработка сервисов, использующих пассивный кеш

Служба, использующая пассивный кеш, должна:

  • Чтение данных только из кеш-хранилища.
  • Очередь записывает в источник правды после каждой операции создания, обновления и удаления.
  • Создайте объект ресурса для хранения в кеш-хранилище и поставьте задачу на обновление кеш-хранилища после каждой операции создания, обновления и удаления.

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

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

Однако, если вы читаете это, вы, вероятно, заботитесь о безопасности выше затрат. Хорошая новость заключается в том, что есть простой способ смягчить указанную выше атаку - сделать шаблон идентификатора ресурса непредсказуемым.

Подпись идентификаторов ресурсов

Причина, по которой шаблон активного кеша подвержен этой атаке, заключается в том, что шаблон идентификатора ресурса предсказуем. Независимо от того, является ли идентификатор числовым идентификатором (например, в предыдущем примере), идентификатором GUID в кодировке base64, например, используемым в API GraphQL или UUID, например, в большинстве баз данных, ориентированных на документы, проблема заключается в том, что когда сервер получает запроса, у него нет возможности узнать, существует ли ресурс - ему нужно сделать обратный путь либо к хранилищу кеша, либо к источнику истины. Есть простой способ решить эту проблему - подписать идентификатор ресурса.

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

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

Я использую этот шаблон при разработке идентификаторов ресурсов GraphQL. В частности, прокси, который направляет запрос в GraphQL, утверждает, что идентификатор ресурса действителен.

SGUID

Я произвел абстрактное создание и проверку подписанных идентификаторов с помощью пакета Node.js под названием sguid (подписанный GUID). Подписание идентификаторов осуществляется с помощью toSguid; проверка и открытие SGUID выполняется с помощью fromSguid:

import {
  fromSguid,
  InvalidSguidError,
  toSguid,
} from 'sguid';
const secretKey = '6h2K+JuGfWTrs5Lxt+mJw9y5q+mXKCjiJgngIDWDFy23TWmjpfCnUBdO1fDzi6MxHMO2nTPazsnTcC2wuQrxVQ==';
const publicKey = 't01po6Xwp1AXTtXw84ujMRzDtp0z2s7J03AtsLkK8VU=';
const namespace = 'gajus';
const resourceTypeName = 'article';
const generateArticleSguid = (articleId: number): string => {
  return toSguid(secretKey, namespace, resourceTypeName, articleId);
};
const parseArticleSguid = (articleGuide: string): id => {
  try {
    return fromSguid(publicKey, namespace, resourceTypeName, articleSguid).id;
  } catch (error) {
    if (error instanceof InvalidSguidError) {
      // Handle error.
    }
    throw error;
  }
};

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

SGUID использует систему подписи с открытым ключом Ed25519. Результирующая подпись кодируется с использованием URL-безопасного кодирования base64.

Обратной стороной этого подхода являются неудобные для пользователя идентификаторы, например

pbp3h9nTr0wPboKaWrg_Q77KnZW1-rBkwzzYJ0Px9Qvbq0KQvcfuR2uCRCtijQYsX98g1F50k50x5YKiCgnPAnsiaWQiOjEsIm5hbWVzcGFjZSI6ImdhanVzIiwidHlwZSI6ImFydGljbGUifQ

Плюс - масштабируемая защита от DoS-атак уровня 7 без ущерба для опыта разработчика.

Последние мысли

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

Часто имеет значение то, что атака выглядит неэффективной, а не то, что она неэффективна. Злоумышленник прекратит попытку, если не будет видимого стимула для продолжения.

Тебе нравится читать? Потому что я пишу

Вы можете поддержать мою работу с открытым исходным кодом и меня в написании технических статей через Buy Me A Coffee и Patreon. Моя вечная благодарность