Skip to content

0077. Единая консоль площадок ​

Статус: proposed (этапы F1–F4 реализованы). Owner: Ian Panaev. Дата: 2026-10-14.

Контекст ​

Установка Arkvory редко одна: основная площадка, зеркала на других площадках (ADR 0058), установки с импортом по стадиям, узлы HA (ADR 0072), шлюзы чтения (ADR 0009). Сейчас каждая установка самостоятельна:

  • консоль работает только со своим API (connect-src 'self', один адрес);
  • источник не знает своих зеркал; процесс записи не показывает шлюзы чтения; узлы HA видны только через readiness активного узла;
  • всё, кроме запроса обновления, настраивается локальной командой arkvory configure / cluster-* от root на хосте; консоль показывает команды для копирования;
  • у каждой площадки свои пользователи и ключи (ADR 0058 намеренно не переносит учётные данные).

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

Решение ​

1. Управляющая площадка и реестр ​

Одна установка — управляющая (обычно основная). В её БД хранится реестр площадок: имя, адрес API, закреплённый отпечаток сертификата TLS (SHA-256 всего сертификата, см. уточнения F1), ключ управления площадкой (зашифрован AES-256-GCM ключом из ARKVORY_FLEET_KEY_FILE, файл 0600 в конфигурации установки), время последнего ответа и последний снимок состояния. Консоль по-прежнему ходит только к своему API; к площадкам ходит сервер управляющей установки. Отдельного сервиса нет.

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

2. Сопряжение ​

  1. На площадке глобальный администратор создаёт код сопряжения (консоль площадки или arkvory fleet-pair): одноразовый, 10 минут, показывается один раз.
  2. На управляющей площадке администратор вводит адрес площадки. Сервер получает сертификат и GET /health/status, показывает отпечаток и имя площадки для сверки с тем, что показала площадка вместе с кодом.
  3. Администратор вводит код. Управляющая площадка по закреплённому TLS вызывает POST /api/v1/system/fleet/enrollment с кодом и своим именем и отпечатком. Площадка создаёт учётные данные управления (новый вид credential fleet-key) и возвращает ключ один раз.

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

3. Права ключа управления ​

fleet-key — не сервисный ключ и не пользователь. Ему разрешён только список маршрутов:

  • чтение состояния площадки: GET /api/v1/system/topology (новый, глобальный администратор или fleet-key): роль, версия, схема, HA (копии, узлы), шлюзы чтения (аренды), зеркала и их курсоры, голова ленты изменений, обновления, резервные копии, очередь скачиваний, readiness;
  • чтение каталога: репозитории, артефакты, пакеты, файлы по путям, история, лента изменений — без байтов содержимого (этап F2);
  • запросы изменений (п. 5) и их состояние.

Каждый вызов несёт заголовок Arkvory-Fleet-Actor с именем администратора управляющей площадки: он пишется в аудит площадки рядом с fleet-key и не участвует в авторизации.

4. Опрос и состояние ​

Процесс записи управляющей площадки раз в 30 секунд (с разбросом, не больше 8 запросов одновременно, таймаут 10 с) читает topology каждой площадки и хранит последний снимок. Недоступная площадка — состояние unreachable с временем последнего ответа; ошибка TLS — certificate, отозванный ключ — revoked. Отставание зеркала считается на управляющей площадке, когда и источник, и зеркало сопряжены: голова ленты источника против курсора зеркала. Метрики: arkvory_fleet_site_up, arkvory_fleet_site_last_seen_timestamp_seconds, arkvory_fleet_mirror_lag_changes.

5. Удалённые изменения ​

