До Cloud.ru я работал в компании, которая разрабатывала и внедряла корпоративные ИТ-решения для крупных заказчиков. Основным продуктом была коробочная платформа — корпоративная CRM, но с более широким набором модулей для работы с данными и справочниками, документами, отчетами, бизнес-процессами, картами, правами доступа и интеграциями с системами заказчика.
Платформа была не монолитом, а набором микросервисов, который мы разворачивали по классической коробочной модели: заказчик выделял нам несколько своих серверов, мы размещали на них сервисы, после чего платформа продолжала жить практически автономно.
Обычно такие внедрения были похожи друг на друга, но в одном проекте заказчик попросил развернуть платформу в своем облаке и встроить ее в существующий инфраструктурный контур. Для нас это стало стресс-тестом: часть платформы адаптировалась без проблем, а аутентификация уперлась в требование заменить привычный OIDC на Kerberos.
В статье разбираем, почему облачное внедрение вытащило наружу старую зависимость от OIDC, почему замена на Kerberos потребовала пересмотреть архитектуру и какие границы мы в итоге провели между системой управления доступом и бизнес-логикой.

У нас был OIDC, а заказчик хотел Kerberos
К моменту этого проекта платформу уже использовали у разных заказчиков. Ее основа оставалась общей, а под конкретное внедрение менялась только часть модулей и сценариев: где-то сильнее дорабатывали работу с картой, где-то подключали модули для документов, а где-то интегрировали специфичную ролевую модель над данными.
Обычно большая часть платформы уже была готова, и мы лишь дорабатывали ее под требования конкретного заказчика. Мы привыкли, что практически вся логика оставалась внутри нашей платформы, а использовать возможности инфраструктуры заказчика почти не приходилось.
Однако в этом проекте все оказалось иначе. Помимо развертывания платформы в облаке заказчика, нам нужно было отказаться от части собственного функционала и использовать вместо него механизмы заказчика. Одним из таких требований была замена привычного для нас OIDC на Kerberos. Пользователи уже работали в корпоративной доменной среде, входили в систему на компьютерах под своими учетными записями и не должны были повторно вводить пароль в каждой системе. Такой подход снижал зависимость безопасности от человеческого фактора.
Мы ошиблись в оценке: думали, что меняем только механизм входа, а в итоге затронули архитектуру. Задачу долго считали небольшой, поэтому к активной разработке подошли поздно.В итоге нам было нужно решение, которое легко встраивалось бы в существующий код, не требовало масштабной переработки сервисов и позволяло сохранить единый подход во всей платформе.
Почему простая замена не сработала
Если бы платформа была монолитной, задача, скорее всего, решилась бы гораздо быстрее, потому что изменения затронули бы значительно меньше кода. Но у нас была микросервисная платформа, которую предстояло интегрировать с внешним IAM-контуром заказчика. И если с входящими запросами все было относительно понятно, то передача пользовательского контекста между сервисами оказалась гораздо сложнее.
Упрощенно схема до внедрения Kerberos выглядела так:

В этой схеме микросервис заказов обращался к микросервису хранения файлов не просто от своего имени, а в контексте исходного пользователя. Это не сценарий Client Credentials, где сервисы взаимодействуют от своего технического имени, а информация об исходном пользователе следующему сервису не нужна. В нашей модели следующий сервис, наоборот, должен был понимать, от имени какого пользователя выполняется действие.
При использовании OIDC эта проблема почти не ощущалась. access token фактически выступал как переносимый артефакт: сервис получал его на входе и при необходимости передавал дальше, а следующий сервис восстанавливал пользовательский контекст из этого токена. Такой подход нельзя назвать идеальным с точки зрения OAuth 2.0/OIDC, но исторически платформа была построена именно вокруг такого допущения.
С Kerberos это допущение перестало работать. Нельзя было просто заменить access token из старой OIDC-схемы на сервисный билет Kerberos во всех местах и считать задачу решенной. Этот билет предназначен только для того сервиса, которому его предъявляет клиент. Передать его следующему микросервису так же, как bearer-токен, уже нельзя — для этого требуется отдельный сценарий делегирования.
Из-за этого задача уже не сводилась к простой замене заголовка в запросе. Нужно было выстроить механизм, который позволял бы безопасно передавать пользовательский контекст между сервисами.
Рабочим вариантом для нас стало делегирование. В упрощенном виде это выглядело так: если микросервис заказов должен был вызвать микросервис хранения файлов от имени исходного пользователя, то он сначала получал сервисный билет Kerberos для микросервиса хранения файлов, после чего вызов выполнялся уже с этим билетом.

