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



Мне довелось поучаствовать в работе над этим проектом в качестве консультанта. Посещаемость ресурса составляет порядка 200 миллионов уникальных пользователей в месяц. Такая популярность означает и высокий уровень рисков в сфере информационной безопасности, в частности, это риск подвергнуться различным видам атак, самые распространённые среди которых – DDoS. Организация, которую называть не буду, внедрила широкий спектр решений для предотвращения воздействия подобных атак на работоспособность сервиса.

Эти защитные системы вполне обычны. В их основе – сборка контента на граничных узлах (CDN, ESI) и применение многоуровневого пассивного кэша.

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

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

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


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

В подобной конфигурации система поддержки кэша – это хранилище данных в формате «ключ-значение» (например, Redis), а основной источник данных – это система управления реляционными базами данных (например, Oracle Database).

Сервис с активным кэшем сначала пытается прочесть данные из кэша, а если это не удаётся – обращается к основному источнику данных.

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


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

Например, http://gajus.com/blog/ — это служба блогов. Здесь размещают статьи. Клиент может получать доступ к отдельным статьям, используя их уникальные индексы. Вот примеры адресов статей:


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

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

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

Ситуация сильно меняется, если при атаке используются произвольные значения для конструирования идентификатора статьи, например, значения в диапазоне от 1 до 1 миллиона. Теперь каждый запрос приведёт к тому, что придётся обращаться к основной базе данных.

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

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

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

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


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

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

Каждую CRUD-операцию системы нужно реализовать с учётом вышеописанных ограничений.

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

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

Разрабатывать системы, использующие пассивный кэш, сложнее, но, когда на первом месте – безопасность, обычно на подобные сложности внимания не обращают. Однако, это не значит, что все способы защиты от DDoS-атак непременно потребуют огромных усилий. Выше мы рассматривали пример атаки на службу блогов путём перебора идентификаторов статей. Смягчить последствия подобных атак можно, сделав идентификаторы ресурсов непредсказуемыми.

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


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

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

Работает всё это так: сервис получает запрос, и пытается расшифровать идентификатор ресурса. Если ему это удаётся, расшифрованное значение используется для поиска запрошенной записи. Если же идентификатор не может быть расшифрован – обработка запроса завершается.

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

SGUID


Signed GUID, или sguid – это пакт для Node.js, в который я вынес процедуры создания и проверки подписанных идентификаторов. Подписать идентификатор можно с помощью команды toSguid. Для проверки и открытия подписанных идентификаторов используется команда 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

Плюс – масштабируемая защита от DDoS-атак, проводимых на прикладном уровне модели OSI, без чрезмерного усложнения процесса разработки.

Итоги


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

Кроме того, надо помнить о том, что в деле защиты от кибератак важно то, как результаты нападения выглядят с точки зрения злоумышленника. Для того, чтобы, перебирая идентификаторы ресурсов, «пробить» эффективно сконструированную систему, использующую описанный здесь подход, понадобится серьёзная мощность. Возможно, атакующий просто не рассчитывает на подобное, и видя, что система на его действия не реагирует (хотя она вполне может работать на пределе возможностей), решит, что он уже испробовал всё, что можно, и прекратит атаку.

А как вы защищаетесь от DDoS-атак?
Поделиться с друзьями
-->

Комментарии (14)


  1. maaGames
    10.02.2017 13:40
    +2

    Статический сайт и безызвестность — лучшая защита от DDOS.
    (половина шутки юмора)


    1. VolCh
      10.02.2017 15:22

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


      1. maaGames
        10.02.2017 18:16

        Меня пока только «школьники» докучали. Судя по логам, брутфорсили вордпресс… А у меня сайт не на вордпрессе.) Не знаю, считается ли это DDoS'ом, но php работать из-за нагрузки перестал, но статические страницы грузились без проблем. А вот от «взрослых» на моём тарифе сайт задохнётся.(
        Мне кэширование не помогло бы в любом случае. Единственная php страница считает загрузки, так что её кэшировать нельзя. А остальное итак плоский html.


  1. savostin
    10.02.2017 13:49

    А что мешает хранить все ID в дешевом «кэше», отдавать 404, если его там нет, и обращаться к серверу приложений, если ID есть, но данных в кэше нет?


    1. GadenbIsh
      10.02.2017 15:24
      -1

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


      1. savostin
        10.02.2017 15:55
        +1

        Как я понимаю, это «дешевые» запросы.
        Даже дешевле, чем запросы к кэшу для существующих ресурсов.


  1. zelenoff
    10.02.2017 14:21

    Поправьте меня, если я ошибаюсь:

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

    т.е. это актуально если мы не можем позволить себе такой большой кэш?


    1. qzark1
      10.02.2017 18:00

      Теперь каждый запрос приведёт к тому, что придётся обращаться к основной базе данных.

      Полагаю, тут больше проблема в нагрузке на заполнение кэша.


  1. SlimHouse
    10.02.2017 20:46
    +1

    А что мешает вытащить все урлы сайта и начать по ним активно "ходить"? То есть перебирать все корректные урлы. В этом случае данная проверка только увеличит нагрузку ведь.


    1. biziwalker
      11.02.2017 17:01

      Согласен, хороший DDoS тот, который легко спутать с поведением рядового пользователя


  1. zuborg
    11.02.2017 17:42

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

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

    Либо эти идентификаторы недоступны к генерации пользователем (т.е. они системные), но тогда их достаточно шифровать шифром с коротким блоком, например, для 32-х битных идентификаторов шифрование 56-битным DES увеличит представление идентификатора меньше чем в два раза, а вероятность попадания в нужный диапазон (после расшифровки) будет меньшей 2**(56-32)=2**24, т.е. меньше одной шестнадцатимиллионной.


  1. mrFleshka
    14.02.2017 16:47

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


    1. VolCh
      17.02.2017 09:48

      Сделать ключом кэша не только адрес ресурса, но и идентификатор пользователя?


      1. mrFleshka
        17.02.2017 13:34

        А потом пытаться на кончиках пальцев контролировать все изменения данных, текущих условий и т.д.?

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

        Пока доверяю простым запросам к БД, которые должны сами по себе кэшироваться и простой шаблонизации/сериализации, но только для AJAX запросов. Основные страницы всё же из кэша.

        А так да, можно сделать составные ключи, но я вот чувствую, что работать с ними слишком больно…