Площадка ничего не выполняет из API: как обновления (ADR 0038), изменение — это запрос в локальный почтовый ящик, который применяет привилегированный агент площадки тем же кодом, что arkvory configure и cluster-*, с проверками, откатом и readiness. Запрос идемпотентен (id), содержит ожидаемую ревизию настроек, состояние читается тем же маршрутом.

  • Зеркала и импорт: подключить, отключить, сменить ключ источника, стадии импорта, сертификат источника. Ключ источника — секрет: он идёт отдельным файлом 0600 рядом с запросом, читается агентом и удаляется после применения; в самом запросе и состоянии его нет.
  • Обновления: запрос обновления, окно обслуживания, канал.
  • Очередь скачиваний (этап 3 ADR 0076): настройки хранятся в БД площадки с ревизией и применяются без перезапуска; переменные окружения остаются значениями по умолчанию.
  • HA: переключение активного узла и режим одной копии — только с подтверждением: администратор вводит имя целевого узла, площадка проверяет его по своему состоянию; запрос живёт 5 минут. Ограждение и вывод узлов остаются локальными командами.

6. Консоль ​

Новый раздел «Площадки» (только глобальный администратор):

  • карта: площадки и связи «источник → зеркало», «импорт», узлы HA и шлюзы внутри площадки, состояние цветом;
  • список площадок: роль, версия, доступность, копии HA, шлюзы, зеркала с отставанием, обновления, резервные копии, очередь скачиваний;
  • площадка — вкладки «Обзор», «Содержимое» (каталог, пакеты, файлы площадки в той же раскладке, что локальные страницы), «Зеркала», «Обновления», «Очередь скачиваний», «Кластер»; действия — кнопками с подтверждением и состоянием запроса;
  • управляющая площадка — первая в списке, тот же вид; локальные страницы остаются.

7. Безопасность ​

  • Только HTTPS к площадкам (HTTP — только loopback), без перенаправлений, с закреплённым отпечатком; проверку TLS не отключать.
  • Адрес площадки вводит глобальный администратор; разрешены внутренние адреса (площадки внутри сети), запросы идут только на адреса реестра.
  • Ключи площадок зашифрованы ключом установки; без файла ключа реестр недоступен, опрос останавливается с понятной ошибкой. Ключ не входит в резервную копию БД; реестр восстанавливается с пометкой «нужно повторное сопряжение».
  • Отзыв на площадке действует сразу; компрометация управляющей площадки компрометирует управление всеми площадками — это явный риск выбора серверного реестра, поэтому права fleet-key ограничены списком, а удаление данных и пользователей удалённо недоступно.
  • С этапа F3 ключ управления может подключить к площадке новый репозиторий-зеркало с источником, который выберет управляющая площадка, перевести кластер на другой узел и принять запись с одной копией. Захваченная управляющая площадка может так подсунуть содержимое в новый репозиторий площадки или снизить её долговечность на время решения. Что площадка доверяет (сертификаты центров источников), удалённо не меняется; существующие репозитории не перенаправляются (только новый репозиторий, отключение — без потери данных). Площадка, которой это не нужно, не сопрягается или отзывает управляющую; ограничение списка изменений на самой площадке — возможное развитие.

8. Этапы (каждый — отдельный PR) ​

  • F1. topology на каждой площадке, коды сопряжения и fleet-key, реестр и опрос на управляющей, раздел «Площадки» с обзором (только чтение).
  • F2. Содержимое площадок: каталог, пакеты, файлы через сервер управляющей площадки, только метаданные.
  • F3. Почтовый ящик изменений: зеркала и импорт, обновления, очередь скачиваний (вместе с этапом 3 ADR 0076), HA с подтверждением.
  • F4. Карта связей, отставание зеркал, оповещения и метрики.