Где OIDC успел проникнуть в архитектуру
Поначалу мы считали, что OIDC сосредоточен всего в нескольких местах: фильтрах аутентификации, получении текущего пользователя и отдельных участках межсервисного обмена. На практике зависимость оказалась гораздо глубже.
В разных микросервисах слой аутентификации был устроен по-разному. Где-то он только проверял пользователя, а где-то уже содержал прикладные проверки и специальные сценарии. Сервисы тоже получали данные текущего пользователя разными способами, часть прикладного кода напрямую зависела от данных, сформированных слоем аутентификации, а отдельные участки уже знали про детали OIDC и JWT.
Некоторые сервисы сами собирали пользователя из контекста аутентификации, межсервисные вызовы были завязаны на привычную токенную модель, а входящие запросы от соседних сервисов обрабатывались неодинаково. Например, в одних сервисах из токена извлекали отдельные поля и приводили логин к своему формату, в других сами решали, какие заголовки передавать дальше, а где-то между аутентификацией и прикладным кодом уже сформировался собственный внутренний контракт.
После такого разбора стало понятно, что заменить один протокол другим недостаточно. Сначала нужно было отделить прикладную логику от механизма аутентификации. Иначе зависимость от OIDC просто сменилась бы зависимостью от Kerberos.
Типичный пример нашей проблемы
Я попробовал описать этот хаос через диаграмму, но она быстро превратилась в нечитаемую схему. Поэтому дальше покажу ту же проблему на простом примере: что мы ожидали и что на самом деле получили.
Давайте представим, что у нас было какое-то приложение, в котором, как и во многих других, для оптимизации использовалось кеширование. Никакого регламента по кешированию не существовало — каждый делал так, как ему было удобно.
Проходит время. Вы проводите анализ и начинаете понимать, что огромное количество человеко-часов уходит не только на исправление багов, связанных с кешированием, но и на постоянную доработку функционала вокруг него. Вы решаете наконец навести в этом порядок. Но вы руководитель проекта и не знаете, что именно происходит внутри кода. Вы мыслите более абстрактно и начинаете искать решение, которое позволит исправить проблему с минимальными усилиями.
Самое очевидное решение — перейти на Managed Redis. Например, в Cloud.ru для такого сценария есть готовый управляемый сервис: большая часть инфраструктурных задач уже решена, поэтому со стороны кажется, что остается лишь заменить реализацию кэша.
Вы убеждены, что код выглядит так:

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

Каждый разработчик в свое время реализовал кеширование так, как ему казалось правильным. В результате вместо единой точки интеграции вы получили десятки разных реализаций, разбросанных по всему проекту.
Вместо того чтобы за несколько кликов поднять Redis-кластер и переписать всего один сервис, через который работает все приложение, вы понимаете, что не можете сделать вообще ничего. Стоимость такой переработки оказывается настолько высокой, что на нее просто не хватает ресурсов.
Именно в этом и заключается ценность хороших абстракций.
С аутентификацией у нас получилась похожая история. Разница была в том, что отказаться от перехода мы не могли — Kerberos был обязательным требованием заказчика. Поэтому пришлось не закрывать глаза на старые зависимости, а вытаскивать их из кода и проводить более четкую границу между прикладной логикой и инфраструктурой.
Как мы искали решение
Теоретически можно было сказать: «Давайте все перепроектируем правильно и сделаем платформу полностью готовой к любому облачному контуру». На практике такой вариант не подходил: на полную переработку не было времени, а попытка переписать все сразу могла сорвать приемку. Нам нужно было сохранить работающую платформу, выполнить обязательное требование заказчика и не сорвать проект.
Мы рассматривали несколько вариантов.
Kerberos поверх OIDC
Самый быстрый путь выглядел так: оставить существующую архитектуру и добавить Kerberos поверх нее через специфичные прослойки и проксирование. Такое решение, возможно, прошло бы на демонстрации, но для реальной сдачи проекта его никто бы не принял.
Единые правила без общей библиотеки
Другой вариант — договориться об архитектурных правилах и описать их в документации. Каждый сервис сам реализует работу с аутентификацией, но строго по общим паттернам.
Этот подход хорошо сохраняет независимость сервисов. Но в нашем случае он слишком сильно зависел от дисциплины и времени: достаточно нескольким командам начать решать задачи по-своему, и общий подход быстро перестал бы быть общим.
Универсальная платформа аутентификации
Можно было построить отдельную универсальную платформу аутентификации, которая закрыла бы все специфичные потребности микросервисов. На практике такие решения быстро начинают разрастаться. В результате получается универсальный сервис, который сложно расширять без риска что-то сломать и практически невозможно изменить без глубокого погружения в его внутреннее устройство.
Общий инфраструктурный слой с точками расширения
В итоге мы остановились на четвертом варианте.
Суть его была в том, чтобы вынести общую логику аутентификации и пользовательского контекста в инфраструктурный слой, а различия между сервисами оставить в заранее определенных точках расширения. Во многом это решение стало возможным благодаря тому, что в команде был опытный инженер, способный написать общий функционал, которым затем могла пользоваться вся остальная команда.
Наша цель состояла не в интеграции Kerberos как таковой. Мы хотели полностью отвязать прикладной код от конкретного способа аутентификации и получения пользовательского контекста. Для этого появился единый контракт работы с текущим пользователем. Все детали проверки запроса, формирования пользовательского контекста, подготовки межсервисных вызовов и работы с OIDC, JWT, Kerberos и другими механизмами остались внутри общего слоя.
При этом точки расширения позволили сохранить различия между сервисами, не распространяя их по прикладному коду.

