Внутренняя аутентификация YTsaurus Flow
Страница описывает, как компоненты Flow доказывают друг другу свою подлинность. Есть три независимые границы доверия, и для каждой используется свой механизм:
- Аутентификация прокси → контроллер — запросы извне, которые приходят в контроллер через RPC-прокси YTsaurus.
- Аутентификация внутри нод пайплайна — обращения нод одного пайплайна друг к другу (контроллер ↔ воркеры).
- Аутентификация раннер → контроллер напрямую — команды, которые раннер пайплайна отправляет самому контроллеру в обход RPC-прокси.
Источник истины — код: интерфейс IPipelineAuthenticator и его реализация в authenticator.cpp.
Аутентификация прокси → контроллер
Задача
Контроллер Flow поднимает внутренний RPC-эндпоинт (FlowExecute). Весь легитимный трафик до него проксируется через RPC-прокси, который уже аутентифицировал пользователя и проверил его права на пайплайн. Механизм подписи даёт контроллеру криптографическое доказательство того, что запрос:
- пришёл от RPC-прокси его собственного кластера YTsaurus (а не от подменённого источника);
- прошёл проверки авторизации на стороне прокси;
- адресован именно этому контроллеру и именно этому пайплайну.
Это защищает от подмены источника / прямого обращения к эндпоинту / SSRF: клиент, который может достучаться до RPC-порта контроллера напрямую, не сможет обойти прокси и подействовать на пайплайн без валидной подписи.
Схема
Прокси подписывает структурированные метаданные запроса и прикладывает их к RPC-запросу.
-
Метаданные запроса —
TControllerRequestMetadata, сериализованные в бинарный YSON. Поля:Method— фиксированный тегFlowExecute. Привязывает подпись к этому виду запроса, чтобы её нельзя было переиграть против другой подписанной операции YTsaurus.PipelineObjectId— Cypress-идентификатор адресуемого пайплайна. Это основная привязка: она привязывает перехваченную подпись к одному пайплайну, так что её нельзя переиспользовать против другого.ControllerAddress— адрес лидера-контроллера, который разрезолвил прокси. Само по себе это поле не даёт никаких гарантий безопасности — оно ни от чего не защищает. Оно присутствует, потому что осмысленно для взаимодействия прокси ↔ контроллер, и контроллер может на всякий случай использовать его как sanity-check.
-
Подпись — выпускается асимметричным ключом подписи кластера (YTsaurus,
ISignatureGenerator). Подписываются байтызаголовок || сериализованные метаданные. Заголовок (TSignatureHeader) несёт издателя, идентификатор ключа, идентификатор подписи и окно валидности (ValidAfter/ExpiresAt), поэтому каждая подпись короткоживущая. -
Транспорт — подпись едет в
TCustomMetadataExtзапроса под ключомControllerRequestMetadataSignatureKey. Сериализованные метаданные отдельно не передаются: они лежат внутри payload подписи.
Валидация (на стороне контроллера): проверить подпись против доверенных публичных ключей кластера, убедиться, что она не протухла, заново распарсить TControllerRequestMetadata из payload подписи и подтвердить, что Method и PipelineObjectId совпадают с обслуживаемым запросом (ControllerAddress может дополнительно проверяться как sanity-check).
Аутентификация одноразовая: каждый управляющий запрос Flow аутентифицируется заново. Долгоживущего соединения или рукопожатия, которое можно переиспользовать, нет, поэтому прокси генерирует свежую подпись на каждый запрос, а контроллер проверяет её на каждый запрос.
Основная привязка — PipelineObjectId: пользователь, который управляет подами пайплайна A, может перехватить подпись прокси к лидеру A, но не сможет переиграть её против другого пайплайна B — его контроллер отвергнет метаданные, чей идентификатор не совпадает с его собственным. Именно это не даёт перехваченной подписи обойти проверку прав на конкретный пайплайн, которую делает прокси.
Контроллер знает свой собственный ControllerAddress: это адрес, который контроллер публикует в Cypress (leader_controller_address) и который прокси резолвит оттуда. Адрес может меняться в динамических окружениях, но контроллер принимает только тот адрес, который сейчас анонсирует; если опубликованный адрес меняется на лету, прокси перерезолвит его, и с точки зрения Flow это уже новый лидер.
Примечание
Новый лидер-контроллер проверяет, что кластер может подключиться к нему через RPC-прокси: отправляет себе flow-команду по опубликованному адресу (подтверждение лидерства) и отказывается от лидерства, если она не доходит. Проверка пропускается в двух случаях:
- Автоматически, если кластер требует TLS для подключения к контроллеру, а у bus-сервера контроллера нет TLS-сертификата и ключа. Так сделано потому, что сейчас Flow не поддерживает работу в YTsaurus с включённым шифрованием.
- Если задана переменная окружения
YT_FLOW_SKIP_LEADER_PROXY_CONFIRMATION=1— обходной путь для тестирования, когда кластер вообще не может подключиться к контроллеру. Переменную задают в окружении процесса контроллера; для vanilla-операции — в окружении раннера, перечислив её вsecret_env.
В обоих случаях пайплайн обрабатывает данные, но пользовательские flow-команды (yt flow, SDK-клиенты) и UI не работают.
Модель угроз
Закрытые угрозы:
- Подмена источника / прямое обращение к эндпоинту / SSRF. Без приватного ключа кластера атакующий не может подделать подпись, поэтому запрос, дошедший до контроллера напрямую (в обход прокси), отвергается.
- Переигрывание между пайплайнами.
PipelineObjectIdпривязывает перехваченную подпись к её пайплайну. Это основная защита (см. выше). - Переигрывание между методами. Тег
Methodне даёт переиспользовать подпись Flow против другой подписанной операции YTsaurus, и наоборот. - Долгоживущее переигрывание. Окно валидности из заголовка ограничивает время, в течение которого перехваченная подпись остаётся пригодной.
ControllerAddress намеренно не входит в этот список: он не даёт гарантий безопасности и присутствует только ради взаимодействия прокси ↔ контроллер и опционального sanity-check.
Принятые угрозы / вне области защиты:
- Перехват на канале прокси → контроллер. Подпись запроса не шифрует канал. Если атакующий может наблюдать этот трафик, он может получить токен пользователя; используйте защищённый сетевой контур для этого соединения.
- Компрометация ключа подписи кластера. Вне области защиты; обрабатывается инфраструктурой подписей YTsaurus (ротация ключей, распространение доверенных публичных ключей).
Аутентификация внутри нод пайплайна
Задача
Ноды одного пайплайна общаются между собой по RPC: воркеры шлют контроллеру Handshake и Heartbeat, контроллер рассылает воркерам PushMessages, а также работают служебные сервисы (admin, orchid). Все эти обращения должны быть аутентифицированы, чтобы посторонний, дотянувшийся до RPC-порта ноды, не мог выдать себя за компонент пайплайна.
В отличие от границы прокси → контроллер, все ноды пайплайна запускаются вместе одной доверенной сущностью и с одними и теми же учётными данными (YTsaurus OAuth-токен). Поэтому здесь используется более простая симметричная схема на общем секрете — HMAC-тикеты.
Схема
Реализация — класс THmacTicketAuth в authenticator.cpp.
-
Общий секрет. Все ноды пайплайна выводят один и тот же статический секрет из учётных данных запуска:
- при OAuth —
<путь_пайплайна>,<OAuth-токен>.
Секрет нигде не передаётся по сети. Поскольку обе стороны выводят его одинаково, HMAC получается симметричным; посторонний без токена тикет подделать не может. Путь пайплайна в составе секрета привязывает тикеты к конкретному пайплайну, поэтому тикет нельзя переиграть против другого.
- при OAuth —
-
Тикет имеет формат
ytflow_hmac_v0:<секунды>:<sha256-hex>, где hex — этоSHA256(префикс + секунды + статический_секрет).<секунды>— момент выпуска тикета. -
Транспорт. Тикет кладётся в
TCustomMetadataExtRPC-запроса под ключомytflow-hmac. Инъекцию делает обёртка каналаTHmacTicketInjectingChannel(фабрика —CreateSelfCredentialsInjectingChannelFactory), так что вызывающему коду об аутентификации думать не нужно. -
Срок жизни. Тикет живёт 10 минут и перевыпускается на середине срока (раз в 5 минут).
Валидация (на стороне принимающего сервиса, THmacTicketAuthenticator): достать тикет из метаданных запроса, проверить, что момент выпуска отстоит от текущего не более чем на срок жизни (10 минут), пересчитать HMAC по своему секрету и сравнить. При успехе запрос аутентифицируется как пользователь root с realm ytflow_hmac. Сервисы подключают эту проверку через CreateSelfRpcAuthenticator.
Как и для прокси → контроллер, аутентификация одноразовая: свежий тикет вкладывается в каждый запрос и проверяется на каждый запрос. Отдельной конфигурации у этого механизма нет — он работает всегда, как только у пайплайна есть OAuth-токен.
Модель угроз
Закрытые угрозы:
- Подделка тикета посторонним. Без статического секрета (то есть без токена пайплайна) валидный HMAC вычислить нельзя.
- Переигрывание между пайплайнами. Путь пайплайна входит в секрет, поэтому тикет одного пайплайна не пройдёт валидацию в другом.
- Долгоживущее переигрывание. Окно в 10 минут ограничивает время, в течение которого перехваченный тикет остаётся пригодным.
Принятые угрозы / вне области защиты:
- Man-in-the-middle внутри пайплайна. Аналогично границе прокси → контроллер: атакующий, наблюдающий трафик между нодами, уже видит и может переиспользовать сами учётные данные, поэтому HMAC-тикеты против него ничего не добавляют.
Аутентификация раннер → контроллер напрямую
Раннер в прямом режиме минует RPC-прокси: он читает адрес лидера-контроллера из таблицы flow_control пайплайна (для этого нужно право read на пайплайн — как и на пути через прокси) и отправляет команду самому контроллеру. До прихода запроса в контроллер вызывающего никто не проверял, поэтому контроллер выполняет и аутентификацию, и авторизацию.
Схема
- Учётные данные. Раннер передаёт в запросе те же учётные данные YTsaurus, что и при обращении к прокси: токен. Новых секретов и подписей не вводится.
- Аутентификация. Раннер помечает прямой запрос записью
ytflow-directвTCustomMetadataExt, и контроллер отличает такие запросы от пересланных прокси по этой метке. Получив такой запрос, контроллер создаёт с его учётными данными клиент кластера пайплайна и вызывает через RPC-прокси командуget_current_user. Прокси проверяет токен той аутентификацией, что настроена на кластере (Cypress-токены, OAuth, IAM), и возвращает владельца; дальше запрос выполняется от его имени. Если раннер передал ещё и имя пользователя, прокси отвергает запрос, когда имя не совпадает с владельцем токена. Помеченный запрос без токена отвергается. - Авторизация. Затем контроллер своим клиентом вызывает
check_permissionдля этого пользователя на узел пайплайна:readдля запросов иwriteдля изменений. Без нужного права команда отвергается. Запросы, пришедшие через прокси, этот шаг пропускают: прокси уже сделал ту же проверку. Прямой запрос принимается только командойflow_execute: остальные методы контроллера, которые не знают требуемого права, отвергают запрос, помеченный как прямой. - Запрос без метки. Он не считается прямым и попадает к проверке подписи прокси; прямой режим этот путь не меняет. Контроллер с
require_proxy_signature = %trueотвергает такой запрос, потому что подписи нет. Контроллер сrequire_proxy_signature = %falseвыполняет его без аутентификации, как и до появления прямого режима, поэтому проверки прямого режима защищают только вместе сrequire_proxy_signature = %true.
Модель угроз
Закрытые угрозы:
- Запрос, помеченный как прямой, без учётных данных или с поддельным токеном. Кластер не называет владельца, и запрос отвергается до выполнения какой-либо команды.
- Повышение привилегий. Проверка прав использует ACL узла пайплайна — тот же, на который опирается путь через прокси.
- Подмена имени пользователя. Имя, переданное вместе с токеном, должно совпадать с владельцем токена, иначе кластер отвергает запрос.
Принятые угрозы / вне области защиты:
-
Токен виден контроллеру. Процесс пайплайна работает от имени владельца пайплайна и получает токен вызывающего; недобросовестный владелец пайплайна может его переиспользовать — например, подсунув пользователю свой пайплайн и собрав токены тех, у кого есть на него право
read. Прямой режим рассчитан на собственные или доверенные пайплайны.Это можно закрыть, если не передавать в контроллер учётные данные вызывающего. Вместо них прокси может выдавать клиенту подпись на конкретный запрос и короткий срок (несколько минут) — теми же асимметричными ключами, по которым контроллер уже проверяет запросы от прокси. Клиент идёт в контроллер с этой подписью, и контроллер доверяет ей так же, как пересланному запросу. Схема здесь не реализована.
-
Запрос без метки при
require_proxy_signature = %false. Контроллер выполняет его без аутентификации; это поведение пути через прокси, и прямой режим его не закрывает. Порт контроллера открывайте только вместе сrequire_proxy_signature = %true. -
Кластер без аутентификации. Кластер принимает имя пользователя из запроса как есть, поэтому проверка прав защищает не больше, чем на пути через прокси того же кластера.
-
Man-in-the-middle на канале раннер → контроллер. Как и для других границ: канал — обычный TCP, если
bus_serverконтроллера не настроен на TLS, так что наблюдающий трафик видит токен.