Уточнения реализации (этап F1) ​

  • Отпечаток — SHA-256 всего сертификата (как показывают браузеры и openssl x509 -fingerprint -sha256), а не открытого ключа: администратор сверяет его где угодно. Площадка со встроенным HTTPS показывает свой отпечаток рядом с кодом сопряжения; за TLS-прокси сверяют с отпечатком, который браузер показывает для адреса площадки.
  • Проверка TLS не отключается. При знакомстве управляющая площадка один раз читает сертификат без доверия, ничего не отправляя, только чтобы показать отпечаток. Все запросы к площадке идут с закреплённым сертификатом как единственным якорем доверия (ca + allowPartialTrustChain): цепочка проверяется против него, и отпечаток должен совпасть. Перевыпущенный сертификат (даже с прежним ключом) — состояние certificate: площадку удаляют на управляющей, отзывают старую управляющую на самой площадке и сопрягают заново. Закрепление SPKI с переносом через перевыпуск — возможное развитие.
  • Ключ печати по умолчанию — fleet.key в каталоге данных, создаётся процессом записи при первом запуске (0600, исключительное создание). На HA он переезжает с данными; его нет в БД и резервных копиях; установщику и Compose ничего не нужно. ARKVORY_FLEET_KEY_FILE переопределяет путь. Шлюзы чтения ключа не создают и площадками не управляют.
  • fleet-key — новый вид учётных данных с белым списком маршрутов (в F1 — только GET /api/v1/system/topology); всё остальное — 403 permission_missing, даже при флаге администратора. Управлять площадками и создавать коды может только человек — глобальный администратор, не ключ управления.
  • Код — 60 бит в алфавите Crockford (XXXX-XXXX-XXXX), 10 минут, хранится только как SHA-256, обмен — DELETE … RETURNING, один раз. Повторное подключение того же адреса отклоняется до траты кода.
  • Опрос: процесс записи раз в 30 секунд, не больше 8 одновременно, 10 секунд и 1 МиБ на ответ; ответ проверяется по контракту до сохранения. Неудача сохраняет последний известный снимок.
  • Отставание зеркала считается на самом зеркале: голова источника, которую оно последний раз видело, минус его курсор. Сопрягать источник для этого не нужно.
  • Реестр, коды и управляющие — вне резервной копии: доверие устанавливается заново после восстановления.

Уточнения реализации (этап F2) ​

  • Один список маршрутов fleetReadRoutes в contracts: по нему площадка пускает fleet-key (только GET), и по нему же управляющая площадка проверяет path до запроса. Разойтись они не могут. Маршрутов с байтами (содержимое артефактов и файлов, загрузки) в списке нет; путь — абсолютный, не длиннее 2048 символов, без фрагмента, ../., %2e%2e, %2f и обратной косой черты; иначе 400 до любого сетевого запроса.
  • Права на площадке. fleet-key получает чтение репозиториев площадки — из журнала каталога (рекурсивный skip-scan) и из прав групп, так что видны и пустые репозитории; не больше 1000 по имени. Принципал ключа несёт флаг администратора, но он не даёт ничего сверх списка маршрутов: запрос fleet-key к маршруту вне списка отклоняется до обработчика (403 permission_missing), включая HEAD и другие шаблоны URL. Поэтому любой маршрут, добавляемый в список, — решение о правах управляющей площадки: обработчики, проверяющие флаг администратора, становятся ей доступны. Запись, удаление, пользователи и ключи остаются вне списка. Вторая проверка — на площадке: даже скомпрометированная управляющая площадка не получает байты через fleet-key.
  • Прокси без интерпретации. GET /api/v1/fleet/sites/{id}/read?path= возвращает статус и тело ответа площадки как есть (до 16 МиБ, тот же закреплённый TLS и таймаут); SDK client.fleet.site(id) даёт те же методы каталога, что локальный клиент, и проверяет ответы теми же читателями контракта. Ошибки транспорта: отозванный ключ → 409, отказ площадки → 403, недоступность и TLS → 503.
  • Консоль: кнопка «Содержимое» в строке площадки открывает репозитории, поиск артефактов и страницы файлов с пагинацией; данные площадки выводятся только как текст.