Ответственность разделили между общим инфраструктурным слоем и конкретными микросервисами. Библиотека вела запрос по общему маршруту, а сервисы добавляли только свои отличия через заранее определенные точки расширения. Например, аудит сохранял способ входа, приводил данные к формату журнала и указывал, кто записывает событие, а файловое хранилище преобразовывало логин в свой формат.
Это была не идеальная архитектура на все случаи жизни. У нее тоже была цена: появлялась общая зависимость между сервисами, общий слой можно было случайно превратить в набор несвязанных частных случаев, а изменения в библиотеке нужно было аккуратно версионировать и раскатывать. Кроме того, плохо выбранные точки расширения могли снова протащить бизнес-логику в инфраструктуру.
Но в нашем случае плюсы были важнее. Такой подход позволял подключать разработчиков разного уровня: сложная логика проверки пользователя оставалась внутри общего слоя, а в сервисе нужно было описать только свое поведение в понятной точке расширения.
Что получилось в итоге
Дорогой оказалась не сама поддержка Kerberos. Дорогим оказалось исследование существующей платформы и тех неявных допущений, с которыми она раньше жила в более привычном контуре.
Почти каждое изменение начиналось не с написания нового кода, а с понимания того, как конкретный сервис определяет текущего пользователя, каким образом пользовательский контекст передается в следующий микросервис и где заканчивается инфраструктурная логика и начинается бизнес-логика.
Обычно работа выглядела одинаково: сначала мы находили все места, где использовался OIDC, затем разбирались, какая логика от него зависит, отделяли инфраструктурную часть от прикладной, приводили сервис к общему подходу и только после этого подключали Kerberos.
Главная сложность была в непредсказуемости. Каждый раз, когда казалось, что основные зависимости уже найдены, появлялся очередной сервис или сценарий, где OIDC использовался иначе.
Практический результат получился таким: мы внедрили Kerberos без полной переработки платформы и смогли пройти приемочные испытания в рамках проектных ограничений. Платформа встроилась в облачный контур заказчика без отдельной модели входа только для нашей платформы.
Типовые задачи по аутентификации перестали начинаться с изучения всех микросервисов: разработчику было достаточно подключить сервис к общей схеме, получить текущего пользователя и подготовить межсервисный вызов через общий механизм.
Новые особенности сервисов появлялись рядом с существующими точками расширения, а при следующей замене системы управления доступом основная работа осталась бы в общем инфраструктурном слое, а не разошлась бы по каждому микросервису.
Что мы вынесли из этой истории
Главный урок простой: чем меньше прикладной код знает об инфраструктуре, тем проще платформе переживать изменения.
Если разработчик оказывается в похожей ситуации, сначала стоит не бросаться переписывать код, а понять масштаб зависимости: где она реально используется, какие сценарии ломает и можно ли вообще отказаться от перехода. Иногда правильное инженерное решение неприятное: не идеальная переработка, а ограниченный общий слой, адаптер или временный компромисс, который позволяет не сорвать бизнес-задачу.
Если переход обязателен и время на переработку есть, зависимость лучше уводить за явный контракт. Прикладной код не должен знать детали протокола, сервисы должны работать через общий механизм, а различия нужно оставлять в точках расширения. Так команда защищает себя не только от текущей миграции, но и от будущих замен инфраструктуры.
Для облачных и гибридных проектов это особенно важно. Компоненты приложения должны быть заменяемыми и готовыми к интеграции со сторонними компонентами, поскольку полностью защититься от будущих изменений невозможно. Но можно заранее провести границу между прикладным кодом и деталями инфраструктуры.
В нашем случае именно это и оказалось главным уроком перехода с OIDC на Kerberos.

