В наступающем году Namely Engineering планирует добавить более 100 новых микросервисов, чтобы идти в ногу с техническим ростом и развитием продуктов. И по мере роста Namely нам необходимо продолжать придерживаться лучших практик в области безопасности и обеспечивать безопасность по умолчанию. При этом нам необходимо реализовать безопасные механизмы для предоставления информации аутентификации службам таким образом, чтобы это было масштабируемым и поддерживаемым способом.

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

А именно до общей структуры аутентификации

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

Разработка нового привратника

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

  1. Масштабируемость и высокая производительность
  2. Предоставляет стандартный и единообразный метод аутентификации для всех служб.
  3. Рассматривает аутентификацию как действие по умолчанию, запретное действие.
  4. Минимизирует усилия, необходимые для использования системы аутентификации для новой службы

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

Мы называем эту систему аутентификацией на границе.

Как это работает?

Любой запрос, проходящий через наши ELB, проходит через наши прокси-серверы OpenResty в кластер Kubernetes. Наши прокси-серверы OpenResty аутентифицируют запрос, просматривая информацию о сеансе. Затем они создают и подписывают сообщение J SON W eb T (JWT) для конкретного запроса. Вышестоящая служба может использовать этот JWT для проверки личности пользователя и для извлечения другой информации о пользователе. Этот JWT будет виден только изнутри кластера и никогда не будет открыт для конечного пользователя, тем самым обеспечивая защищенный уровень безопасности.

OpenResty

OpenResty - это разновидность NGINX с добавленной поддержкой Lua, которая добавляет возможность предоставлять некоторую базовую логику внутри прокси-сервера.

Когда запрос поступает в nginx, модуль nginxauth_request используется для вызова настраиваемой библиотеки OpenResty Lua, которая проверяет наличие действительного сеанса в запросе. Если сеанс недействителен, он перенаправляет запрос на страницу аутентификации пользователя. В случае допустимого сеанса JWT создается с использованием переменных сеанса, и запрос пересылается на входной контроллер kubernetes для разрешения службы с добавлением JWT в качестве заголовка.

JWT

JSON Web Tokens - это отраслевой стандарт с открытым исходным кодом, изложенный в RFC 7519. Они используются как метод безопасного представления претензий между сторонами.

JWT состоит из трех строк в кодировке base64, представляющих поле заголовка, поле полезной нагрузки и подпись. Заголовок содержит информацию о чтении JWT (алгоритм подписи, тип токена). Поле полезной нагрузки содержит серию утверждений (пар ключ-значение). Подпись представляет собой цифровую подпись полей заголовка и полезной нагрузки.

Например, следующее:

{
  "alg": "HS256",
  "typ": "JWT"
},
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

станет закодированным JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

где последнее поле - это подпись первых двух полей. Поля полезной нагрузки зависят от приложения, хотя некоторые стандартные поля определены в RFC.

В Namely мы используем JWT в качестве источника информации для аутентификации. Благодаря этому стандартизированному методу распространения информации аутентификации нам не нужно поддерживать несколько заголовков или несколько вызовов хранилища сеансов для получения информации о пользователе.

Аутентифицированные и неаутентифицированные

Мы используем настраиваемую библиотеку сеансов, созданную поверх расширения для управления сеансами openresty, которое позволяет нам хранить информацию о сеансе в настраиваемом хранилище, таком как cookie (на стороне клиента) или Redis (на стороне сервера). После перехвата запроса код lua проверяет наличие двух свойств, чтобы проверить подлинность запроса. Запрос, если он аутентифицирован, должен иметь действующий настраиваемый файл cookie сеанса или заголовок сеанса. Если любой из этих двух присутствует, openresty вызывает наше собственное хранилище сеансов для проверки сеанса с использованием заданных критериев, включая, помимо прочего, срок действия и действительность пользователя.

Подробная информация о реализации

Мы использовали модуль nginx auth_request для аутентификации запросов. Общий блок для местоположения nginx выглядит так

location / {
  auth_request /auth-service;
  proxy_pass $ingress_upstream;
}

Модулю auth_request требуется, чтобы конечная точка /auth_service оценила запрос и вернула либо код состояния HTTP 401 для неаутентифицированных запросов, либо код состояния 200 для запросов аутентификации. Переменная ingress_upstream указывает на входной контроллер kubernetes, который разрешает восходящую службу, используя правила входа.

Местоположение auth_service инкапсулирует content_by_lua_block, который действует как «обработчик содержимого» и выполняет указанный код Lua. При перехвате запроса код lua проверяет наличие действительного настраиваемого файла cookie сеанса или заголовка сеанса для проверки подлинности запроса. Если любое из этих свойств присутствует, openresty вызывает наше собственное хранилище сеансов для проверки сеанса с использованием заданных критериев, включая, помимо прочего, срок действия, действительность пользователя и другую информацию о домене.

location /auth-service {
  internal;
  content_by_lua_block {
    local session_id = session.get_cookie()
    -- If no session cookie
    if not session_id or session_id == nil then
      ngx.status = 401
      ngx.exit(ngx.OK)
    end
    -- If session cookie is nil, check request headers
    if session_id == nil then
      session_id = ngx.var.http_x_shared_session_id
    end
    -- Obtain the JWT and check validity
    local res, err = session.get_JWT(session_id)
    if not jwtutil.validate_jwt(res.jwt.token) then
      ngx.status = 401
      ngx.exit(ngx.OK)
    end
    add_bearer_header(res.jwt.token)
    ngx.status = 200
    ngx.exit(ngx.OK)
  }
}

Безопасность по умолчанию

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

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

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

Первоначальные результаты

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

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

Будущая работа

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

В заключение, мы в восторге от гибкости, которую нам предоставила Authentication at the Edge. Openresty продемонстрировал высокую производительность, и размещение бремени аутентификации на самом внешнем уровне сделало границы нашей экосистемы чистыми и безопасными. А именно инженеры теперь могут разрабатывать услуги более быстро и расти, продолжая придерживаться лучших практик безопасности.

Заинтересованы в работе над другими подобными проектами? Посетите нашу Страницу вакансий!

Это было бы невозможно без помощи нескольких команд в Namely, которые позволили нам разработать это эффективно и совместно. Кроме того, я хотел бы поблагодарить Николаса Нарха и Мартина Кесса как двух других основных участников этого проекта. Наконец, я хотел бы поблагодарить Сида Гопинатха и Майка Хамра за их огромную помощь в написании этой статьи!