Уточнения реализации (этап F3, первая часть) ​

  • Без почтового ящика, где он не нужен. Очередь скачиваний (этап 3 ADR 0076) меняется в БД площадки и применяется её процессом записи без перезапуска, поэтому агент площадки для неё не нужен. Обновления уже идут через почтовый ящик ADR 0038 (POST /api/v1/system/updates/requests); управляющая площадка кладёт туда те же запросы. Почтовый ящик агента для зеркал и HA — следующая часть F3.
  • Список изменений fleetChangeRoutes в contracts, у каждого маршрута один метод: PUT /api/v1/system/downloads/settings, POST /api/v1/system/updates/requests и (вторая часть F3) POST /api/v1/system/changes. Площадка пускает к ним fleet-key, управляющая площадка проверяет путь до запроса (POST /api/v1/fleet/sites/{id}/change?path=) и пересылает тело как есть, без повторов. Оба изменения защищены на площадке ревизией или id запроса: повтор не применяется дважды, гонка — 409 от площадки.
  • Для чтения текущей ревизии в список чтения добавлены GET /api/v1/system/downloads/settings и GET /api/v1/system/updates.
  • Кто изменил. Площадка записывает в настройки очереди id ключа управления и Arkvory-Fleet-Actor (только печатный ASCII, до 160 символов). Имя не участвует в авторизации. Запрос обновления формата ADR 0038 поля автора не имеет; его отправитель виден в журнале доступа по id ключа управления (fleet:<id>).
  • Пределы прокси. Весь обмен с площадкой ограничен сроком (15 секунд, чтение каталога — 60), а не только тишиной сокета: площадка, отдающая байт раз в 9 секунд, не держит опрос и остановку. Одновременно — не больше 4 чтений/изменений на площадку и 16 всего (иначе 503 busy, Retry-After: 1): ответы до 16 МиБ лежат в памяти.
  • Проверка пути. Любое написание точечного сегмента (., .., %2e, .%2E) и закодированные / и \ отклоняются до запроса: разбор URL по пути к площадке не может превратить проверенный путь в другой.
  • Сертификат при знакомстве разбирается внутри обработчика с перехватом: нечитаемый сертификат — состояние certificate, а не падение процесса. Имя и версия площадки при сопряжении ограничены (печатный ASCII до 63 символов, версия до 64). 429 и 5xx при сопряжении — unreachable, а не «код отклонён».
  • used_at ключа управления обновляется не чаще раза в минуту: параллельные чтения одного управляющего не выстраиваются в очередь на строке.
  • Консоль. Кнопка «Настройки» в строке площадки и «Настройки этой площадки» открывают одну карточку с портом локального API или площадки: очередь (пустое поле — значение окружения, конфликт ревизии перечитывает форму) и обновления (час обслуживания, автоматическая установка, проверка, установка с подтверждением).

