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. Сопряжение
- На площадке глобальный администратор создаёт код сопряжения (консоль площадки или
arkvory fleet-pair): одноразовый, 10 минут, показывается один раз. - На управляющей площадке администратор вводит адрес площадки. Сервер получает сертификат и
GET /health/status, показывает отпечаток и имя площадки для сверки с тем, что показала площадка вместе с кодом. - Администратор вводит код. Управляющая площадка по закреплённому TLS вызывает
POST /api/v1/system/fleet/enrollmentс кодом и своим именем и отпечатком. Площадка создаёт учётные данные управления (новый вид credentialfleet-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); всё остальное — 403permission_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к маршруту вне списка отклоняется до обработчика (403permission_missing), включая HEAD и другие шаблоны URL. Поэтому любой маршрут, добавляемый в список, — решение о правах управляющей площадки: обработчики, проверяющие флаг администратора, становятся ей доступны. Запись, удаление, пользователи и ключи остаются вне списка. Вторая проверка — на площадке: даже скомпрометированная управляющая площадка не получает байты черезfleet-key. - Прокси без интерпретации.
GET /api/v1/fleet/sites/{id}/read?path=возвращает статус и тело ответа площадки как есть (до 16 МиБ, тот же закреплённый TLS и таймаут); SDKclient.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>.key0600 пишется до запроса, агент берёт ключ только с именем своего запроса; API удаляет только ключ, который записал сам. Ключи и временные файлы, оставшиеся дольше часа, агент удаляет по имени. - Идемпотентность.
idвыбирает клиент, к запросу прилагается SHA-256 изменения (digest); агент помнит 20 последних. Тот жеidс тем же изменением — тот же запрос, с другим — 409idempotency_mismatch. Хешируемый текст строит одна чистая функцияsiteChangeDigestTextвcontracts(каноническое изменение и строка с SHA-256 ключа); API и агент хешируют его каждый своимnode:crypto. Агент пересчитывает отпечаток с пришедшим ключом и отклоняет несовпадение (digest_mismatch): запрос или ключ, подменённые во входящем каталоге, не применяются под чужимid. Нужный, но не пришедший ключ остаётсяkey_missing. Имя автора обрезается до 200 символов, запрос проверяется контрактом до публикации. - Сроки. Изменения HA живут 5 минут (решение о кластере, каким он был при вопросе), зеркал — 30 минут (переживают обновление площадки). Запрос старше срока или «из будущего» дальше срока —
failed: expired. Пока площадка ставит обновление (тот же такт),GETговоритupdating,POST— 503busy. - Прерванное изменение — барьер. Изменение, прерванное на полпути (процесс убит, команда не подтвердила завершение), записывается
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/*, вид credentialfleet-key, заголовокArkvory-Fleet-Actor. - Новые таблицы реестра и сопряжений; ключи площадок вне резервной копии.
- Управляющая площадка становится точкой высокой ценности: её глобальные администраторы и файл ключа должны защищаться так же, как root площадок.
- Площадки без сопряжения работают как прежде; локальная настройка командами остаётся.