---
metadata:
  - name: generator
    content: Diplodoc Platform v5.66.0
alternate:
  - https://ytsaurus.tech/docs/ru/flow/contributor/internal-authentication.md
  - href: https://ytsaurus.tech/docs/ru/flow/contributor/internal-authentication.md
    type: text/markdown
    title: Markdown version
  - href: https://ytsaurus.tech/docs/ru/llms.txt?revision=0cbf61d073ea8c5f76387586cca5192cd6c7011d
    rel: describedby
---
> **Documentation Index:** Fetch the complete configuration index at https://ytsaurus.tech/docs/ru/llms.txt?revision=0cbf61d073ea8c5f76387586cca5192cd6c7011d

<!-- source: ru/_includes/flow/contributor/internal-authentication.md -->
# Внутренняя аутентификация YTsaurus Flow

Страница описывает, как компоненты Flow доказывают друг другу свою подлинность. Есть три независимые границы доверия, и для каждой используется свой механизм:

1. [Аутентификация прокси → контроллер](#proxy-to-controller) — запросы извне, которые приходят в контроллер через RPC-прокси YTsaurus.
2. [Аутентификация внутри нод пайплайна](#inside-pipeline) — обращения нод одного пайплайна друг к другу (контроллер ↔ воркеры).
3. [Аутентификация раннер → контроллер напрямую](#client-to-controller) — команды, которые раннер пайплайна отправляет самому контроллеру в обход RPC-прокси.

Источник истины — код: интерфейс [`IPipelineAuthenticator`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/authenticator.h) и его реализация в [`authenticator.cpp`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/authenticator.cpp).

## Аутентификация прокси → контроллер {#proxy-to-controller}

### Задача {#proxy-goal}

Контроллер Flow поднимает внутренний RPC-эндпоинт (`FlowExecute`). Весь легитимный трафик до него проксируется через RPC-прокси, который уже аутентифицировал пользователя и проверил его права на пайплайн. Механизм подписи даёт контроллеру криптографическое доказательство того, что запрос:

- пришёл от RPC-прокси его собственного кластера YTsaurus (а не от подменённого источника);
- прошёл проверки авторизации на стороне прокси;
- адресован именно этому контроллеру и именно этому пайплайну.

Это защищает от **подмены источника** / **прямого обращения к эндпоинту** / **SSRF**: клиент, который может достучаться до RPC-порта контроллера напрямую, не сможет обойти прокси и подействовать на пайплайн без валидной подписи.

### Схема {#proxy-scheme}

Прокси подписывает структурированные метаданные запроса и прикладывает их к 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 это уже новый лидер.

{% note info %}

Новый лидер-контроллер проверяет, что кластер может подключиться к нему через RPC-прокси: отправляет себе flow-команду по опубликованному адресу (подтверждение лидерства) и отказывается от лидерства, если она не доходит. Проверка пропускается в двух случаях:

- Автоматически, если кластер требует TLS для подключения к контроллеру, а у bus-сервера контроллера нет TLS-сертификата и ключа. Так сделано потому, что сейчас Flow не поддерживает работу в YTsaurus с включённым шифрованием.
- Если задана переменная окружения `YT_FLOW_SKIP_LEADER_PROXY_CONFIRMATION=1` — обходной путь для тестирования, когда кластер вообще не может подключиться к контроллеру. Переменную задают в окружении процесса контроллера; для vanilla-операции — в окружении раннера, перечислив её в [`secret_env`](https://ytsaurus.tech/docs/ru/flow/devops/vanilla/security.md#secrets).

В обоих случаях пайплайн обрабатывает данные, но пользовательские flow-команды (`yt flow`, SDK-клиенты) и UI не работают.

{% endnote %}

### Модель угроз {#proxy-threat-model}

**Закрытые угрозы:**

- **Подмена источника / прямое обращение к эндпоинту / SSRF.** Без приватного ключа кластера атакующий не может подделать подпись, поэтому запрос, дошедший до контроллера напрямую (в обход прокси), отвергается.
- **Переигрывание между пайплайнами.** `PipelineObjectId` привязывает перехваченную подпись к её пайплайну. Это основная защита (см. выше).
- **Переигрывание между методами.** Тег `Method` не даёт переиспользовать подпись Flow против другой подписанной операции YTsaurus, и наоборот.
- **Долгоживущее переигрывание.** Окно валидности из заголовка ограничивает время, в течение которого перехваченная подпись остаётся пригодной.

`ControllerAddress` намеренно **не** входит в этот список: он не даёт гарантий безопасности и присутствует только ради взаимодействия прокси ↔ контроллер и опционального sanity-check.

**Принятые угрозы / вне области защиты:**

- **Перехват на канале прокси → контроллер.** Подпись запроса не шифрует канал. Если атакующий может наблюдать этот трафик, он может получить токен пользователя; используйте защищённый сетевой контур для этого соединения.
- **Компрометация ключа подписи кластера.** Вне области защиты; обрабатывается инфраструктурой подписей YTsaurus (ротация ключей, распространение доверенных публичных ключей).

## Аутентификация внутри нод пайплайна {#inside-pipeline}

### Задача {#inside-goal}

Ноды одного пайплайна общаются между собой по RPC: воркеры шлют контроллеру `Handshake` и `Heartbeat`, контроллер рассылает воркерам `PushMessages`, а также работают служебные сервисы (admin, orchid). Все эти обращения должны быть аутентифицированы, чтобы посторонний, дотянувшийся до RPC-порта ноды, не мог выдать себя за компонент пайплайна.

В отличие от границы прокси → контроллер, все ноды пайплайна запускаются вместе одной доверенной сущностью и с одними и теми же учётными данными (YTsaurus OAuth-токен). Поэтому здесь используется более простая симметричная схема на общем секрете — HMAC-тикеты.

### Схема {#inside-scheme}

Реализация — класс `THmacTicketAuth` в [`authenticator.cpp`](https://github.com/ytsaurus/ytsaurus/tree/main/yt/yt/flow/library/cpp/common/authenticator.cpp).

- **Общий секрет.** Все ноды пайплайна выводят один и тот же статический секрет из учётных данных запуска:

  - при OAuth — `<путь_пайплайна>,<OAuth-токен>`.


  Секрет нигде не передаётся по сети. Поскольку обе стороны выводят его одинаково, HMAC получается симметричным; посторонний без токена тикет подделать не может. Путь пайплайна в составе секрета привязывает тикеты к конкретному пайплайну, поэтому тикет нельзя переиграть против другого.

- **Тикет** имеет формат `ytflow_hmac_v0:<секунды>:<sha256-hex>`, где hex — это `SHA256(префикс + секунды + статический_секрет)`. `<секунды>` — момент выпуска тикета.
- **Транспорт.** Тикет кладётся в `TCustomMetadataExt` RPC-запроса под ключом `ytflow-hmac`. Инъекцию делает обёртка канала `THmacTicketInjectingChannel` (фабрика — `CreateSelfCredentialsInjectingChannelFactory`), так что вызывающему коду об аутентификации думать не нужно.
- **Срок жизни.** Тикет живёт 10 минут и перевыпускается на середине срока (раз в 5 минут).

Валидация (на стороне принимающего сервиса, `THmacTicketAuthenticator`): достать тикет из метаданных запроса, проверить, что момент выпуска отстоит от текущего не более чем на срок жизни (10 минут), пересчитать HMAC по своему секрету и сравнить. При успехе запрос аутентифицируется как пользователь `root` с realm `ytflow_hmac`. Сервисы подключают эту проверку через `CreateSelfRpcAuthenticator`.

Как и для прокси → контроллер, аутентификация одноразовая: свежий тикет вкладывается в каждый запрос и проверяется на каждый запрос. Отдельной конфигурации у этого механизма нет — он работает всегда, как только у пайплайна есть OAuth-токен.

### Модель угроз {#inside-threat-model}

**Закрытые угрозы:**

- **Подделка тикета посторонним.** Без статического секрета (то есть без токена пайплайна) валидный HMAC вычислить нельзя.
- **Переигрывание между пайплайнами.** Путь пайплайна входит в секрет, поэтому тикет одного пайплайна не пройдёт валидацию в другом.
- **Долгоживущее переигрывание.** Окно в 10 минут ограничивает время, в течение которого перехваченный тикет остаётся пригодным.

**Принятые угрозы / вне области защиты:**

- **Man-in-the-middle внутри пайплайна.** Аналогично границе прокси → контроллер: атакующий, наблюдающий трафик между нодами, уже видит и может переиспользовать сами учётные данные, поэтому HMAC-тикеты против него ничего не добавляют.

## Аутентификация раннер → контроллер напрямую {#client-to-controller}

Раннер в [прямом режиме](https://ytsaurus.tech/docs/ru/flow/tools/cli.md#direct-controller-commands) минует RPC-прокси: он читает адрес лидера-контроллера из таблицы `flow_control` пайплайна (для этого нужно право `read` на пайплайн — как и на пути через прокси) и отправляет команду самому контроллеру. До прихода запроса в контроллер вызывающего никто не проверял, поэтому контроллер выполняет и аутентификацию, и авторизацию.

### Схема {#client-scheme}

- **Учётные данные.** Раннер передаёт в запросе те же учётные данные 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`.

### Модель угроз {#client-threat-model}

**Закрытые угрозы:**

- **Запрос, помеченный как прямой, без учётных данных или с поддельным токеном.** Кластер не называет владельца, и запрос отвергается до выполнения какой-либо команды.
- **Повышение привилегий.** Проверка прав использует ACL узла пайплайна — тот же, на который опирается путь через прокси.
- **Подмена имени пользователя.** Имя, переданное вместе с токеном, должно совпадать с владельцем токена, иначе кластер отвергает запрос.

**Принятые угрозы / вне области защиты:**

- **Токен виден контроллеру.** Процесс пайплайна работает от имени владельца пайплайна и получает токен вызывающего; недобросовестный владелец пайплайна может его переиспользовать — например, подсунув пользователю свой пайплайн и собрав токены тех, у кого есть на него право `read`. Прямой режим рассчитан на собственные или доверенные пайплайны.

  Это можно закрыть, если не передавать в контроллер учётные данные вызывающего. Вместо них прокси может выдавать клиенту подпись на конкретный запрос и короткий срок (несколько минут) — теми же асимметричными ключами, по которым контроллер уже проверяет запросы от прокси. Клиент идёт в контроллер с этой подписью, и контроллер доверяет ей так же, как пересланному запросу. Схема здесь не реализована.
- **Запрос без метки при `require_proxy_signature = %false`.** Контроллер выполняет его без аутентификации; это поведение пути через прокси, и прямой режим его не закрывает. Порт контроллера открывайте только вместе с `require_proxy_signature = %true`.
- **Кластер без аутентификации.** Кластер принимает имя пользователя из запроса как есть, поэтому проверка прав защищает не больше, чем на пути через прокси того же кластера.
- **Man-in-the-middle на канале раннер → контроллер.** Как и для других границ: канал — обычный TCP, если `bus_server` контроллера не настроен на TLS, так что наблюдающий трафик видит токен.
<!-- endsource: ru/_includes/flow/contributor/internal-authentication.md -->