Уточнения реализации (этап F3, вторая часть: агент площадки) ​

  • Тот же почтовый ящик. Изменения зеркал и HA кладутся в почтовый ящик обновлений ADR 0038 (updates/inbox/change.json, состояние — updates/status/changes.json): каталоги, права и монтирование в контейнер уже есть. Тот же ежеминутный updates-poll под блокировкой установки после такта обновлений вызывает pollChanges; сбой одного не останавливает другое.
  • Виды изменений (readSiteChange, строгие поля): mirror-attach (только новый репозиторий: источник, репозиторий источника, стадии импорта или null), mirror-key (только подключённое зеркало), mirror-detach (каждое зеркало по имени, all не принимается), cluster-switchover (node и confirm обязаны совпасть), single-copy (until с поясом или null, причина). Агент применяет их кодом configureMirror, switchover, decideSingleCopy. Ограждение и вывод узлов остаются локальными командами.
  • Почтовый ящик враждебен. Входящий каталог доступен API на запись (под Compose — всем): любое имя там может оказаться ссылкой, каталогом или FIFO. Агент проверяет lstat до переноса: не обычный файл не переносится и отклоняется (ссылка, FIFO и сокет теряют только имя через unlink, каталог удаляется только пустым через rmdir). Обычный файл переносится в корень установки (пишет туда только root), проверяется ещё раз (обычный файл, одна ссылка; каталог, подменённый между проверкой и переносом, возвращается во входящий каталог под временным именем) и читается через дескриптор с O_NOFOLLOW|O_NONBLOCK и пределом размера. Ничего из входящего каталога не удаляется рекурсивно: пока root обходит дерево, подкаталог можно подменить ссылкой на чужое. API тоже открывает файлы почтового ящика с O_NONBLOCK: FIFO во входящем каталоге не занимает поток пула. Ключ передаётся в configureMirror значением, путь к нему не открывается. Иначе ссылка change.key на однострочный секрет хоста ушла бы заголовком Authorization на источник атакующего.
  • Ключ привязан к запросу: файл <id>.key 0600 пишется до запроса, агент берёт ключ только с именем своего запроса; API удаляет только ключ, который записал сам. Ключи и временные файлы, оставшиеся дольше часа, агент удаляет по имени.
  • Идемпотентность. id выбирает клиент, к запросу прилагается SHA-256 изменения (digest); агент помнит 20 последних. Тот же id с тем же изменением — тот же запрос, с другим — 409 idempotency_mismatch. Хешируемый текст строит одна чистая функция siteChangeDigestText в contracts (каноническое изменение и строка с SHA-256 ключа); API и агент хешируют его каждый своим node:crypto. Агент пересчитывает отпечаток с пришедшим ключом и отклоняет несовпадение (digest_mismatch): запрос или ключ, подменённые во входящем каталоге, не применяются под чужим id. Нужный, но не пришедший ключ остаётся key_missing. Имя автора обрезается до 200 символов, запрос проверяется контрактом до публикации.
  • Сроки. Изменения HA живут 5 минут (решение о кластере, каким он был при вопросе), зеркал — 30 минут (переживают обновление площадки). Запрос старше срока или «из будущего» дальше срока — failed: expired. Пока площадка ставит обновление (тот же такт), GET говорит updating, POST — 503 busy.
  • Прерванное изменение — барьер. Изменение, прерванное на полпути (процесс убит, команда не подтвердила завершение), записывается interrupted, статус получает blocked, и агент не берёт другие изменения до arkvory changes-reset на площадке после проверки; незавершённая команда сохраняет и блокировку установки (её снимают так же, как после прерванного configure, затем changes-reset). Статус, который эта версия не может прочитать, тоже барьер (unreadable): исходный файл один раз переносится в changes.json.unreadable (последующий его не заменяет), пишется только пульс с барьером, а взятый запрос остаётся на месте до changes-reset. Изменения не применяются поверх незавершённого журнала развёртывания: pollChanges, как и changes-reset, сначала вызывает checkJournal, а updates-poll не вызывает его вовсе, если такт обновлений завершился неподтверждённым прерыванием команды. Переключение, отклонённое до --move, не вызывает --clear: иначе снялись бы ограничения оператора. Прямо перед --move агент записывает и синхронизирует метку change-moving с id запроса в корне установки; следующий такт завершает переключение (--clear) только при метке этого запроса, иначе агент был остановлен до перемещения и запрос записывается interrupted. Ключ входит в отпечаток запроса: тот же id с другим ключом — другой запрос. Переключение узла — исключение: агент переезжает вместе с группой Pacemaker, и следующий такт на новом узле его завершает (--clear и проверка расположения), повторяя, пока запрос не истёк; исход переключения определяется тем, где Arkvory работает, даже если снять ограничение не удалось. Это полагается на force_unmount=true ресурса ocf:heartbeat:Filesystem — его значение по умолчанию, которое cluster-plan не меняет: при переезде он завершает процессы на томе, включая агента.
  • Ошибки кодами. Статус несёт код (expired, interrupted, key_missing, not_a_mirror, already_attached, mirror_failed, switchover_failed, single_copy_failed, unreadable, digest_mismatch); собственные сообщения агента с адресами и путями остаются в его журнале на площадке и управляющей площадке не видны. Статус пишется компактно (20 записей укладываются в несколько килобайт), API читает до 64 КиБ.

Уточнения реализации (этап F4) ​

  • Правила — чистые функции домена (fleet-health.ts): fleetAlerts и fleetLinks над тем, что управляющая площадка последний раз прочитала, со временем от вызывающего. Одна неудачная попытка — не оповещение: unreachable после 2 минут без ответа; stale — площадка отвечала, но не опрашивалась дольше трёх периодов (застрял фоновый опрос). Ответ «из будущего» считается полученным сейчас. Пока площадка не отвечает, её последний снимок для оповещений не используется (старые сведения не выдаются за текущие).
  • Виды: unreachable, certificate, revoked, refused, invalid, stale (недоверие и сбои), not-writable (процесс записи отказывает, не будучи в остановке), copies (не хватает копий; single-copy, если оператор принял одну копию на время), mirror-failing (кроме чтения восстановленного источника), mirror-lag (отставание ≥ ARKVORY_FLEET_LAG_ALERT, по умолчанию 1000, сравнение в bigint), updates-failed, version-skew (сведения). Порядок устойчивый: важность, имя, площадка, вид.
  • Связи: зеркало связано с площадкой, по адресу которой его источник отвечает; адреса сравниваются в одном написании (регистр, порт по умолчанию, конечные / и .). Источник, которым управляющая не управляет, — внешний узел external:<origin>. Своя установка — self; связи к ней появляются при ARKVORY_SITE_URL (адрес, по которому к ней обращаются площадки; проверяется при старте). Циклы и зеркало самого себя показываются как есть; на карте глубина ограничена шестью столбцами.
  • GET /api/v1/fleet/overview и метрики строятся процессом записи из реестра и своей топологии не чаще раза в 5 секунд, общее чтение на консоль и сбор метрик. Строка реестра, не читающаяся по контракту, — площадка invalid, а не сбой всего обзора. Метрики: arkvory_fleet_site_up, arkvory_fleet_site_last_seen_timestamp_seconds, arkvory_fleet_mirror_lag_changes, arkvory_fleet_alerts; правила ArkvoryFleetSiteDown, ArkvoryFleetMirrorBehind, ArkvoryFleetCritical. Имена площадок в метках экранируются реестром метрик.
  • Консоль: карточка «Состояние кластера» на странице «Площадки»: оповещения, карта SVG (все надписи — текстовые узлы, стили — классы и токены) и таблица с теми же связями для клавиатуры и экранного диктора.
  • Отставание зеркала считается на самой площадке-зеркале (F1), поэтому метрика рядом с площадкой, а не парой «источник — зеркало».
  • Уточнения после ревью (2026-10-15). Связи и метрика отставания строятся по последнему известному снимку и у неотвечающей площадки (связь помечена stale, на карте пунктир), а оповещения — только по свежим данным. stale срабатывает после трёх периодов опроса плюс длительности раунда (раунд с недоступными площадками длится дольше 90 секунд, и здоровые площадки не должны считаться «застрявшими»). Порог отставания экспортируется метрикой arkvory_fleet_mirror_lag_alert_changes, и правило ArkvoryFleetMirrorBehind сравнивает с ней, а не с числом 1000. Отставание длиннее 30 цифр — ответ invalid; метрика никогда не выдаёт NaN или Infinity. Строки реестра, не читающиеся по контракту, получают свой id (unknown:<n>), а не общий.
  • Устойчивость опроса (2026-10-15). Размер топологии проверяется в байтах UTF-8 с запасом до проверки столбца (jsonb добавляет пробелы); топология, которую столбец всё же отверг, записывается как invalid, а не обрывает раунд. Одна площадка читается одним опросом: фоновый раунд, «Прочитать все» и обновление карточки присоединяются к идущему чтению, и старый ответ не перезаписывает новый. Запросы к площадкам отменяются: остановка процесса прерывает раунд, проксируемые чтения и изменения заканчиваются, когда браузер ушёл. seen_at пишет часы процесса API, с которыми его сравнивает обзор; кеш обзора отмеряется монотонными часами. При сбое сбора метрики arkvory_fleet_* сохраняют последнее чтение, а сбой считается в arkvory_metrics_collection_failures_total{collector="fleet"}: исчезающие ряды сбрасывали бы for правил, и неотвечающая площадка не оповещалась бы при мигающем сборе.

Последствия ​

  • Публичный контракт расширяется аддитивно: /api/v1/system/topology, /api/v1/system/fleet/*, вид credential fleet-key, заголовок Arkvory-Fleet-Actor.
  • Новые таблицы реестра и сопряжений; ключи площадок вне резервной копии.
  • Управляющая площадка становится точкой высокой ценности: её глобальные администраторы и файл ключа должны защищаться так же, как root площадок.
  • Площадки без сопряжения работают как прежде; локальная настройка командами остаётся.

Arkvory · ProAnimaStudio