Перейти к содержимому
GitRiverGitRiver
EN
Навигация

Руководство администратора

Руководство администратора: настройка установки, пользователи и группы, вход через каталог, квоты, журнал действий, обслуживание

Панель администрирования

Доступна по адресу /admin для пользователей с ролью администратора.


Управление пользователями

Создание пользователя

API: POST /api/v1/auth/register

В интерфейсе: пользователи регистрируются самостоятельно. Администратор может управлять существующими пользователями.

Закрытая регистрация

Переключатель «Регистрация разрешена» (Администрирование → Система, PUT /api/v1/admin/system) закрывает заведение новых учётных записей всеми способами входа сразу:

Способ Что происходит при закрытой регистрации
Форма регистрации, POST /api/v1/auth/register отказ 403, код registration_closed
Первый вход через OAuth2 вход у поставщика проходит, учётная запись не создаётся, отказ 403
Первый вход через SAML то же; в журнале сервера — check=registration
Первый вход через LDAP/AD то же; вход возвращает отказ, а не «неверный пароль»

Отказ отличается от отказа по правам машинным кодом registration_closed — клиенту не нужно разбирать переводимый текст.

Тем, у кого учётная запись уже есть, настройка не мешает ничем: она про заведение новых, а не про вход. Не затрагивает она и два администраторских пути — внешнее управление по SCIM и мастер первоначальной настройки: там учётные записи заводит не тот, кто входит.

Признак «Автоматическая регистрация» у поставщика OAuth или SAML остаётся вторым условием: учётная запись заводится, только когда разрешены оба. Выключив его у одного поставщика, можно закрыть авторегистрацию точечно, не трогая остальные способы входа.

Вход по каталогу при закрытой регистрации. Признака «Автоматическая регистрация» у LDAP нет, поэтому общий переключатель — единственный рычаг: закрыв регистрацию, вы закрываете и заведение записей из каталога. Если нужно, чтобы сотрудники входили по каталогу, а сами регистрироваться не могли, заведите учётные записи заранее — внешним управлением по SCIM или вручную — и включите в настройках LDAP «Связывать существующие локальные учётные записи», иначе каталог не свяжет их с собой.

Закрытие входа, деактивация и удаление

Три разные меры против учётной записи — у каждой своё назначение:

Мера Кто распоряжается Что происходит Обратима
Закрытие входа (Администрирование → Пользователи → замок, PATCH /api/v1/users/{username} с is_blocked) администратор установки закрываются все входы сразу: пароль, внешние поставщики, персональные токены, ключи SSH; выданные токены гасятся; расписания сборок этого автора встают с причиной creator_blocked да, тем же переключателем
Деактивация (active=false через SCIM) кадровая система то же закрытие входов, но решение принадлежит внешней системе да, из кадровой системы
Удаление (DELETE /api/v1/users/{username}) администратор установки учётная запись, её токены и ключи исчезают; запрещено, пока за пользователем числятся репозитории нет

Закрытие входа — временная мера: отпуск, подозрение на кражу пароля, проверка службы безопасности, уход подрядчика с сохранением истории. Данные, авторство и участие в группах остаются на месте, содержимое не скрывается.

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

Закрытие и возврат входа попадают в журнал аудита событиями block_user и unblock_user с именем того, кому мера применена.

Уже запущенная сборка заблокированного автора доводится до конца: токен задания принадлежит заданию, а не человеку. Новые запуски закрыты вместе с входом.

Управление

Действие API
Список пользователей GET /api/v1/users
Информация GET /api/v1/users/{username}
Редактирование PATCH /api/v1/users/{username}
Закрыть или вернуть вход PATCH /api/v1/users/{username} с is_blocked
Сброс пароля PUT /api/v1/users/{username}/password
Удаление DELETE /api/v1/users/{username}
Переименование POST /api/v1/users/{username}/rename

Переименование пользователя

POST /api/v1/users/{username}/rename с телом {"username": "новое-имя"}. Права: сам пользователь или администратор.

Переименование — отдельный вызов, а не поле в PATCH /api/v1/users/{username}, и оно попадает в журнал аудита.

Что происходит:

  • вместе с именем пользователя меняется имя его пространства имён: адреса репозиториев становятся /{новое-имя}/{repo}, каталоги репозиториев и вики на диске переезжают под новое имя. Реестр пакетов, LFS, артефакты и уже выложенные сайты Pages продолжают работать без перенастройки;
  • старое имя освобождается, но пока его никто не занял, ведёт на нового владельца. По прежнему имени продолжают работать git clone/fetch/push, Git LFS, реестры пакетов, опубликованные сайты Pages и весь REST API — менять git remote и внешние ссылки не обязательно, хотя и желательно.

Ограничения и последствия:

  • новое имя проверяется теми же правилами, что при регистрации, включая список имён, занятых маршрутами приложения (admin, settings, explore, api и другие);
  • как только освободившееся имя займёт другой пользователь или группа, перенаправление исчезает, и ссылки на старое имя ведут уже к нему;
  • вернуть себе прежнее имя можно, пока его не занял кто-то другой;
  • если переименование не удалось, остаётся прежнее имя — целиком, вместе с адресами и каталогами на диске.

Имена пользователей и групп живут в одном пространстве имён, поэтому имя, занятое группой, недоступно пользователю и наоборот.

Видимость профилей

По умолчанию сведения о пользователях доступны только тем, кто вошёл в систему: анонимный посетитель не узнает, кто работает на установке и над чем.

Настройка закрывает ВСЕ пути к этим сведениям сразу: карточку по имени, карточку по идентификатору и перечисление пользователей с поиском по ним. Закрывать что-то одно смысла нет — перечисление отвечает на тот же вопрос «кто здесь работает» и вдобавок показывает, кто из них администратор.

Публичной установке (открытый код, витрина проектов) видимость включают настройкой:

public_profiles = true

или переменной окружения: GITRIVER_PUBLIC_PROFILES=true.

Настройка действует одинаково на основной интерфейс /api/v1 и на слой совместимости /api/v3: обойти её через слой совместимости нельзя. Адрес почты она не раскрывает: его по-прежнему видят только сам владелец и администратор.

«Все пути» — это и боковые адреса, привязанные к имени: список репозиториев пользователя, его звёзды, лента событий, карта активности, GPG-ключи и справка /api/v1/namespaces/{имя} о том, занято ли имя. Каждый из них отвечает на тот же вопрос «кто здесь работает», и любой из них, оставленный открытым, сводит настройку на нет: по паре «есть/нет» перебираются имена учётных записей.

Текущее значение публично доступно в /api/v1/server-info. При закрытых профилях пункт «Пользователи» гостю не показывается.

Значки пользователей и групп выводятся из имени и отдаются по /avatars/{имя}.svg без аутентификации. Значок строится для любого имени, поэтому по нему нельзя выяснить, существует ли такое имя.

Чего настройка НЕ закрывает — подпись под записью. Задача, комментарий, запрос на слияние, обзор и релиз несут карточку своего автора (author: учётное имя, отображаемое имя, идентификатор) прямо в ответе, и на публичном репозитории её видит любой, кто видит саму запись. Перечислить по ней состав установки нельзя: имя приходит только вместе с записью, которую зритель и так читает. Слои совместимости отвечают так же. Адрес почты и признак оплаченного места в карточку не входят.

Недоступное неотличимо от несуществующего

Репозиторий, группа и любой их вложенный объект, недоступные обратившемуся, отвечают 404 — тем же кодом и тем же телом, что несуществующий адрес. Отдельный 403 подтверждал бы, что объект есть, и перебором имён (а в слоях совместимости — и номеров, /api/v4/projects/1..N) снимался бы список закрытых проектов и пространств имён установки.

Правило одинаково для /api/v1, /api/v3 и /api/v4.

403 остаётся там, где обратившийся объект ВИДИТ, а прав не хватило на само действие: запись в архивный репозиторий, токен без нужной области доступа, предел редакции или квоты: человек должен понимать, что делать дальше.

Настоящая причина отказа пишется в журнал сервера на уровне debug: по нему закрытый объект отличается от опечатки в адресе.

В git по HTTP, в LFS и в реестре контейнеров действует то же правило, но скрытие устроено зеркально: клиенту, который ещё не назвался, оба случая отвечают 401 с WWW-Authenticate, а не 404. На 404 ни git, ни docker учётных данных не предъявляют — закрытый репозиторий стало бы невозможно клонировать и забрать образ. Клиент, уже назвавший себя, получает 404 в обоих случаях. По SSH отказ в чтении даёт то же сообщение, что и отсутствие репозитория.

Публичный репозиторий не скрывается: его читает кто угодно, и отказ в записи в него называет причину прямо — например, CI-токену чужого задания.


Группы (организации)

Группы объединяют пользователей и репозитории.

Действие API
Создать группу POST /api/v1/groups
Список групп GET /api/v1/groups
Переименовать группу POST /api/v1/groups/{path}/rename
Добавить участника POST /api/v1/groups/{path}/members
Сменить роль участника PATCH /api/v1/groups/{path}/members/{user_id}
Удалить участника DELETE /api/v1/groups/{path}/members/{user_id}
Свои роли POST /api/v1/groups/{path}/custom_roles

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

Группа не остаётся без владельца. Ни удаление последнего владельца, ни его понижение не проходят. «Последний» считается по тем, кто МОЖЕТ управлять группой: владелец с закрытым входом (заблокирован администратором или деактивирован кадровой системой) в счёт не идёт — иначе группа осталась бы формально с владельцем и фактически без управления.

Путь группы меняется через POST /api/v1/groups/{path}/rename с телом {"path": "новый-путь"} — права администратора группы. Семантика та же, что у переименования пользователя (см. выше), включая перенаправление старого пути. Отображаемое название группы (name) — отдельное поле; его, в частности, меняет SCIM, и на путь это не влияет.

Свои роли

Позволяют создавать роли с точечными правами для участников группы.

Полный перечень прав в формате домен:действие отдаёт GET /api/v1/permissions — тот же список показывает интерфейс при настройке роли. Перечень меняется с выпусками, поэтому актуальный список даёт только этот запрос.

Свои роли и этот перечень — редакция Pro, и меряются они МЕСТАМИ: настраивает роли группы тот, у кого есть место (см. лицензирование). В Community права участника определяет его уровень доступа к репозиторию: чтение, запись, администрирование.


Квоты

Ограничение ресурсов пользователей и групп.

Действие API
Глобальные квоты GET/PUT /api/v1/admin/quotas
Список пользователей с расходом GET /api/v1/admin/quotas/users
Квоты пользователя GET/PUT /api/v1/admin/quotas/users/{id}
Список групп с расходом GET /api/v1/admin/quotas/groups
Квоты группы GET/PUT /api/v1/admin/quotas/groups/{id}

Нулевое значение любой квоты означает «без ограничения», а не «ничего нельзя».

Персональная квота заменяет умолчания целиком. Задав пользователю или группе свои пределы, вы задаёте их ВСЕ: незаполненный предел в персональной строке читается как ноль, то есть «без ограничения», а не как «взять системный». Поэтому, ограничивая одну категорию, заполняйте и остальные — иначе они окажутся сняты. По той же причине предел, добавленный в новой версии (вложения релизов, кеш CI), у владельцев с уже заданной квотой начинает действовать не сразу: у них он пуст, и обновление версии ничего не ограничивает, пока квоту не сохранят заново.

Сводные списки квот отдаются страницами

GET /api/v1/admin/quotas/users и GET /api/v1/admin/quotas/groups отдают страницу, а не весь список:

GET /api/v1/admin/quotas/users?per_page=20&after=<курсор>
GET /api/v1/admin/quotas/groups?per_page=20&after=<курсор>

Ответ — как у остальных курсорных списков: {"items": [...], "next_cursor": "..."}. Порядок — по имени владельца; next_cursor пуст, когда список дочитан. В интерфейсе администратора страницы догружаются кнопкой в конце списка.

При обновлении с 1.0.x. В версиях по 1.0.x включительно оба вызова отдавали массив верхнего уровня — это ломающее изменение. Клиент, читавший ответ как массив, должен перейти на поле items и дочитывать страницы по next_cursor.

Чья квота расходуется

Репозиторий принадлежит либо личному пространству пользователя, либо группе — и расходует квоту того, кому принадлежит:

  • репозиторий вне группы — квота его владельца;
  • репозиторий группы — квота группы, а не личная квота создателя.

Правило одинаково для всех категорий: LFS, Pages, CI-артефакты, CI-кеш, вложения релизов и пакетное хранилище. Расход виден там же, где применяется предел: репозитории группы входят в «использовано» группы и не входят в личное «использовано» её участников. При переносе репозитория между владельцем и группой расход переезжает вместе с ним.

Подгруппы считаются отдельно. В отличие от членства и CI-переменных, квота вниз по подгруппам не наследуется: репозиторий подгруппы расходует квоту своей подгруппы, а не группы-предка, и в «использовано» предка не попадает. Чтобы ограничить ветку целиком, задайте предел каждой подгруппе.

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

Когда проверяется каждый предел

Пределы не пересчитываются по расписанию — каждый проверяется в тот момент, когда ресурс запрашивают. Ноль в любом из них по-прежнему означает «без ограничения».

Предел Когда проверяется Что происходит при исчерпании
Число репозиториев (max_repos_count) создание репозитория, форк, импорт из внешнего сервиса, перенос в чужое пространство 403, репозиторий не создаётся
Размер репозитория (max_repo_size) перед приёмом git push (по всем транспортам: HTTP, SSH, gitriver serv), перед форком, перед переносом в чужое пространство, перед синхронизацией зеркала; после импорта и форка размер пересчитывается по факту отправка отклонена целиком, git печатает причину; форк и импорт — 403, созданный репозиторий откатывается
LFS (max_lfs_size) загрузка LFS-объекта — до приёма тела, по объявленному размеру загрузка отклонена (403), место не занимается
Pages (max_pages_size) выкладка сайта — ручная и автоматическая из CI; распаковка обрывается по остатку ручная выкладка отклонена (403); автоматическая не создаёт развёртывания, причина уходит в журнал сервера (конвейер остаётся успешным)
CI-артефакты (max_ci_artifacts) запуск конвейера И сохранение архива артефактов: у внешнего исполнителя приём обрывается по остатку, у встроенного исполнителя проверяется до упаковки и по её итогу. Повторный прогон задания заменяет его прежний архив, и замена считается приростом конвейер не запускается; выгрузка отклонена (403), архив на диске не остаётся. У встроенного исполнителя задание ПРОВАЛИВАЕТСЯ (артефакты объявлены, зависимые задания их ждут), в журнале задания строка «Сбор артефактов: архив не сохранён на сервере. Квота превышена: …»
CI-кеш (max_ci_cache) сохранение кеша задания — у внешнего исполнителя приём обрывается по остатку, у встроенного исполнителя проверяется до упаковки и по её итогу. Замена архива по существующему ключу считается приростом, и не растущая замена проходит даже при исчерпанной квоте кеш не сохраняется, в журнале задания строка «Сохранение кеша: архив не сохранён на сервере. Квота превышена: …»; задание остаётся успешным, прежний архив по этому ключу цел
Вложения релизов (max_release_attachments_size) загрузка вложения — до записи файла в хранилище загрузка отклонена (403), место не занимается
Пакеты (max_packages_size) запись в пакетное хранилище см. «Квота пакетного хранилища»

Размер репозитория — предел на ОДИН репозиторий, а не на их сумму. Он сравнивается с размером того репозитория, в который идёт отправка (он пересчитывается после каждой отправки), поэтому у владельца с пределом 1 ГБ может быть десять репозиториев по 900 МБ. Суммарный объём git-данных владельца отдельного предела не имеет: он показан в квотах как справочная величина.

Проверка стоит перед приёмом данных отправки и опирается на размер, известный на её начало. Отсюда следствие, о котором стоит знать: отправка, которая переводит репозиторий за предел, принимается целиком, а отклоняется уже следующая.

Отправка, которая только удаляет ветки и теги, не проверяется никогда: так владелец за пределом всегда может освободить место.

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

Недоступность базы данных не снимает пределы. Если база данных недоступна, операция, проверяемая квотой, отклоняется, а не разрешается.

Место занимается на время приёма

Загрузка занимает место сразу, как началась, а не когда закончилась: поэтому одновременные загрузки не могут вместе превысить предел.

Что из этого следует администратору:

  • параллельные загрузки одного владельца делят остаток между собой. Если свободно 500 МБ, а идут две загрузки по 400 МБ, вторая получит 403 — даже если первая в итоге окажется меньше объявленного;
  • загрузка без объявленной длины занимает весь свой предел на файл. Клиент, не приславший Content-Length (Transfer-Encoding: chunked), считается самым большим из возможных, пока не закончит;
  • оборванное соединение освобождает место сразу, а если сервер перезапустили посреди приёма — не позже, чем истечёт срок запроса загрузки (upload_http_timeout_secs, по умолчанию час). Ручной уборки не требуется;
  • отчёты о расходе показывают завершённое, без идущих загрузок: «Квоты → Пользователи/Группы» и GET /api/v1/admin/quotas/... называют занятый диск, а не обещания приёма. Единственное исключение — незавершённые загрузки слоёв образа: принятые ими байты уже лежат на диске и в расход входят (см. «Квота пакетного хранилища»).

Приём обрывается по МЕНЬШЕМУ из двух пределов — остатка квоты и предела на один файл. Что именно сработало, видно по коду ответа: 403 — место кончилось (правится квотой), 413 — файл больше предела (правится загружающим).

Цена заданного предела. Пока предел не задан (ноль — «без ограничения»), расход не считается вовсе. Как только предел задан, каждая загрузка заново считает место, занятое владельцем в пакетном хранилище, а одновременные загрузки одного владельца проходят эту проверку по очереди. Для docker push обычного образа это доли секунды, но у владельца с сотнями тысяч слоёв приём замедляется заметно. Если предел вам не нужен — не задавайте его: нулевое значение не только разрешает всё, но и ничего не считает.

Квота вложений релизов

Файлы, выложенные к релизу, — своя категория расхода (max_release_attachments_size), а не часть «пакетов». Считаются они по-разному, и смешивать их нельзя: пакетное хранилище дедуплицирует содержимое (одинаковый слой или файл занимает место однажды), а вложение лежит своим файлом, и одна и та же сборка в двух релизах занимает диск дважды — ровно так её и считает квота.

Где виден расход:

  • владелец — «Настройки → Использование хранилища», строка «Вложения релизов»;
  • администратор — «Квоты → Пользователи/Группы», та же строка рядом с пределом, и GET /api/v1/admin/quotas/users/{id} (поле usage.release_attachments_size);
  • сам предел — limits.max_release_attachments_size там же.

Когда проверяется. При загрузке вложения, ДО того как файл попадает в хранилище. Если клиент объявил длину части (Content-Length), отказ приходит до приёма тела; если не объявил — приём обрывается по остатку квоты, а принятое остаётся во временном каталоге и снимается вместе с отказом. Занятое место после отказа не растёт ни в том, ни в другом случае.

Отказ — 403 с занятым объёмом, пределом и остатком. Не путать с 413 «файл больше предела»: предел на ОДИН файл — 500 МиБ, он настройками не меняется (см. пределы загрузки файлов) и правится загружающим, а 403 правится квотой.

Освобождение места — удаление вложения или релиза целиком: расход считается по текущему состоянию, отдельного пересчёта нет.

При обновлении. Расход вложений становится виден сразу после обновления, а предел остаётся нулевым — то есть «без ограничения», — пока его не задать. Начните с того, чтобы посмотреть на реальные числа: у установок с большими сборками в релизах эта строка бывает крупнейшей.

Квота пакетного хранилища

Слои образов, файлы пакетов (npm, PyPI, Cargo, Maven, NuGet, Generic, Composer) и кеш посредника для зависимостей лежат в одном хранилище и считаются вместе. Одинаковое содержимое хранится однажды и в объём входит один раз — сколько бы ссылок на него ни было.

Ограничений два, действуют они одновременно:

  • квота владельца — категория «пакеты», общая на все его репозитории (для репозитория группы это квота группы, см. «Чья квота расходуется»);
  • квота репозитория registry_quota_bytes — GET /api/v1/repos/{owner}/{name}/packages/usage.

При исчерпании любой из них отклоняются все пути записи: публикация пакета, приём слоя образа (целиком, по частям и переносом из другого репозитория) и наполнение кеша посредника. Отказ — 403 с указанием занятого объёма и предела.

Незавершённая загрузка слоя занимает место сразу. docker push шлёт большой слой сотней запросов подряд, и принятое каждым из них уже лежит на диске: поэтому в расход входит и то, что накопила ещё не завершённая загрузка. Отказ по квоте приходит на том запросе, на котором предел исчерпан, а не на завершении — и принятое этой загрузкой снимается вместе с отказом. Брошенные на середине загрузки подбирает автоочистка реестра.

Квота ограничивает суммарный объём, а размер ОДНОГО запроса ограничен отдельно и настройками не меняется — см. пределы загрузки файлов. Отказы у них разные и по смыслу, и по коду: 413 — «файл больше предела», исправляется файлом; 403 — «место кончилось», исправляется квотой.

Неизменность опубликованной версии

Опубликованный файл версии пакета нельзя заменить другим содержимым. Повторная публикация той же версии отвечает 409 и оставляет прежнее содержимое на месте; менять настройками это нельзя — правило действует всегда и во всех реестрах (npm, PyPI, Cargo, Maven, NuGet, Generic, Composer).

Поэтому сборка, прошедшая проверку, и завтра получит тот же код под тем же номером версии.

Неизменяемо и то, что версия о себе объявляет: описание, список зависимостей, требования к среде — по ним клиент выбирает, что установить. Повтор, который проходит как побайтный, метаданные версии не переписывает.

Что при этом остаётся разрешённым:

  • побайтный повтор — та же версия с тем же содержимым принимается и отвечает 200 вместо 201, поэтому запрос безопасно повторять после разрыва связи;
  • второй файл той же версии — правило смотрит на файл, а не на весь номер версии: колесо рядом с исходниками в PyPI и sources.jar рядом с jar в Maven публикуются как обычно;
  • черновые версии Maven (-SNAPSHOT) — повторный mvn deploy той же версии принимается;
  • отзыв (yank) и удаление версии — они меняют не содержимое, а доступность. Отзыв выводит версию из числа устанавливаемых: карточка пакета и поиск (npm, cargo, NuGet) с этого мгновения называют «последней» наибольшую из оставшихся — ровно ту, которую поставит клиент. Сам файл остаётся доступным по точному номеру версии, а снятие отзыва возвращает её и в карточку.

Убрать уже опубликованную версию, чтобы освободить номер, можно только удалением — вкладка Пакеты репозитория или DELETE /api/v1/repos/{owner}/{name}/pkg/{тип}/{пакет}/{версия}. Это осознанное действие с правом записи, а не побочный итог повторной публикации.


Помощник на основе языковой модели

Разбор упавших заданий CI и замечания к изменениям запроса на слияние языковой моделью — своя модель внутри контура или облачный поставщик, по выбору администратора. Настройка: Администрирование → Помощник ИИ. Редакция Pro целиком: и настройки, и обращения. Настройки местами не меряются, а обращения к модели сверх редакции требуют места у того, кто зовёт.

Ненастроенный или выключенный помощник не делает ни одного исходящего запроса — это важно для закрытого контура. Расход модели считается на владельца репозитория и ограничивается месячным пределом (0 — без ограничения), как и квоты хранилища.

Подробно — ai-assistant.md.


Реестр контейнеров

Встроенный Docker Registry V2 — каждый репозиторий получает свой реестр.

Использование

# Вход
docker login git.example.com

# Отправка
docker tag myapp:latest git.example.com/owner/repo/myapp:latest
docker push git.example.com/owner/repo/myapp:latest

# Получение
docker pull git.example.com/owner/repo/myapp:latest

Управление

  • Правила защиты тегов — запрет перезаписи тега по маске
  • Правила хранения — автоматическое удаление старых тегов
  • Уборка — ручной запуск сборки мусора
  • Сканирование на уязвимости — через Trivy

Автоочистка реестра

Проход очистки один и тот же у расписания и у кнопки: для каждого репозитория применяются сначала глобальные ограничения установки, затем правила хранения самого репозитория, затем уборка. Глобальные ограничения задаются в настройках («Хранить последних N тегов» и срок), правила репозитория — его владельцем, см. правила хранения тегов.

Решение «удалять ли тег» у обоих одно и то же: тег удаляется, только если он И за пределами keep_last, И старше max_age_days. Ноль в поле означает «ограничения нет»; два ноля — такое правило не удаляет ничего.

Обнулённые глобальные ограничения не отключают очистку целиком: правила хранения самих репозиториев продолжают действовать, как и уборка. Снятый выключатель «Включить автоматическую очистку» тоже отменяет не всё — он убирает только запуск ПО РАСПИСАНИЮ, а кнопка «Запустить сейчас» работает и при нём, тем же проходом. Чтобы теги не удалялись, снимаются правила хранения у самих репозиториев: их не отменяет ни один переключатель установки.

С версии 1.1.0 правило РЕПОЗИТОРИЯ, где заданы оба числа — «хранить последних N» и предельный возраст, — удаляет тег, только если он одновременно и вне последних N, и старше предельного возраста. Если у вас заданы оба числа, после обновления будет удаляться меньше тегов: часть образов, которые раньше подпадали под удаление по возрасту, теперь остаётся — потому что входит в число последних N. Потери данных это не несёт — правило только сохраняет то, что прежде удалялось; но если оно было настроено в расчёте на прежнее поведение, освобождение места замедлится. Вернуть прежнюю чистку по возрасту можно, обнулив keep_last в правиле.

Уборка и одновременная публикация

Уборку (POST /api/v1/repos/{owner}/{name}/packages/gc, кнопка «GC» и прогон по расписанию) можно запускать в любой момент: docker push, идущий прямо сейчас, она не задевает.

docker push — это два запроса: сначала загружаются слои, потом отправляется манифест. Между ними слой уже принадлежит репозиторию, но ещё ни одним манифестом не назван. Уборка такие слои не трогает — привязке, как и манифесту незавершённой публикации, даётся сутки; по их истечении слой, которого не назвал своим ни один манифест, отвязывается и удаляется. Манифест, назвавший слой, которого в репозитории нет, реестр не примет: ответ 404 MANIFEST_BLOB_UNKNOWN, после которого клиент повторяет загрузку слоя. Образа, который лежит в реестре, но не скачивается, уборка не оставляет.

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

Сканирование образов на уязвимости

Сканер запускается двумя способами, и настройка у них разная:

  • автоматически при публикации манифеста — только при registry_scan_enabled = true;
  • по требованию — кнопка «Сканировать» в списке образов либо POST /api/v1/repos/{owner}/{name}/packages/scans с телом {"image": "myapp", "reference": "1.2.3"}. Требуется право записи в реестр. Флаг registry_scan_enabled на ручной запуск не влияет: администратор просит проверку явно.

Ссылка (reference) — тег либо sha256:…; тег разрешается в дайджест до запуска сканера, поэтому отчёт всегда относится к тому образу, который был запрошен, даже если тег успел переехать.

Trivy забирает образ из реестра самой установки с правами запросившего, поэтому сканируются и приватные образы, и полученные через посредника. Путь к исполняемому файлу — trivy_path (по умолчанию ищется trivy в PATH).

Сканирование образа, полученного через посредника, загружает все его слои в кеш репозитория — они расходуют квоту реестра так же, как опубликованные.

Результат хранится на дайджест — по одной записи на образ, независимо от того, сколько тегов на него указывает и сколько раз его просили проверить:

  • пока сканирование образа идёт, повторный запуск на тот же дайджест возвращает уже идущее, а не начинает вторую проверку;
  • процессу Trivy отводится 30 минут; не уложившийся прекращается, а сканирование помечается ошибкой с указанием истёкшего срока;
  • сканирование, не подававшее признаков жизни дольше 35 минут (срок сканирования плюс запас), считается оборванным: так выглядит запуск, чей сервер остановили посреди работы. Оно показывается ошибкой, а не вечным «сканируется», и дайджест снова можно проверить. Повторить проверку завершённого образа можно когда угодно — база уязвимостей пополняется, и вчерашний отчёт поводом для отказа не считается.

Результаты читаются двумя разными выдачами, и различие между ними существенно для тех, кто ходит в API сам:

  • GET /api/v1/repos/{owner}/{name}/packages/scans — сводки: состояние, дайджест, счётчики по уровням критичности. Полного отчёта Trivy в них нет. Без параметров отдаются сводки всех образов репозитория; ?digests=sha256:…,sha256:… ограничивает выдачу указанными образами (не больше 100 за раз);
  • GET /api/v1/repos/{owner}/{name}/packages/scans/{digest} — та же сводка плюс полный отчёт Trivy (full_report_json) по одному образу.

Подписи образов (Cosign)

Подпись cosign sign попадает в реестр обычной отправкой — тегом sha256-<hex>.sig рядом с подписанным образом. Вкладка «Пакеты» помечает такие образы значком «Подписан»; значок означает ровно наличие тега подписи, а не её проверку — за проверкой идти к cosign verify.

Выдач тоже две:

  • GET /api/v1/repos/{owner}/{name}/packages/signatures?digests=sha256:…,sha256:… — подписанные образы перечисленных дайджестов (не больше 100 за раз). Параметр обязателен. В ответе только подписанные, парами image_name + digest: подпись лежит рядом с образом, и один и тот же манифест под двумя образами репозитория может быть подписан лишь под одним из них;
  • GET /api/v1/repos/{owner}/{name}/packages/signature?image=…&digest=… — то же про один образ.

Обе выдачи, как и остальная вкладка, доступны всем, кто может читать репозиторий: в публичном репозитории — в том числе анониму. Тем же правилом живёт GET …/packages/sbom?image=…&digest=… — наличие SBOM (тег sha256-<hex>.sbom), который публикуют Trivy и Syft.

Зеркало внешнего реестра образов

Репозиторий может работать кеширующим зеркалом Docker Hub, quay.io, registry.k8s.io или частного реестра: Настройки репозитория / Пакеты / Выдача пакетов через посредника, тип реестра docker, адрес — корень реестра без /v2 (https://registry-1.docker.io).

docker pull git.example.com/owner/mirror/library/nginx:1.25

Что это даёт администратору:

  • сборки перестают зависеть от доступности и ограничений внешнего реестра: после прогрева образ отдаётся из кеша, а при аварии источника манифест по тегу отдаётся устаревшим вместо отказа;
  • слои лежат в общем хранилище наравне с локальными образами: дедупликация бесплатна, уборка их учитывает, квота считает вместе с остальным;
  • маски allow/deny ограничивают, какие образы вообще уходят наружу;
  • запрос без входа наружу не уходит никогда: посторонний не может пользоваться установкой как бесплатным зеркалом внешнего реестра;
  • образы, полученные через посредника, не попадают в автоматическое сканирование Trivy. Автоматически сканируются образы, опубликованные в репозиторий; образ от посредника проверяется по требованию — кнопкой «Сканировать» с указанием его ссылки.

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


LDAP

Корпоративная авторизация через LDAP/Active Directory.

Настройка в интерфейсе: Администрирование / LDAP

Действие API
Получить настройки GET /api/v1/admin/ldap
Сохранить PUT /api/v1/admin/ldap
Тест подключения POST /api/v1/admin/ldap/test
Удалить DELETE /api/v1/admin/ldap

Настройка «Связывать существующие локальные учётные записи» по умолчанию выключена: вход через каталог не занимает локальную учётную запись с тем же именем — см. «Связывание внешнего входа с учётной записью».

Заведение записей из каталога подчиняется общему переключателю регистрации — см. «Закрытая регистрация».

Имя входа в каталоге приводится к правилам GitRiver

Имя входа в каталоге правил GitRiver не соблюдает: там обычны точки (ivan.petrov), пробелы и кириллица, а admin завести не сложнее прочих. При первом входе имя входа приводится к правилам имени GitRiver — так же, как userName в SCIM (см. «Имя пользователя приводится к правилам GitRiver»):

Имя входа в каталоге Имя в GitRiver
a_orlova a_orlova
ivan.petrov ivan_petrov
admin (занято маршрутом) ldap_admin

Если приведённое имя занято, добавляется числовой суффикс.

Связь с каталогом держат DN и имя входа, а не имя учётной записи. DN переживает смену имени входа, имя входа — перенос записи между подразделениями; ни то, ни другое не заводит второй учётной записи. Записи, заведённые через каталог до версии 1.1.0, связываются так же при первом входе — делать ничего не нужно.

Разные имена входа, которые приводятся к одному имени («ivan.petrov» и «ivan petrov» дают ivan_petrov), получают разные учётные записи: второй подбирается свободное имя (ivan_petrov1), а чужая запись не затрагивается.

Шифрование канала

Схема в URL Что происходит
ldaps://host:636 TLS с первого байта, сертификат проверяется всегда. Выключателя проверки нет
ldap://host:389 Соединение поднимается до TLS расширенной операцией StartTLS — по умолчанию включено

Сервер каталога без поддержки StartTLS получает отказ, и соединение не устанавливается: «зашифровать не удалось» не означает «продолжим открытым текстом». Проверка сертификата и здесь обязательна.

Доверие берётся из системного хранилища корневых сертификатов той машины, где работает GitRiver (в контейнере — из его образа). Каталог с сертификатом внутреннего удостоверяющего центра требует, чтобы корень этого центра был добавлен в хранилище этой машины — иначе проверка подключения ответит отказом по сертификату. Это одинаково верно для ldaps:// и для StartTLS.

Хранение пароля служебной учётной записи

Пароль служебной учётной записи хранится в базе зашифрованным ключом, производным от jwt_secret, — как TOTP-секреты, client_secret поставщиков OAuth и ключ подписи id_token. Наружу он не отдаётся: настройки показывают только сам факт, что пароль задан.

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

То же относится к паролю SMTP.

Выключатель «Поднимать соединение до TLS (StartTLS)» снимает шифрование — тогда пароль служебной учётной записи и пароли пользователей идут по сети открытым текстом. Оставлять так осмысленно только в закрытом контуре; о таком выборе сервер предупреждает в журнале при каждом старте и при сохранении настроек.

Настройки каталога хранятся в базе и переживают перезапуск; секция [ldap] в config.toml служит запасным источником, если в базе настроек нет.


SMTP (уведомления по почте)

Настройка в интерфейсе: Администрирование / SMTP

Действие API
Получить настройки GET /api/v1/admin/smtp
Сохранить PUT /api/v1/admin/smtp
Тест отправки POST /api/v1/admin/smtp/test
Удалить DELETE /api/v1/admin/smtp

Канал уведомлений «Max»

Настройка в интерфейсе: Репозиторий / Настройки / Каналы уведомлений, тип канала Max.

Поля канала:

Поле Значение
Токен бота Токен, выданный при создании бота в мессенджере Max. Уходит значением HTTP-заголовка, поэтому допустимы только видимые символы латиницы, цифры и знаки — токен с кириллицей или пробелом отвергается при сохранении
Получатель Чат/канал (chat_id) либо пользователь (user_id)
ID Целое число. Bot API адресует сообщение только числовым идентификатором

Особенности работы канала:

  • Запросы уходят на platform-api2.max.ru — прежний домен platform-api.max.ru выведен из эксплуатации 19.07.2026.
  • Сертификат Bot API выпущен центром Минцифры. Корень «Russian Trusted Root CA» встроен в поставку и применяется только к запросам этого канала: остальной исходящий трафик GitRiver проверяется штатными корнями.
  • Текст длиннее 4000 символов обрезается — это предел Bot API.
  • Темп отправки удерживается на уровне двух сообщений в секунду на получателя (ограничение Bot API), отказы 429 и сбои сервиса повторяются до трёх раз с нарастающей паузой.

Переменные окружения (нужны только в нетиповых установках):

Переменная Назначение
GITRIVER_MAX_API_BASE Другой адрес Bot API — испытательный стенд или посредник внутри контура. По умолчанию https://platform-api2.max.ru
GITRIVER_MAX_CA_BUNDLE Путь к PEM-связке доверенных корней вместо встроенного сертификата — если Минцифры выпустит новый корень раньше очередной сборки

OAuth2 / SAML SSO

Связывание внешнего входа с учётной записью

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

Путь первый — привязка владельцем. Пользователь входит в GitRiver, открывает Настройки / Внешние входы и нажимает «Привязать». Сеанс и есть доказательство владения. Работает и для OAuth, и для SAML, никакой настройки не требует. Если привязка не прошла (например, эта внешняя учётная запись уже привязана к другой учётной записи), пользователь возвращается на ту же страницу с причиной, а подробности отказа попадают в журнал сервера.

Путь второй — доверенные домены. У каждого поставщика (и OAuth, и SAML) есть настройка «Связывание с существующей учётной записью»:

Значение Поведение
«Запрещено» (по умолчанию, в том числе у поставщиков, заведённых до обновления) совпадение адреса не даёт входа; пользователю показывается, что учётную запись нужно привязать самому
«Только доверенные домены» вход занимает существующую учётную запись, если домен адреса перечислен в настройке поставщика

Список доменов при второй политике обязателен: пустой список не сохраняется. Сравнение доменов точное, без поддоменов: corp.example не покрывает evil.corp.example.

Учётные записи администраторов не занимаются никогда — ни при какой политике. Администратор привязывает свой внешний вход сам, из настроек профиля.

После обновления пользователи, которые входили через SSO и уже имеют привязку, продолжают входить как раньше: правило касается только первого связывания. Если до обновления вход опирался на совпадение адреса, включите доверенные домены или попросите людей привязать вход из настроек профиля.

Привязка и отвязка внешнего входа попадают в журнал аудита отдельными событиями («Администрирование» → «Аудит») — и когда привязку делает владелец из настроек, и когда она случается при первом входе по доверенному домену.

То же правило у LDAP, где ключом служит не адрес, а имя: вход через каталог не занимает локальную учётную запись с тем же именем, пока это не разрешено настройкой «Связывать существующие локальные учётные записи». Записи, уже заведённые через LDAP, это не затрагивает — они входят как прежде.

Поставщики OAuth2

Подключение внешних поставщиков OAuth2 и OpenID Connect для входа.

Пользователи связывают внешние учётные записи: Настройки / Внешние входы.

Имя у поставщика приводится к правилам GitRiver так же, как у SCIM и каталога; непригодное как есть получает приставку oauth_, занятое — числовой суффикс.

SAML 2.0

  • GET /api/v1/auth/saml/providers — список поставщиков
  • GET /api/v1/auth/saml/{id}/metadata — метаданные SP (XML)
  • Настройка поставщика входа: укажите в нём ACS URL из метаданных

Требования к подписи утверждения

Что Принимается
Алгоритм подписи RSA или ECDSA с SHA-256, SHA-384, SHA-512 (в том числе RSA-PSS). RSA-SHA1 отклоняется — переключите поставщика на SHA-256
Хеш ссылки (DigestMethod) SHA-256, SHA-384, SHA-512
Канонизация исключающая (exc-c14n), включающая (REC-xml-c14n-20010315), c14n11 — в любом варианте, с комментариями и без
Преобразования только канонизация и enveloped-signature; XSLT и XPath отклоняются
Ссылка Reference URI пустая (весь документ) или #идентификатор; ссылка на файл или внешний адрес отклоняется
DTD в сообщении не допускается (запрещён и стандартом SAML 2.0)
Где стоит подпись по Assertion или по корневому Response — оба варианта принимаются; Assertion в ответе должна быть ровно одна
Сертификат только тот, что задан в настройках поставщика; сертификат из KeyInfo самого ответа игнорируется. Принимается и полный PEM, и голое тело без обрамления; неверный сертификат отклоняется сразу при сохранении поставщика
Conditions обязательны NotOnOrAfter и AudienceRestriction; Audience должен совпадать с sp_entity_id — иначе утверждение выписано не нам
InResponseTo берётся из SubjectConfirmationData внутри подписанной Assertion, атрибут корня Response — только запасной источник
Запрос на выход подпись должна покрывать сам LogoutRequest, а не вложенный в него элемент

Поддерживаются утверждения Keycloak, ADFS и Okta.

Формат идентификатора: постоянный, а не одноразовый

Привязка входа к учётной записи строится на NameID, поэтому одноразовый формат (transient) как ключ привязки не годится: поставщик меняет значение при каждом входе, и к следующему разу привязка не найдётся.

  • При сохранении поставщика такой формат выбрать нельзя.
  • При входе привязка по нему не создаётся вовсе — даже если поставщик прислал transient вопреки запрошенному NameIDPolicy (так поступает SimpleSAMLphp с умолчаниями).
  • Впустить или отказать решает политика связывания по адресу. Разрешена (доверенные домены) — вход проходит по совпадению адреса. Запрещена — вход отклоняется сразу, с объяснением.
  • Явную привязку («Привязать вход» в настройках профиля) по одноразовому идентификатору отвергают: следующий вход её бы не нашёл.

Годятся emailAddress, persistent и unspecified. Если поставщик формат не называет вовсе, утверждение принимается: по стандарту это unspecified.

Негодный необязательный атрибут вход не рвёт

Адрес и отображаемое имя из утверждения принимаются, только если проходят те же правила, что и при обычной регистрации: адрес — по формату, имя — по длине (максимум 255 символов). Негодное значение пропускается с предупреждением в журнале сервера, а вход продолжается: без имени работать можно, без входа — нет. То же правило действует для входа через OAuth и LDAP.

Если вход не проходит

Причина отказа пишется в журнал сервера предупреждением с полем check — именем непройденной проверки:

check Что не так
base64, utf8 ответ не декодируется — проверьте привязку (binding) на стороне поставщика входа
status поставщик вернул не Success
issuer Issuer в ответе не совпадает с idp_entity_id в настройках
signature подпись не сошлась, сертификат не тот или алгоритм не из таблицы выше
assertion-scope подпись не покрывает Assertion, или их в ответе больше одной
conditions вышел срок (NotOnOrAfter), нет AudienceRestriction или Audience не совпал с sp_entity_id
in-response-to ответ не соответствует ни одному отправленному запросу (или он уже использован)
replay это же утверждение уже предъявлялось
attributes в утверждении нет NameID
transient-name-id поставщик вернул одноразовый идентификатор (transient), а связывание по адресу у него запрещено — см. «Формат идентификатора» выше
email поставщик не передал email, а NameID на email не похож — регистрировать нового пользователя не из чего
registration пользователь новый, а регистрация на установке закрыта (Администрирование → Система)
auto-register пользователь новый, а автоматическая регистрация у поставщика выключена
email-link учётная запись с таким адресом уже есть, но связывание по адресу запрещено политикой поставщика или домен адреса не в списке доверенных
link-owner этот вход SAML уже привязан к другой учётной записи
slo-unsigned, slo-signature, slo-scope, slo-name-id то же для запроса на выход: без подписи, подпись не сошлась, подпись покрывает не сам запрос, нет NameID

OpenID Connect (как поставщик)

GitRiver может выступать OAuth2/OIDC поставщиком для других сервисов:

  • GET /.well-known/openid-configuration — конфигурация
  • GET /.well-known/jwks.json — публичные ключи

Подпись id_token — RS256. Ключ заводится сам при первом запуске: приватная часть хранится в базе зашифрованной (ключ шифрования выводится из jwt_secret), публичная отдаётся в JWKS. Отдельной настройки не требует, и в резервную копию базы попадает вместе с остальными данными.

Из этого следуют два правила эксплуатации:

  • jwt_secret менять нельзя, не потеряв ключ подписи. После смены секрета прежний ключ подписи прочитать нельзя, установка заведёт новый, а id_token, выданные до смены, перестанут проверяться. Получатели переживут это как разовый повторный вход.
  • Восстановление базы из копии восстанавливает и ключ, поэтому получателям не нужно перечитывать JWKS после восстановления.

Что должен делать получатель (в этом порядке — как того требует OIDC Core):

  1. Взять ключи по jwks_uri и найти среди них тот, чей kid указан в заголовке id_token. Набор ключей — именно набор: получатель обязан выбирать по kid и перечитывать выдачу, когда встретил незнакомый, а не запоминать единственный ключ навсегда. Сейчас в выдаче один действующий ключ, и меняется он только вместе с jwt_secret (см. правила эксплуатации выше): тогда прежний ключ из выдачи уходит, а выданные им id_token перестают проверяться — получатели переживают это как разовый повторный вход. Отдельной команды смены ключа подписи, оставляющей прежний опубликованным, у установки нет.
  2. Проверить подпись этим ключом, а также iss, aud и exp.
  3. Сверить nonce с тем, что был отправлен в запросе авторизации. GitRiver возвращает его тем же значением. Если nonce в запросе не передавался, claim в токене отсутствует — пустым он не приходит никогда.

id_token, выданный при обновлении токена (grant_type=refresh_token), nonce не содержит: пользователь в этом обмене не участвует (OIDC Core 12.2).

Токен обновления одноразовый, и повторно предъявленный токен гасит разрешение. Каждое обновление выдаёт новую пару; прежний токен обновления не просто перестаёт работать — его повторное предъявление означает, что одной парой пользуются двое, и разрешение отзывается целиком (RFC 9700 §4.14.2). Отсюда правило для получателя: новую пару надо сохранить до того, как старая понадобится снова, а повторять обновление прежним токеном нельзя. Потерял ответ на обновление — начинай с запроса авторизации (RFC 6749 §10.4), а не повторяй прежним токеном: разрешение уже погашено, и пользователю понадобится выдать согласие заново.

Как получатель удостоверяется на /oauth/token — любым из двух способов, но ровно одним за раз (RFC 6749 §2.3): заголовком Authorization: Basic (значения кодируются как поля формы перед склейкой) либо полями client_id/client_secret в теле. Предъявление обоих сразу отклоняется, а не разрешается в чью-то пользу.

Публичные приложения (переключатель «Конфиденциальное» выключен — одностраничные приложения, мобильные и настольные клиенты) секрета не имеют и на /oauth/token не удостоверяются вовсе: в описании конфигурации (discovery) этот способ объявлен как none. Для них обязателен PKCE:

  • запрос авторизации без code_challenge (метод только S256) отклоняется — и на GET /oauth/authorize, до показа согласия, и на выдаче кода;
  • код, выданный публичному приложению без PKCE, к обмену не принимается.

Конфиденциальному приложению PKCE остаётся рекомендацией: там владение подтверждает client_secret.

Секрет публичному приложению не выдаётся вовсе — ни при создании, ни по кнопке «Создать секрет заново» (она у такого приложения не показывается, а запрос отклоняется): /oauth/token секрет у публичного приложения не спрашивает.

Природа приложения задаётся при создании и не меняется: конфиденциальное приложение не превратить в публичное и наоборот — нужно завести новое.

redirect_uri в запросе обмена обязателен и сверяется с тем, на который выдан код (RFC 6749 §4.1.3) — недостаточно, чтобы адрес просто был зарегистрирован у приложения.

Отказ /oauth/token приходит в форме RFC 6749 §5.2 — код в поле error и пояснение для человека в error_description:

error Что произошло Код ответа
invalid_request не хватает параметра (code, client_id, redirect_uri, code_verifier при коде с PKCE) или он предъявлен дважды 400
invalid_client клиент не удостоверен: секрет не тот или назван чужой client_id 401 + WWW-Authenticate
invalid_grant код потрачен, просрочен, выдан на другой redirect_uri, не подтверждён PKCE или выдан публичному приложению без PKCE 400
unsupported_grant_type grant_type не поддерживается 400

Двухфакторная аутентификация (2FA)

Для пользователей

  1. Настройки / Безопасность — включить TOTP
  2. Сканировать QR-код приложением (Google Authenticator, Authy)
  3. Сохранить резервные коды

API

Действие API
Получить QR-код GET /api/v1/auth/totp/setup
Включить TOTP POST /api/v1/auth/totp/enable
Отключить POST /api/v1/auth/totp/disable
Новые резервные коды POST /api/v1/auth/totp/backup
Проверка при входе POST /api/v1/auth/totp/verify

Внешние входы

Второй фактор спрашивается на всех способах входа: локальный пароль, каталог LDAP, SAML и OAuth. Поставщик подтверждает только, кто пришёл; сеанс выдаётся после одноразового или резервного кода, и внешний вход приводит на ту же страницу ввода кода /login/totp, что и обычный.


Хранилище (S3)

Настройка S3-совместимого хранилища для реестра контейнеров.

Действие API
Получить настройки GET /api/v1/admin/storage
Сохранить PUT /api/v1/admin/storage
Тест подключения POST /api/v1/admin/storage/test

Одно и то же хранилище (S3 или локальная файловая система) обслуживает реестр контейнеров, Git LFS и реестр Composer — переключение хранилища прозрачно для всех трёх.

Временный каталог для S3 настраивать не нужно. Файлы, которые разбираются только целиком (пакет .nupkg, ZIP сайта Pages), на время приёма кладутся в каталог upload-staging рядом с git_repos_path, а не в системный /tmp: место под них нужно на том же диске, что и репозитории. Поле temp_dir в ответе API ни на что не влияет.


Унаследованные артефакты CI

У архивов артефактов заданий, выполненных ранними версиями GitRiver, не записаны ни срок хранения, ни размер: фоновая чистка такие архивы не удаляет никогда, а квота max_ci_artifacts их не видит. На установке, долго работавшей на внешних исполнителях, это может быть весь накопленный каталог артефактов.

Простановка срока задним числом означает удаление накопленных архивов ближайшей чисткой, поэтому она не делается сама при обновлении версии. Это отдельное действие администратора в разделе Администрирование → Хранилище, всегда в два шага.

Действие API
Отчёт: сколько заданий и байт GET /api/v1/admin/ci-artifacts/legacy
Поставить архивы на учёт POST /api/v1/admin/ci-artifacts/legacy/recompute

Отчёт ничего не меняет: показанные числа — ровно то, что произойдёт при постановке на учёт. В отчёте:

  • заданий без срока и их объём — им будут записаны срок и размер;
  • сколько из них получат срок по умолчанию (30d) — в рабочем процессе коммита expire-in не задан либо сам процесс уже не прочитать (коммит или файл исчезли);
  • бессрочные (expire-in: never) — им записывается только размер: место в квоте архив занимает, но удалению не подлежит, и срок ему не ставится;
  • задания без архива на диске — учёта не требуют.

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


Целостность архивов CI

Архив артефактов или кеша появляется под своим именем только целиком: пока он собирается или принимается, он лежит рядом под временным именем (<имя>.tar.gz.tmp-…) и получает конечное имя, лишь когда полностью записан на диск. Это верно и для архивов от внешнего исполнителя, и для архивов встроенного исполнителя.

Что это означает для администратора:

  • после аварии машины (потеря питания, паника ядра — не остановка сервиса) архив либо на месте и цел, либо его нет вовсе. Укороченного .tar.gz, на котором зависимое задание падает при распаковке, а кеш восстанавливается битым, не будет: отсутствие архива — это штатный промах, повторный запуск задания соберёт его заново;
  • одновременные задания с общим ключом cache: не мешают друг другу. Пока одна сохраняет кеш, другая распаковывает прежний архив целиком, а не наполовину записанный; остаётся архив того задания, которое завершило сохранение последним;
  • неудачная упаковка не разрушает то, что уже накоплено: если tar не смог собрать архив, прежний кеш по этому ключу остаётся как был;
  • цена — сброс архива на диск перед тем, как он получит своё имя. На быстром локальном диске это малая доля времени, за которое архив идёт по сети; на медленном или сетевом хранилище доля заметнее.

Временные файлы .tmp-… убираются сами на любом исходе — и на удачном, и на отказе; уцелевший временный файл остаётся только от процесса, убитого посреди записи. Ни артефактом задания, ни архивом кеша он не считается (и тем, и другим считается только имя, оканчивающееся на .tar.gz), в квоты max_ci_artifacts и max_ci_cache не входит. В каталоге артефактов остаток уходит вместе с каталогом конвейера, а в каталоге кеша его снимает обслуживание кеша — но лишь когда файлу больше суток: свежий может быть идущей прямо сейчас записью архива, и её обрыв разрушил бы кеш работающей сборки.


Git LFS

LFS-объекты хранятся в общем хранилище (S3 или файловая система), адресуясь по содержимому (SHA-256 объекта). Отправка идёт через git lfs push, получение — через git lfs fetch/pull.

Хранилище общее на всю установку, а право на объект — нет: принадлежность репозиторию учитывается отдельно, и выдача, batch и verify отвечают только по объектам своего репозитория. Отсюда два следствия для администратора:

  • один и тот же файл, впервые попадающий в очередной репозиторий, присылается клиентом заново, даже если такой же уже лежит в хранилище: место на диске он не займёт (хранилище адресуется содержимым), а трафик потратит;
  • форк получает права на объекты родителя в момент создания; у форков, созданных до обновления, эти права появляются сами при первом запуске новой версии.

Расход LFS считается по привязкам, поэтому форк увеличивает расход владельца на размер унаследованных объектов, хотя места на диске не занимает. Предел LFS при создании форка не применяется именно поэтому; загрузку новых объектов сверх предела сервер по-прежнему отклоняет.

Исполнитель CI и LFS

Встроенный исполнитель получает LFS-объекты рабочей копии через HTTP API сервера по адресу base_url, поэтому этот адрес должен быть доступен встроенному исполнителю. Доступ даёт короткоживущий токен (по умолчанию 15 минут — lfs_token_ttl_secs): он записывается только в .git/config рабочей копии задания и удаляется вместе с ней после задания.

Срок действия токена LFS

# gitriver.toml
lfs_token_ttl_secs = 900   # по умолчанию; увеличить, если большие объекты
                           # не успевают скачаться за 15 минут

или переменной окружения: GITRIVER_LFS_TOKEN_TTL_SECS=1800.

Значение должно быть не меньше времени, за которое git lfs fetch скачивает самый крупный двоичный объект в самом долгом задании CI. При меньшем сроке скачивание оборвётся на середине с кодом 401.


Вики

Вики репозитория хранится отдельным bare-репозиторием на диске рядом с самим репозиторием: {git_repos_path}/{owner}/{repo}.wiki.git. В резервную копию она попадает вместе с каталогами репозиториев, отдельной настройки не требует.

У вики три постоянных ограничения:

  • Вики — не отдельный репозиторий. Её каталог заводится при создании ПЕРВОЙ страницы: у репозитория, вики которого не трогали, каталога нет вовсе. В списках репозиториев вики не появляется, к ней нельзя привязать запрос на слияние, и рецензирования правок вики не бывает.
  • Git-доступа к вики нет. Клонировать .wiki.git и отправлять в неё изменения по HTTP или SSH нельзя. Единственный вход — интерфейс и API вики (/api/v1/repos/{owner}/{name}/wiki/...).
  • Ветка одна — main, задана в самой платформе. Настройки, меняющей её, нет, второй ветки у вики не бывает.

Права вики берутся у репозитория: чтение доступно тем, кто видит репозиторий, правка — тем, у кого есть право записи. Отдельного управления доступом к вики нет и не требуется.


Kubernetes-кластеры

Подключение кластеров для развёртывания приложений.

Действие API
Список кластеров GET /api/v1/admin/k8s-clusters
Добавить POST /api/v1/admin/k8s-clusters
Тест подключения POST /api/v1/admin/k8s-clusters/{id}/test

Безопасность

Ограничение доступа по сетевому адресу

Правила бывают двух родов, и приговоры у них НЕЗАВИСИМЫЕ: пускают, только если разрешили оба.

Род Кто заводит API
Глобальные — «кого пускать на установку» администратор установки /api/v1/admin/ip_rules
Групповые — «кого пускать в группу» владелец группы либо администратор установки /api/v1/groups/{path}/ip_rules

Внутри одного рода набор без совпадений трактуется так: только allow — это список допуска, всё непопавшее закрыто; только deny — запрещающий список, всё непопавшее открыто.

Правило группы действует на ВСЕ входы, а не только на веб-интерфейс: /api/v1, слои совместимости /api/v3 и /api/v4 (у /api/v4 — обе формы адреса: /api/v4/projects/{путь} и /api/v4/projects/{номер}), git по HTTP и по SSH, LFS и реестр образов. Отдельного исключения нет ни у одного вида токена.

Проверьте сеть исполнителей. CI клонирует репозиторий обычным git-путём и предъявляет токен задания. Если группа закрыта списком допуска, адреса исполнителей обязаны в него попадать — иначе задания начнут падать на этапе клонирования. Исключений для служебных токенов нет.

Правило родительской группы действует и на подгруппы. Членство в группе наследуется вниз по дереву, ограничение по адресу — вместе с ним. Своим allow подгруппа ограничение родителя не отменяет: разрешение требуется на каждом уровне.

Перечисления. Из списков и из области поиска по коду уходят: репозитории закрытой группы, сама группа, её задачи и запросы на слияние в сводках, уведомления и события ленты. Страница выдачи при этом может оказаться короче запрошенной — листание не обрывается и объектов не теряет.

Что ограничение НЕ прячет (и это намеренно): занятость имени (GET /api/v1/namespaces/{имя} — распределение имён общее на установку), личные счётчики на главной и тепловую карту активности. Это сводные числа и распределение имён, а не содержимое группы.

Действия над группой закрыты так же, как чтение: с запрещённого адреса нельзя ни создать в ней репозиторий, ни завести подгруппу, ни перенести репозиторий из неё или в неё.

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

Отказ по сети отличим от «не найдено». Тому, кто репозиторий и так видит, приходит 403 с причиной «доступ с этого сетевого адреса запрещён правилами». Тому, кто его не видит, — 404, как на несуществующий адрес.

Защита от самоблокировки. Просмотр и снятие правил, а также GET /api/v1/me/ip (свой адрес, как его видит сервер), сами под ограничение НЕ подпадают и доступны в любой редакции. Заведение и правка правил установки — возможность уровня установки (редакция Max), правил группы — редакция Pro и место; но после истечения лицензии фильтр продолжает действовать, и снять запершее правило можно всегда — без правки базы данных. Интерфейс в редакции Community показывает список и кнопку удаления, а «Добавить» отключает.

За обратным прокси адрес берётся из X-Real-IP/X-Forwarded-For только если соединение пришло с доверенного адреса (trusted_proxies в конфигурации; по умолчанию — loopback и приватные сети). От остальных адресов эти заголовки не принимаются. Если адрес определить не удалось, запрос отклоняется везде, где правила заведены.

Это же правило действует везде, где адрес показывается человеку: список входов в профиле («Настройки» → «Сеансы»), записи об удачных и неудачных входах, сеанс, выданный мастером первоначальной настройки. Адрес из заголовка, пришедшего не через доверенный прокси, в них не попадает; адрес, определить который не удалось, остаётся пустым.

Сканирование кода

  • Находки безопасности — результаты статического анализа (SAST)
  • Импорт SARIF — загрузка результатов внешних сканеров
  • Проверка лицензий — проверка лицензий зависимостей

Настройки безопасности репозитория

Действие API
Параметры GET /api/v1/repos/{owner}/{name}/security/settings
Обновить PATCH /api/v1/repos/{owner}/{name}/security/settings
Запустить сканирование POST /api/v1/repos/{owner}/{name}/security/scan

Требования к паролю

Требование Значение
Наименьшая длина 12 символов (именно символов: пароль из кириллицы считается так же, как из латиницы)
Наибольшая длина 1024 символа — длинная парольная фраза проходит свободно
Сверка со списком утёкших пароль отвергается, если встречается в списке чаще всего утекавших (ASVS 2.1.7)

Список утёкших встроен в поставку и обращений в интернет не требует: это верхние сто тысяч паролей из свода NCSC по данным Have I Been Pwned, из которых оставлены записи не короче наименьшей длины — остальные и так не проходят по длине. Регистр при сверке не различается: Password12345 и password12345 — одна и та же догадка подбирающего.

Проверка применяется там, где пароль ЗАДАЁТСЯ: регистрация, смена пользователем, сброс администратором, мастер первоначальной настройки. Уже заведённые пароли к смене не принуждаются: после обновления все входят как прежде.

Список обновляется вместе с выпусками, вручную его не редактируют. Условия распространения списка приложены к поставке (THIRD-PARTY-NOTICES.md, раздел «Встроенные данные»).

Пределы частоты запросов

Предел действует на каждую реплику сервера отдельно: за балансировщиком нагрузка делится между репликами, и суммарная пропускная способность установки кратна их числу — таблица ниже даёт предел ОДНОЙ реплики, а не всей установки. Превышение — 429 Too Many Requests с телом {"error", "code": "rate_limited", "retry_after"} и заголовками Retry-After, X-RateLimit-Limit, X-RateLimit-Remaining.

Группа Маршруты Ключ Предел, запросов в минуту
auth вход, регистрация, проверка второго фактора адрес 10
sso вход через внешнего поставщика: /api/v1/auth/saml/** и /api/v1/auth/oauth/**, любым методом адрес 300
oidc выдача кода и токена стороннему приложению: /oauth/authorize, /oauth/token, /oauth/userinfo адрес 600
scim подача учётных записей: /scim/v2/** токен поставщика 600
api_write POST/PUT/PATCH/DELETE своего интерфейса (/api/v1/) и слоёв совместимости (/api/v3, /api/v4) адрес 60 анонимно, 300 с токеном
search поиск по коду адрес 20 анонимно, 60 с токеном
git_write git-receive-pack (отправка по HTTP) адрес 30 анонимно, 120 с токеном
runner протокол внешнего исполнителя: /api/v1/runner/**, включая чтения токен исполнителя 600
ai обращения к языковой модели: разбор упавшего задания, разбор изменений, проверка связи с поставщиком токен, иначе адрес 10
knowledge_view отметка показа и чтения записи знания: .../knowledge/shown, .../knowledge/entries/{id}/read токен, иначе адрес 600

Чтения API (GET, кроме поиска, SSO, OIDC, SCIM, протокола исполнителя) не ограничиваются.

Группу определяет маршрут, а не адрес запроса. Запись через слой совместимости расходует тот же предел, что и та же запись через /api/v1, — сменой диалекта предел не обойти. Запросы по несуществующим адресам в счётчики не попадают.

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

Вход через внешнего поставщика считается при любом методе запроса, включая GET: часть шагов SAML идёт по GET, а slo/callback принимает и POST, и GET — предел на оба общий. Идентификатор поставщика публичен (GET /api/v1/auth/saml/providers), и аутентификация на этом шаге не нужна.

Предел sso щедрее анонимного пишущего потому, что ключ — адрес соединения: за обратным прокси весь вход организации приходит с одного адреса и делит один предел. 300 в минуту — это пять входов в секунду на реплику. Порог один и с Authorization: у входа токена ещё нет.

Контуры вне /api/v1/ считаются своими группами: наплыв на /oauth/token не отбивает вход сотрудников через поставщика.

Метаданные поставщика (/.well-known/openid-configuration, /.well-known/jwks.json) предела не имеют: получатели обращаются к ним чаще всего при смене ключа, и отказ сломал бы им проверку подписи. Срок, на который получатель может сохранить ответ, объявлен в Cache-Control.

Страница интерфейса /oauth/callback в группу oidc не входит и пределом не ограничивается.

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

Пределом не покрыты выдача репозитория по HTTP (git-upload-pack), LFS, реестр образов (/v2/**) и Pages. Здесь один шаг пользователя — это запрос на КАЖДЫЙ объект (git push с большими файлами обращается к LFS на каждый файл, docker push выгружает слой частями), а все исполнители организации часто приходят с одного адреса. Если предел для них нужен, ставьте его на обратном прокси, где виден настоящий адрес клиента.

Повышенный предел (вторая колонка порога) даёт заголовок Authorization: Bearer с JWT-подобным токеном. Подпись токена на этом шаге не проверяется, поэтому заголовок только поднимает порог, а ключом остаётся адрес: подстановкой произвольной строки собственный предел не получить. У auth, sso, oidc, scim, runner и ai порог один.

Адрес берётся из соединения, X-Forwarded-For не читается. За обратным прокси все клиенты приходят с одного адреса и делят предел группы. Для интерфейса и API это ожидаемо (прокси обычно ставит и свой предел); исполнители и SCIM считаются по токену и этого не замечают.

Протокол исполнителя считается по токену. Ключ — отпечаток предъявленного токена, а не адрес: исполнители за одним NAT или за общим прокси не делят предел, и добавление исполнителя не роняет уже работающие. Предел 600 запросов в минуту на исполнителя — страховка от зациклившегося исполнителя, а не рабочая норма: при опросе раз в 2 секунды исполнитель выбирает меньше сотни. Норму задаёт --poll-interval, см. Запуск внешнего исполнителя.

Исполнитель переживает отказ по частоте сам. Получив 429 на любом вызове протокола, он ждёт названное в Retry-After время (не дольше 60 секунд) и повторяет запрос — до трёх раз. Свой предел может стоять и на обратном прокси перед сервером. Исчерпав повторы, исполнитель называет причину в журнале задания вместе с кодом 429 — например в строке о несохранённом архиве артефактов, — так что отказ не проходит незамеченным.

Перебор пароля ограничен отдельным счётом. Кроме предела группы auth, каждая неудачная попытка входа копится по паре «имя пользователя — адрес»: пять неудач за пятнадцать минут останавливают дальнейшие попытки этой пары ещё до проверки пароля. Счёт общий для веб-входа, Basic-пароля у git по HTTP, LFS и реестра образов и входа в мастере первоначальной настройки; успешный вход счёт пары снимает. Счёт ведётся по паре, а не по одному имени, поэтому с чужого адреса нельзя запереть вход владельцу имени. Неудачей считается только настоящий отказ в учётных данных: сбой базы данных вход не запирает.

Исчерпанный счёт — это отказ ПО ЧАСТОТЕ, и веб-вход (POST /api/v1/auth/login) отвечает на него так же, как ограничитель частоты: 429 с кодом rate_limited, заголовком Retry-After и полем retry_after в теле. Интерфейс подставляет этот срок в сообщение на языке пользователя («Слишком много запросов. Повторите через 4м 12с»), а скрипт получает число, не разбирая текст. Остальные пути того же счёта — git по HTTP, LFS, реестр образов — отвечают в своих протоколах: их читает не браузер, а сторонний клиент.

Освобождение адресов от предела. Настройка rate_limit_exempt_cidrs (переменная GITRIVER_RATE_LIMIT_EXEMPT_CIDRS, подсети через запятую) перечисляет адреса, к которым пределы частоты не применяются: rate_limit_exempt_cidrs = ["10.20.0.0/16"]. По умолчанию список пуст — ограничены все.

Нужно там, где весь поток приходит с одного адреса и рабочие пороги мешали бы работе: прогон E2E, нагрузочная проверка, внутренняя интеграция. Освобождение снимает для названных адресов все пределы частоты, включая предел входа, поэтому список держат узким: подсеть испытательного стенда, а не «вся корпоративная сеть».

Чего оно НЕ снимает — счёта неудачных попыток пароля (пять на пару «имя пользователя — адрес» за пятнадцать минут, см. выше): он ведётся отдельно, и подбор пароля с освобождённого адреса остановится так же, как с любого другого.

Сверяется адрес TCP-соединения, а не заголовок X-Forwarded-For: за обратным прокси в списке должен стоять адрес самого прокси.

Запись 0.0.0.0/0 (и ::/0) освобождает всех и означает работу без пределов частоты вовсе. В рабочей установке так не делают.


Лицензирование

Подробная документация: licensing.md

Платные возможности закрылись, а лицензия оплачена

Такое бывает по одной причине: вышел срок подтверждения — подписанный ответ активации годен ограниченное время (от 14 до 90 дней по сроку подписки) и продлевается любой новой подписью издателя. Установка не получала её слишком долго.

Признаки: на странице Администрирование → Лицензия красная полоса «подтверждение просрочено», у записи лицензии значок вместо даты «подтверждение до …», в ленте активности событие license_lease_expired, а у администраторов — письмо о том же. Заранее, пока возможности ещё работают, о подходящем конце предупреждают событие license_lease_expiring и письмо: связь стоит чинить до того, как разделы закроются. Платные вызовы отвечают 403 с кодом license_required; Git, CI, задачи и все данные работают.

Платить не нужно ничего. Порядок:

  1. Открыть установке исходящую связь с сервером лицензий — она спрашивает подтверждение сама, раз в сутки, а у конца срока подтверждения и после его выхода — раз в 1–6 часов; кнопка Проверить сейчас не ждёт и этого.
  2. Если связи не будет (закрытый контур) — получить подписанный ответ активации тем же путём, каким лицензия активировалась: в личном кабинете владельца лицензии либо по лицензионному ключу (см. «Кто вправе получить подписанный ответ»), — и вставить его на той же странице. Для закрытого контура этот путь основной, а не запасной.

Места при этом не сняты: подтверждение их не трогает, и после него всё открывается само, без переактивации.

Отпечаток базы: восстановление из копии и обновление СУБД

Лицензия привязана к базе данных установки (п. 4.13 соглашения). Восстановление базы из резервной копии в новый кластер и мажорное обновление PostgreSQL (pg_upgrade) меняют эту привязку. Признаки: установка отказывается применить ответ активации словами «ответ выписан для другой базы», а на странице лицензии появляется полоса «лицензия привязана к другой базе данных» с отпечатком базы этой установки — его называют при обращении. Полоса появляется ДО того, как право кончится.

Отсюда практическое правило: перевыпуск заказывается заранее, вместе с планированием обновления СУБД, а не после него. Времени на это есть весь остаток срока подтверждения — прежний ответ продолжает действовать, пока он не вышел, и обновление базы само по себе платные возможности не гасит.

Порядок: на странице лицензии нажать «Добавить лицензию» и вставить тот же лицензионный ключ ещё раз — установка подготовит новый запрос активации — и получить по нему ответ тем же путём, каким лицензия активировалась (см. «Кто вправе получить подписанный ответ»). Ключа под рукой нет — назвать издателю отпечаток со страницы лицензии и попросить перевыпуск. Перевыпуск безвозмезден (п. 4.13 соглашения), и переносить базу можно не в аварийном режиме.

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

Часы базы данных и срок подтверждения

Держите время на машинах установки и базы данных синхронизированным (NTP). Если часы кластера базы убегали далеко вперёд — сбитый аппаратный таймер, снимок машины с датой из будущего, восстановление на машине с неверной датой, — сроки лицензии и подтверждения могут закончиться раньше календарных, и возврат часов к верному времени их не вернёт. Помогает перевыпуск ответа активации — тем же порядком, что и при переезде базы.


Журнал аудита

Администрирование → Аудит, GET /api/v1/admin/audit-log. Отбор — по пользователю (actor), типу события (op_type), репозиторию (repo_id) и времени (before); страница до 200 записей.

В журнал попадают три рода событий: действия с репозиториями и задачами (они же видны в публичной ленте), события входа — и административные изменения: то, ради чего журнал и читают.

Что изменили op_type Что в содержимом
Настройки установки: LDAP, почта, шаблоны писем, помощник, оформление, хранилище, уборка реестра, кластеры и конфигурации исполнителей в Kubernetes, SSH-сервер, регистрация, умолчания квот, резервные копии (создание, удаление, восстановление, расписание), политики лицензий, настройки безопасности репозитория и шаблоны поиска секретов update_instance_settings section, action
Поставщик входа заведён, изменён, снят (OAuth, SAML) create_auth_provider, update_auth_provider, delete_auth_provider kind, имя и идентификатор
Пропуск выдан, изменён или отозван: персональный токен, токен развёртывания, токен внешнего управления, ключ SSH, ключ GPG, ссылка на скачивание резервной копии, приложение OAuth и его секрет, выданный приложению доступ, сеанс входа issue_credential, update_credential, revoke_credential kind, имя или отпечаток
Вход учётной записи закрыт и возвращён block_user, unblock_user target
Прямой доступ к репозиторию выдан или отобран add_collaborator, remove_collaborator имя и роль
Видимость репозитория изменена change_repo_visibility from, to
Репозиторий передан transfer_repo from, to, to_kind
Группа заведена или снята; состав и роли изменены create_group, delete_group, add_group_member, remove_group_member, change_group_member_role путь группы, пользователь, роль
Вебхук заведён, изменён, снят create_webhook, update_webhook, delete_webhook scope, адрес доставки
Своя роль заведена, изменена, снята, назначена, снята с человека create_custom_role, update_custom_role, delete_custom_role, assign_custom_role, unassign_custom_role группа, роль, пользователь
Правило доступа по адресу заведено, изменено, снято create_ip_rule, update_ip_rule, delete_ip_rule scope, cidr, action
Квота владельца изменена update_quota owner, идентификатор, limit (storage, ai_tokens, knowledge_drafts), action
Пароль сменён — самим человеком или администратором password_reset target, source
Переменная CI заведена, изменена, снята update_ci_variable, delete_ci_variable scope, имя переменной
Окружение развёртывания изменено или снято update_environment, delete_environment имя окружения, признак подтверждения

Удаление политики лицензий снимает её со всех репозиториев, где она стояла, — на каждый пишется своя запись action: unbind с причиной policy_deleted и самим репозиторием, так что снятие блокировки слияния видно в журнале каждого репозитория. Повторное удаление отвечает 404 и записи не оставляет.

Значения секретов в журнал не пишутся никогда. Записывается факт: токен выдан, переменная изменена, пароль сброшен — но не сам токен, не значение переменной и не пароль. Адрес вебхука при этом записывается: он отвечает на вопрос, куда начали уходить данные, и секретом не является.

Административные записи помечены как приватные: в публичные ленты репозиториев и профилей они не попадают, их видит администратор в журнале и сам человек в своей ленте.


Мониторинг

Проверка живости

GET /health

Отвечает 200 с состоянием и версией, а при недоступной базе — 503. Адрес — в корне, а не под /api/v1.

Ответ приходит быстро в обоих случаях: проверка базы занимает не больше двух секунд. Поэтому пробе достаточно timeoutSeconds: 3 — она получит именно 503, а не обрыв по собственному сроку, и в журнале будет видно, что сервер ответил. В поставляемом чарте стоит timeoutSeconds: 5 — с тем же запасом.

Prometheus-метрики

GET /metrics

Метрики в формате Prometheus: HTTP-запросы, статистика конвейеров и другое.

Срез ресурсов CI. Предел срезу задаёт systemd, а не настройки сервера. Коротко то же самое показывает панель администрирования («Система» → «Срез ресурсов CI»): имя, пределы, занятую память и счётчики срабатываний. Метрики нужны тому, кто хочет видеть это во времени:

Метрика Что показывает
gitriver_ci_slice_cpu_quota_cores квота ЦП среза в ядрах (CPUQuota=)
gitriver_ci_slice_memory_max_bytes предел памяти среза (MemoryMax=)
gitriver_ci_slice_memory_current_bytes сколько памяти срез занимает сейчас
gitriver_ci_slice_cpu_throttled_seconds_total сколько задания среза простояли, упершись в квоту ЦП
gitriver_ci_slice_oom_kills_total сколько процессов ядро завершило, упершись в предел памяти среза

Последние две — счётчики: смотреть их следует производной (rate(gitriver_ci_slice_cpu_throttled_seconds_total[5m])). Растёт первая — сборки идут медленнее, чем могли бы; растёт вторая — заданиям не хватает памяти. Что с этим делать — руководство по установке.

Метрик нет вовсе, если срез выключен (ci_cgroup_parent = "") или его cgroup серверу недоступна на чтение — например, сервер в контейнере со своим пространством имён cgroup. Нулей в этом случае не публикуется: ноль читался бы как «предел снят».

Доступ. По умолчанию адрес открыт без входа. Имён репозиториев и пользователей в метриках нет (метка пути — шаблон маршрута, а не сам путь), но сводные счётчики установки — сколько пользователей, репозиториев, находок безопасности — видны всякому, кто дотянется до адреса. На установке, доступной из интернета, задайте токен:

metrics_token = "длинная-случайная-строка"

или переменную окружения GITRIVER_METRICS_TOKEN. Тогда сборщик должен слать заголовок Authorization: Bearer <токен> — в Prometheus это authorization.credentials в описании цели сбора.

DORA-метрики

Метрика API
Deployment Frequency, Lead Time, Change Failure Rate, MTTR GET /api/v1/repos/{owner}/{name}/dora/metrics
Value Stream Analytics GET /api/v1/repos/{owner}/{name}/dora/vsa

Новые версии

Установка раз в сутки спрашивает у издателя, вышла ли версия новее, и говорит об этом сама — на странице «Система» рядом с номером версии и письмом администраторам. Письмо уходит ОДИН РАЗ НА ВЕРСИЮ: отметка о том, что эту версию уже объявляли, переживает перезапуск.

Ничего не скачивается и не устанавливается. Ответ издателя не подписан, и доверять ему как команде нельзя: итог опроса — строка со ссылкой, а решение и проверка подписи пакета остаются за администратором.

Действие API
Спросить сейчас GET /api/v1/admin/update-check
Итог последнего опроса GET /api/v1/admin/system, поле update

Ответ проверки различает три исхода: вышла новая версия, установлена последняя и проверить не удалось (checked: false с причиной). Третий случай не выдаётся за второй: «обновлений нет» и «мы не смогли спросить» — разные ответы.

Настройки (gitriver.toml, подробности — в руководстве по установке):

Настройка По умолчанию Зачем менять
update_check_url https://gitriver.com/releases/latest зеркало в закрытом контуре, адрес посредника
update_check_enabled true у бесплатной установки это единственное обращение наружу; false выключает его целиком

У установки с лицензией наружу ходит ещё отметка активности (см. licensing.md), у бесплатной — только эта проверка.


Оформление

Оформление внешнего вида: логотип, заголовок, цвета.

Действие API
Получить GET /api/v1/admin/branding
Обновить PUT /api/v1/admin/branding
Публичный GET /api/v1/branding

Исполнители CI/CD

Встроенный исполнитель

По умолчанию GitRiver выполняет CI-задания на хост-машине через Docker.

Внешние исполнители

Действие API
Список исполнителей GET /api/v1/admin/runners
Зарегистрировать POST /api/v1/admin/runners
Обновить PUT /api/v1/admin/runners/{id}
Приостановить / вернуть в работу POST /api/v1/admin/runners/{id}/pause (`{“paused”: true
Перевыпустить токен POST /api/v1/admin/runners/{id}/token
Удалить DELETE /api/v1/admin/runners/{id}

Распределение заданий по меткам:

# В рабочем процессе
jobs:
  build:
    runs-on: [self-hosted, linux, gpu]

Область исполнителя

Исполнитель видит задания только своей области:

Область Что забирает Кто регистрирует
Весь сервер (instance) задания всех репозиториев администратор сервера
Группа (group) задания репозиториев этой группы, а с признаком «обслуживает подгруппы» — и всего её поддерева сопровождающий или владелец группы
Репозиторий (repo) задания одного репозитория администратор репозитория (право ci:admin)

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

Подгруппы включает владелец машины — переключателем «Обслуживает подгруппы» (поле covers_subgroups, по умолчанию включён у новых исполнителей группы). Включая его, учтите: задания подгрупп запускают на машине скрипты тех, кому в самой группе-предке может быть не выдано никаких прав. Решение за тем, кто отвечает за машину.

При обновлении с версий до 1.1.0 поведение не меняется. У исполнителей групп, заведённых раньше, переключатель после обновления выключен: они продолжают брать задания только своей группы. Включите его там, где хотите обслуживать поддерево, — на странице группы → «Исполнители» или PUT /api/v1/admin/runners/{id} с {"covers_subgroups": true}.

Администратор задаёт область явно — полем scope в теле запроса ({"type": "repo", "id": "<uuid>"}; без поля — instance). Поле covers_subgroups принимается только вместе с областью «группа»: для остальных областей запрос отклоняется.

Владельцам репозиториев и групп доступна регистрация исполнителей своей области; область там подставляет сервер из адреса запроса, тело её не задаёт:

Действие API
Исполнители репозитория GET/POST /api/v1/repos/{owner}/{name}/runners
Приостановить исполнителя репозитория POST /api/v1/repos/{owner}/{name}/runners/{id}/pause
Перевыпустить токен POST /api/v1/repos/{owner}/{name}/runners/{id}/token
Удалить исполнителя репозитория DELETE /api/v1/repos/{owner}/{name}/runners/{id}
Исполнители группы GET/POST /api/v1/groups/{path}/runners
Приостановить исполнителя группы POST /api/v1/groups/{path}/runners/{id}/pause
Перевыпустить токен POST /api/v1/groups/{path}/runners/{id}/token
Удалить исполнителя группы DELETE /api/v1/groups/{path}/runners/{id}

Список показывает исполнителей, которые обслуживают эту область: своих и унаследованных сверху — исполнителей групп-предков и общих исполнителей сервера. Унаследованные помечены признаком inherited и идут после своих; управление ими остаётся там, где они заведены — пауза, перевыпуск токена и удаление из чужой области отвечают 404. Они показываются, потому что забирают задания этой области и получают её секреты: список полностью отвечает на вопрос «кто выполняет мои сборки». Токен возвращается один раз — при регистрации.

Исполнитель живёт ровно столько, сколько его область: при удалении репозитория или группы (в том числе когда группа уходит вместе с родительской) их исполнители удаляются, и выданные им токены перестают действовать. Отдельно отзывать их не нужно. Исполнителей сервера (instance) это не касается.

В интерфейсе: «Настройки» репозитория → «Исполнители», страница группы → «Исполнители».

Пауза и перевыпуск токена

Пауза отбирает у исполнителя задания, не удаляя его. Приостановленный исполнитель:

  • новых заданий не получает (очередь его не выбирает);
  • задание, начатое до паузы, доводит до конца и присылает результат — пауза не срывает идущую сборку;
  • сохраняет имя, метки и область, продолжает отмечаться на связи;
  • возвращается в работу тем же вызовом с {"paused": false} и берёт задания с первого же опроса (несколько секунд).

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

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

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

Регистрация, удаление, пауза, возврат в работу и перевыпуск токена попадают в журнал аудита («Администрирование» → «Аудит») отдельными событиями.

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

Автомасштабирование в Kubernetes

Действие API
Список конфигураций GET /api/v1/admin/k8s-runners
Создать POST /api/v1/admin/k8s-runners
Изменить / удалить PUT/DELETE /api/v1/admin/k8s-runners/{id}

Контроллер раз в десять секунд смотрит очередь и на каждое ожидающее задание с меткой match_label создаёт Job с одним подом gitriver-runner (не больше max_pods одновременно).

Образ пода задаётся полем runner_image конфигурации. Образ исполнителя выкладывается вместе с выпуском — тем же репозиторием реестра, что и образ сервера, отдельным тегом (<образ сервера>:<версия>-runner); укажите здесь его полный адрес в том реестре, из которого кластер получает образы. При создании поле обязательно: короткое имя вроде gitriver-runner:latest кластер ищет в Docker Hub и, не найдя, оставляет под в ImagePullBackOff.

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

Токен уходит в Secret, а под читает его через secretKeyRef. Secret подчинён своему Job: кластер снимает их вместе, когда Job уходит по ttl_after_finished. Учётной записи, под которой GitRiver ходит в кластер, для этого нужны права create, patch, delete и list на secrets в пространстве имён исполнителей — те же, что уже нужны для jobs:

rules:
  - apiGroups: ["batch"]
    resources: ["jobs"]
    verbs: ["create", "get", "list", "delete"]
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["create", "patch", "delete", "list"]

Прав нет — автомасштабирование не останавливается: токен уходит прямо в спецификацию пода, а в журнал сервера пишется предупреждение с названием пространства имён. Спецификацию пода читает всякий, кому в этом пространстве имён доступны поды, поэтому права лучше выдать.

Запуск внешнего исполнителя

Исполнитель ставится своим пакетом (gitriver-runner_<версия>_<арх>.deb / .rpm) или запускается образом — порядок описан в установке. Пакет заводит службу gitriver-runner, читающую адрес и токен из /etc/gitriver-runner/runner.env; ниже — те же параметры, как их принимает сам исполнитель.

gitriver-runner run --url https://git.example.com --token grr_...
Параметр Переменная окружения По умолчанию Назначение
--workdir GITRIVER_RUNNER_WORKDIR /var/lib/gitriver-runner рабочий каталог заданий
--poll-interval GITRIVER_RUNNER_POLL_INTERVAL 5 интервал опроса сервера, секунды
--max-artifact-bytes GITRIVER_RUNNER_MAX_ARTIFACT_BYTES 4294967296 предел размера артефактов одного задания: и принимаемого архива, и распакованного содержимого — оба на диске, не в памяти
--max-artifact-entries GITRIVER_RUNNER_MAX_ARTIFACT_ENTRIES 200000 предел числа записей в архиве артефактов
--git-timeout GITRIVER_RUNNER_GIT_TIMEOUT 120 предел одной git-операции подготовки рабочей копии (clone, fetch, checkout), секунды
--lfs-timeout GITRIVER_RUNNER_LFS_TIMEOUT 600 предел получения LFS-объектов рабочей копии (git lfs pull), секунды
--eraser-image GITRIVER_RUNNER_ERASER_IMAGE busybox образ, которым снимается рабочая копия, закрытая файлами от root (см. ниже)
--cgroup-parent GITRIVER_RUNNER_CGROUP_PARENT gitriver.slice срез ресурсов CI: cgroup, в которую идут ВСЕ контейнеры исполнителя. Пустая строка — без среза. Предел самому срезу задаёт systemd (см. ниже)

Срез ресурсов CI

Все контейнеры исполнителя — контейнер задания, after_script, сервисы services:, служебные контейнеры уборки рабочей копии — идут в одну cgroup (--cgroup-parent), и сама служба лежит в ней же (Slice=gitriver.slice в unit-файле). Имя и умолчание — те же, что у сервера: на машине, где стоят оба, расход CI считается вместе, а не двумя независимыми половинами.

Предел срезу задаётся systemd, а не настройками исполнителя:

systemctl set-property gitriver.slice CPUQuota=600% MemoryMax=12G

Unit-файл среза поставляется с серверным пакетом; на машине, где стоит только исполнитель, срез systemd заводит сам по первой ссылке на него, и команда выше работает так же. Без предела срез остаётся местом учёта: systemd-cgtop gitriver.slice.

Чего срез НЕ считает — сборку образов: её контейнеры демон Docker создаёт вне среза. Подробности и что с этим делать — руководство по CI.

Соединение с сервером

Адрес сервера задаётся --url. По http исполнитель работает — на нём держатся испытательные стенды и закрытые контуры без TLS, — но при старте предупреждает: этим соединением идут его собственный токен, переменные заданий вместе с секретами и учётные данные клонирования, и по http они идут открытым текстом. Для сервера на самой машине исполнителя (localhost, петлевой адрес) предупреждения нет: трафик её не покидает.

Рабочий каталог исполнителя

Рабочий каталог принадлежит исполнителю целиком: в нём лежат рабочие копии заданий с их учётными данными (.git-credentials с действующим CI_JOB_TOKEN, .docker/config.json с учётными данными реестра) и каталоги упаковки архивов. Поэтому при старте исполнитель проверяет, чей это каталог:

  • каталога нет — исполнитель создаёт его сам с правами 0700;
  • каталог принадлежит другому пользователю хоста (или по пути лежит символическая ссылка либо файл) — исполнитель не стартует и называет причину: работать в каталоге, содержимым которого распоряжается кто-то ещё, нельзя;
  • каталог свой, но открыт остальным (например 0755) — исполнитель закрывает его до 0700 и пишет об этом строку в журнал.

Каталог по умолчанию стоит вне общего /tmp именно поэтому: /tmp/имя может создать заранее любой пользователь машины, а /var/lib принадлежит root. Если исполнитель работает не от root, каталог ему заводит администратор:

sudo install -d -m 700 -o gitriver-runner -g gitriver-runner /var/lib/gitriver-runner

Свой путь задаётся --workdir; требования к нему те же, и держать в нём чужое (в том числе каталог другой службы) не следует. Исполнитель в контейнере: каталог, проброшенный с хоста, должен принадлежать тому пользователю, от которого исполнитель работает ВНУТРИ контейнера (обычно root), иначе старт откажет — и это не формальность: в каталог, принадлежащий другому пользователю хоста, тот дописывает символические ссылки, по которым исполнитель пойдёт своими правами.

При обновлении с прежних версий. Прежним умолчанием был /tmp/gitriver-runner. Исполнитель переходит на новый каталог, а за прежним прибирает сам: при первом же старте он делает по нему такой же проход уборки, снимает свои брошенные рабочие копии (вместе с учётными данными заданий внутри) и архивы, а опустевший каталог удаляет целиком. Делается это, только если каталог принадлежит пользователю исполнителя: /tmp общий, и каталог по этому пути мог завести не он. Если что-то осталось — задание ведёт ещё живой исполнитель прежней версии, лежит постороннее или права не пускают, — исполнитель пишет об этом строку с путём и рецептом (rm -rf /tmp/gitriver-runner) при каждом старте, пока остаток на диске. Чтобы остаться на прежнем пути, задайте его явно — --workdir /tmp/gitriver-runner; тогда каталог обязан принадлежать пользователю исполнителя, иначе тот не стартует.

Артефакты и кеш задания исполнитель сначала пакует файлом в каталоге упаковки, а затем отправляет на сервер потоком прямо с диска: расход памяти исполнителя от размера архива не зависит, а вот место под архив нужно в рабочем каталоге. Каталог упаковки живёт до конца отправки и снимается сразу после неё — включая случай, когда сервер архив не принял.

Обратный путь устроен так же: архив зависимости (needs:) и архив кеша исполнитель принимает в тот же каталог файлом и распаковывает уже с диска, поэтому машине исполнителя довольно оперативной памяти под само задание, а не под размер архива. Место в рабочем каталоге планируйте с запасом: на время распаковки там лежат и принятый архив, и его распакованное содержимое — оба ограничены --max-artifact-bytes. Принятый архив снимается вместе с каталогом сразу после распаковки, а брошенный упавшим исполнителем подметается при следующем старте.

Код задания исполнитель забирает по HTTP, а учётные данные для этого выдаёт сервер вместе с заданием — отдельно от CI_JOB_TOKEN и только на чтение репозитория этого задания (см. CI: клонирование репозитория внешним исполнителем). Настраивать доступ на машине исполнителя не нужно, а зависший сервер или сеть прерывают задание по --git-timeout, а не блокируют исполнителя.

Рабочую копию исполнитель готовит так же, как встроенный исполнитель: скрипт задания получает учётные данные git из CI_JOB_TOKEN (git push работает без ручной сборки URL), а LFS-файлы приходят содержимым, а не файлами-указателями (см. CI: git и LFS в рабочей копии внешнего исполнителя). Для LFS на машине исполнителя нужен установленный git-lfs: без него задание продолжается, но в журнал уходит предупреждение, а LFS-файлы остаются файлами-указателями.

Артефакты предыдущих заданий исполнитель распаковывает сам, а не системным tar: запись с путём наружу, ссылка за пределы рабочего каталога и специальный файл пропускаются (в журнал задания попадает число пропущенных записей), а превышение пределов выше прерывает распаковку. Пределы стоит поднимать, если сборки проекта штатно дают более объёмные артефакты. Кеш задания (cache:) восстанавливается тем же кодом и под теми же пределами.

Рабочую копию задания исполнитель сносит сам, когда задание кончилось. Временные архивы отправки (артефакты и кеш) он собирает рядом с ней, в своём рабочем каталоге, и снимает вместе с каталогом упаковки. Если исполнителя убили посреди задания (SIGKILL, перезапуск службы, падение хоста), и брошенный архив, и брошенная рабочая копия — вместе с учётными данными задания внутри неё — снимаются при следующем запуске: держать уборку в cron не нужно. Один --workdir могут делить несколько процессов исполнителя: уборка при старте одного не задевает ни упаковку, ни рабочую копию задания, которое в этот момент ведёт другой. Рабочая копия и каталог упаковки заняты файлами-замками (.job-{id}.lock и lock внутри .pack-*), поэтому снимать что-либо в --workdir посторонними средствами при работающем исполнителе не следует.

Остатки, брошенные исполнителем прежней версии, замка не имеют и потому снимаются при старте по ДАВНОСТИ: временные архивы отправки (.artifacts-upload-*, .cache-upload-*) через час, рабочие копии заданий — через сутки, заведомо больше самого долгого задания (потолок timeout: — 6 часов). Рабочая копия при этом обязана выглядеть рабочей копией: каталог без клона и служебных файлов задания уборка не трогает, даже пролежав в --workdir сколько угодно. Обратная сторона того же правила: --workdir принадлежит исполнителю целиком, и держать в нём своё — в том числе собственные клоны репозиториев — не следует. Ручного rm -rf не нужно, останавливать исполнителя — тоже: задание, которое в этот момент ведёт другой процесс исполнителя (в том числе прежней версии, в окно обновления), под эти сроки не попадает.

Скрипт задания с image: выполняется в контейнере от root, и файлы, созданные им в рабочей копии, принадлежат root. У образов с umask 0077 (например redis:7) это закрывает и каталоги: обойти их пользователю исполнителя нечем. Штатно исполнитель приводит владельца сразу после скрипта, а при уборке брошенной копии повторяет то же контейнером ТОГО ЖЕ образа — образ задания он запоминает в замке задания (.job-{id}.lock) при занятии рабочей копии.

Если образа задания в замке нет (остаток исполнителя ПРЕЖНЕЙ версии; задание без image:, поднявшее контейнер само) или демон контейнеров отказывает в смене владельца (userns-remap, образ задания уже удалён из локального кеша), в дело идёт образ-«ластик»: он удаляет файлы от root, а не возвращает к ним доступ, и потому берёт любой остаток — в том числе оставленный docker buildx под чужим uid. Тот же порядок и с тем же образом применяет встроенный исполнитель сервера (ci_eraser_image, см. установку). Поэтому брошенная копия задания уходит с диска при следующем запуске исполнителя сама.

Образ по умолчанию — busybox (около 4 МБ). На машине его может не быть: тогда исполнитель загружает его при первой надобности, то есть при первой же уборке закрытого каталога. В закрытом контуре сеть для этого недоступна — укажите свой образ из доступного реестра (подойдёт любой с sh, find и rm) или загрузите busybox заранее:

gitriver-runner run --url ... --token ... --eraser-image registry.example.com/base/busybox:1.36

Остаётся один случай, когда снять остаток исполнитель не может: образа-«ластика» на машине нет и загрузить его неоткуда. Тогда при каждом старте исполнитель пишет об остатке одну строку с путём и рецептом:

не снято (брошенная рабочая копия): <причина> — снимите вручную: sudo rm -rf /var/lib/gitriver-runner/<job_id>

Строка пишется предупреждением в журнал самого исполнителя, с полем path — путём к остатку.

Снять такой каталог может только тот, у кого есть права на файлы root, — либо устранить причину (загрузить образ, задать доступный --eraser-image) и перезапустить исполнителя: уборку он делает при старте. Замок рядом с каталогом удалять отдельно не нужно: без каталога уборка снимет его сама при следующем запуске. Строка повторяется до тех пор, пока остаток лежит на диске, — и это единственный случай, когда в --workdir требуется ручное вмешательство.

Протокол исполнителя

Исполнители взаимодействуют через REST API:

  1. POST /api/v1/runner/heartbeat — сигнал жизни
  2. POST /api/v1/runner/fetch_task — получить задание
  3. POST /api/v1/runner/update_status — обновить состояние
  4. POST /api/v1/runner/upload_log — загрузить журналы
  5. POST /api/v1/runner/upload_artifact — загрузить артефакты задания
  6. GET /api/v1/runner/artifact — забрать артефакты задания-зависимости
  7. GET /api/v1/runner/cache — восстановить кеш задания по ключу
  8. POST /api/v1/runner/upload_cache — сохранить кеш задания по ключу

Артефакты предыдущих заданий исполнитель забирает по имени задания: репозиторий и конвейер сервер выбирает по заявленной записи очереди, а имя сверяет с областью выдачи этого задания — артефакты чужой сборки по подобранному имени недостижимы. Область та же, что у встроенного исполнителя: у задания с needs: это его зависимости, у задания без них — задания своего конвейера, уже оставившие артефакты. Состав исполнителю называет сервер, в задании. Отсутствие архива (зависимость артефактов не оставила) — 404, для исполнителя штатный исход. Предел размера принимаемого архива артефактов — 500 МБ; превышение обрывает приём отказом 400 с объяснением. Исполнители прежних версий артефакты зависимостей с этим сервером не получают: в журнале задания — 401, и задание идёт дальше без файлов зависимостей. Исполнителей нужно обновлять вместе с сервером.

Кеш (cache: в рабочем процессе) хранится на сервере, в каталоге CI-данных: cache/{repo_id}/{ключ}.tar.gz. Ключ исполнитель получает в задании уже с подставленными переменными и возвращает как есть; каталог сервер выбирает по репозиторию заявленного задания, поэтому кеш чужого репозитория по совпадающему ключу недостижим. Предел размера архива — 500 МБ, как у артефактов.

Своей настройки языка у исполнителя нет: язык журнала задания он получает от сервера вместе с заданием и пишет на нём все свои строки, а отказы сервера, которые попадают в журнал причиной, просит на том же языке. Язык закреплён за заданием, поэтому смена language у сервера посреди сборки журнала на двух языках не даёт. Исполнитель прежней версии поля языка не знает и пишет журнал по-русски — при language = "en" исполнителей нужно обновлять вместе с сервером.

Весь протокол ограничивается по частоте отдельно от остального API и по токену исполнителя, а не по адресу; исполнитель переживает отказ 429 повтором. Значения и причины — в разделе Пределы частоты запросов.

Состояние, журнал и артефакты принимаются только от исполнителя, который ведёт текущую попытку задания. Если задание отменили или сервер списал его по истёкшему сроку ожидания (runner_max_wait), результат, пришедший следом от прежнего исполнителя, получает 409 Conflict и отбрасывается; текущая попытка не затрагивается. Исполнитель на такой отказ прекращает выполнение задания, как при отмене.


SCIM (автоматическое управление пользователями)

Для интеграции с поставщиком входа (Okta, Microsoft Entra ID и др.):

Действие API
Список пользователей GET /scim/v2/Users
Создать POST /scim/v2/Users
Обновить PATCH /scim/v2/Users/{id}
Удалить DELETE /scim/v2/Users/{id}
Группы GET /scim/v2/Groups

Адрес электронной почты обязателен

Учётная запись заводится по адресу: без него не уйдут уведомления и не сработает восстановление доступа. GitRiver берёт адрес в таком порядке:

  1. emails с признаком primary;
  2. первый элемент emails, если признака нет ни у одного;
  3. userName — но только если он сам является адресом (обычный случай для Okta и Entra ID, где вход настроен по почте).

Если ни одного адреса нет, создание отклоняется с 400 invalidValue и пояснением в поле detail. Настройте в поставщике сопоставление атрибута emails (или назначьте userName равным адресу) — иначе внешнее управление остановится на первом же пользователе. Адрес проверяется по тому же правилу, что и обычная регистрация: имя@домен.зона.

Имя пользователя приводится к правилам GitRiver

Имя пользователя в GitRiver — это ещё и путь: /{owner}/{repo}, каталог на диске, адрес сайта Pages. Поэтому в нём допустимы только латинские буквы, цифры, дефис и подчёркивание, длина 2–39. Присланный userName приводится к этим правилам — остальные символы (@ и . из адреса, кириллица, пробелы) заменяются на _:

userName от поставщика Имя в GitRiver
a_orlova a_orlova
a.orlova@corp.example.com a_orlova_corp_example_com
admin (занято маршрутом) scim_admin

Пустой userName отклоняется с 400 invalidValue: приведение вернуло бы из него один префикс источника, и в GitRiver появился бы владелец scim_.

Если приведённое имя уже занято, добавляется числовой суффикс. Ответ на создание содержит имя, которое действительно сохранено, — сверяйтесь с ним, а не с отправленным. Поиск GET /scim/v2/Users?filter=userName eq "…" понимает обе формы: и исходный userName поставщика, и приведённое имя, поэтому повторной синхронизацией дубль не создаётся. Это работает и внутри составного условия — userName eq "a.orlova@corp.example" and active eq true найдёт приведённое имя так же.

Путь группы выводится из её имени тем же приведением

Группа в GitRiver — это ещё и путь /{группа}, поэтому из присланного displayName выводится путь: буквы (любого алфавита), цифры, дефис и подчёркивание, до 255 символов; остальное заменяется на дефис.

displayName от поставщика Путь в GitRiver
Команда платформы команда-платформы
team.sales team-sales
Admin (занято маршрутом) scim-admin

Приведение — то же самое, что у имени пользователя (см. «Имя пользователя приводится к правилам GitRiver»).

Переименование в поставщике переносит и пространство имён

PATCH с заменой userName (Okta и Entra ID шлют её, когда администратор переименовал сотрудника) переименовывает учётную запись — тем же действием, что и кнопка «Переименовать» в интерфейсе GitRiver.

Что переезжает вместе с именем:

  • адреса репозиториев — /{новое-имя}/{repo}, включая git clone и git push;
  • каталоги репозиториев и вики на диске;
  • упоминания и ссылки, которые строятся по имени владельца.

Прежнее имя ведёт на новое, пока его не займёт кто-то другой, — так же, как после переименования вручную (см. «Переименование пользователя»): клоны, опубликованные сайты Pages и внешние ссылки продолжают работать. GET /api/v1/namespaces/{прежнее-имя} отвечает 301 с адресом нового владельца. Перенастроить клоны (git remote set-url origin <новый адрес>) всё же стоит: как только прежнее имя займут, оно поведёт к новому владельцу. Предупредите сотрудника заранее.

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

Что случилось Ответ
Имя изменилось и свободно 200, в теле — сохранённое имя
Приведённое имя совпадает с текущим 200, ничего не меняется
Имя занято другой учётной записью или группой 409 c scimType: uniqueness
userName пуст или не строка 400 c scimType: invalidValue и userName в detail

Числовой суффикс при переименовании не добавляется: занятое имя — это отказ. Если учётная запись при создании получила имя с суффиксом (a_orlova1), последующий PATCH с userName вернёт 409 — освободите базовое имя или задайте сотруднику в поставщике другой userName.

Переименование выполняется первым среди изменений PATCH: если имя не принято, остальные атрибуты запроса не применяются вовсе.

Имя задаёт поставщик. Пока внешнее управление включено, имя в GitRiver — это приведённый userName поставщика, и сравнивается всегда с ним. Отсюда два следствия:

  • сотрудник, чья учётная запись привязана к поставщику, не может переименовать себя сам: POST /api/v1/users/{username}/rename отвечает 403 и говорит, что имя меняют в консоли поставщика: иначе ближайшая синхронизация отменила бы смену. У администратора GitRiver такого ограничения нет;
  • если имя в GitRiver разошлось с приведённым userName по любой причине (учётная запись заведена вручную, имя получило суффикс при создании, переименована администратором), синхронизация это расхождение снимет.

Расхождение важно и до переименования: поиск учётной записи по фильтру userName eq "…" идёт именно по приведённому имени, и разойдись оно — поставщик не нашёл бы сотрудника и завёл бы дубль.

Что внешнее управление оставляет в журнале аудита

Каждое изменение состава попадает в журнал аудита (GET /api/v1/admin/audit-log), и по содержимому записи видно, откуда оно пришло:

Что случилось op_type Содержимое
Учётная запись создана create_user username, source
Имя изменено rename_user from, to, source
Учётная запись отключена (active: false или DELETE) deactivate_user source
Учётная запись включена обратно activate_user source

Поле source в содержимом различает происхождение события: scim — внешнее управление, registration — самостоятельная регистрация, saml — первый вход через SAML с авторегистрацией, oauth — первый вход через внешнего поставщика OAuth с авторегистрацией, ldap — первый вход через каталог LDAP, setup — мастер первоначальной настройки. Событие create_user пишется при любом из этих способов, в том числе когда первый вход через каталог случился не на странице входа, а при обращении git по HTTP или LFS. Актор у этих событий — сама затронутая учётная запись.

Отключение — не удаление: учётная запись остаётся, но вход ей запрещён и все выданные токены перестают работать. Вместе с ними гаснут разрешения, выданные сторонним приложениям через OAuth и OIDC: обновить по ним токен и прочитать профиль больше нельзя, поэтому вход в такие приложения «через GitRiver» тоже закрывается. Запрет распространяется и на git по SSH: прежний ключ сотрудника не пускает ни во встроенный SSH-сервер, ни в gitriver serv из authorized_keys. Сам ключ остаётся в профиле — удалять его при увольнении сотрудника не нужно. Обратное включение (active: true) снова разрешает вход, и ключ начинает работать без повторного добавления.

Отбор по фильтру: что поддержано

Поставщик ищет учётную запись или группу запросом со списочным фильтром (GET /scim/v2/Users?filter=…). Поддержана грамматика RFC 7644 §3.4.2.2 целиком: операторы eq, ne, co, sw, ew, pr, gt, ge, lt, le, связки and, or, not, скобки, а также подфильтр многозначного атрибута (emails[type eq "work" and value eq "…"]).

Ресурс Атрибуты отбора
Users id, externalId, userName, displayName, emails (и emails.value, emails.type, emails.primary), active, meta.created, meta.lastModified
Groups id, externalId, displayName, members (и members.value), meta.created, meta.lastModified

Правила сравнения:

  • userName, displayName и emails сравниваются без учёта регистра — так эти атрибуты объявлены в RFC 7643 §7. Если в GitRiver почему-либо заведены две учётные записи, отличающиеся только регистром, отбор вернёт обе; это видно по totalResults;
  • externalId сравнивается с учётом регистра: это непрозрачный ключ поставщика, и приведение к одному регистру склеивало бы разные записи;
  • id и members — идентификаторы: к ним применимы только eq и ne. Отбор по значению, которое идентификатором не является, даёт пустую выдачу, а не отказ;
  • active принимает true/false, а также "true", "1", "0" — тем же правилом, каким разбирается active в теле запроса;
  • meta.created и meta.lastModified ведут на одно и то же поле: в ответе GitRiver lastModified равен created, и отбор не расходится с выдачей;
  • значения co, sw, ew ищутся как подстрока: % и _ внутри значения — обычные символы, а не знаки образца.

Неподдержанное — отказ, а не пустая выдача. Неизвестный атрибут, оператор вне RFC, негодный синтаксис, неприменимое сочетание — всё это 400 со scimType: invalidFilter и причиной в detail (например: «отбор пользователя по атрибуту “nickName” не поддержан; поддержаны: …»). Пустой список на такой запрос поставщик прочитал бы как «записи нет» и завёл бы дубль. Пустой filter= — это весь каталог, а не пустой отбор.

Отбор возвращает НАСТОЯЩУЮ страницу: totalResults считает все подходящие записи, startIndex и count листают их, а itemsPerPage — сколько записей отдано в этом ответе (по нему поставщик двигает startIndex).

Отбор на равенство (eq) быстрый и не зависит от размера каталога. Подстрочные операторы (co, sw, ew) на большом каталоге заметно медленнее — на каталоге в десятки тысяч записей держите их для разовых поисков, а не для штатной синхронизации; поставщики её и ведут по eq.

Идентификатор у поставщика можно сменить

externalId учётной записи задаётся при создании и меняется операцией PATCH с путём externalId (или тем же полем в объекте замены без пути). Смена нужна после переноса каталога у поставщика: не приняв её, GitRiver перестал бы находить запись поиском externalId eq "…", и поставщик завёл бы дубль. Смена идентификатора не трогает признак active: отключённая запись отключённой и остаётся. Идентификатор уникален на установку — попытка закрепить один за двумя учётными записями даёт 409 со scimType: uniqueness.

Какие группы отданы под внешнее управление

Внешнее управление управляет только теми группами, которые ему отдали. Остальных групп установки для него не существует: их нет ни в GET /scim/v2/Groups, ни в отборе по фильтру, а точечное обращение к такой группе — 404, как к несуществующей. Это относится и к чтению, и к записи: по коду отказа не видно, какие ещё группы есть в установке.

Отдана внешнему управлению группа бывает двумя способами:

  1. оно само её завело — POST /scim/v2/Groups отдаёт созданную группу внешнему управлению сразу, даже если поставщик не прислал своего externalId;
  2. её отдал администратор GitRiver — в настройках группы, блок «Внешнее управление (SCIM)», кнопка «Отдать под внешнее управление». Тем же блоком группа и возвращается: сама группа и её состав остаются, меняется только то, кто ею управляет.

Отдавая группу, помните про наследование: член группы наследует свою роль во всех её подгруппах. Отдав внешнему управлению группу верхнего уровня, вы отдаёте ему и состав всего поддерева — если это не то, чего вы хотите, отдавайте подгруппы по отдельности.

Токен внешнего управления долгоживущий и хранится в чужой системе, поэтому его действие ограничено учётными записями и теми группами, которые администратор GitRiver отдал ему явно.

Если поставщик пытается завести группу с именем, которое в GitRiver уже занято не отданной ему группой, он получает 409 с объяснением: свяжите существующую группу в GitRiver или задайте группе в поставщике другое имя. Молча присвоить чужую группу он не может.

При обновлении. Отданными считаются группы, у которых записан externalId поставщика, — это все, что заводили Okta и Entra ID со своим идентификатором. Если внешнее управление заводило группы без externalId, после обновления оно перестанет их видеть: отдайте их обратно кнопкой в настройках группы. Проверить просто — сверьте GET /scim/v2/Groups до и после.

Группа помнит свой идентификатор у поставщика

Entra ID заводит группу со своим externalId и в дальнейшем ищет её запросом GET /scim/v2/Groups?filter=externalId eq "…". GitRiver хранит эту привязку и возвращает externalId при чтении группы. Проставить её можно и позже — операцией PATCH с путём externalId (для уже отданной группы) либо сразу при передаче группы внешнему управлению в её настройках. Идентификатор уникален на установку, и попытка закрепить один за двумя группами даёт 409 со scimType: uniqueness.

Отданная группа без идентификатора поставщика — обычное дело: Okta присылает его не всегда. externalId в ответе тогда отсутствует, и это не ошибка.

Владелец группы переживает сверку состава

SCIM не передаёт ролей: весь присланный состав записывается участниками с ролью developer. Поэтому роль «владелец» в группе могла появиться только от администратора GitRiver — например, при создании группы, где создатель становится владельцем.

Полная сверка членства (PATCH с операцией replace по members) заменяет состав ровно на присланный, кроме строк с ролью «владелец»: они остаются. То же правило действует для операции remove — названного владельца она не снимает. Так штатная сверка из Okta или Entra ID не оставляет группу без управления.

Что из этого следует:

  • в ответе на сверку (members) владелец присутствует, даже если поставщик его не присылал, — это не рассогласование, а сохранённая роль;
  • если владелец есть и в присланном составе, он остаётся владельцем, а не понижается до developer;
  • роли «сопровождающий» и ниже сверка снимает наравне с остальными: защищена только роль владельца;
  • чтобы вывести владельца из группы, смените владельца в интерфейсе GitRiver — через поставщика это не делается.

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

Права токена внешнего управления: что он может и чего не может

Токен внешнего управления действует от имени кадровой системы, а не от имени сотрудника, и правами администратора GitRiver не обладает. Его границы:

Что Может ли внешнее управление
Завести и отключить учётную запись, сменить имя, адрес, отображаемое имя да
Завести группу да — заведённая им группа сразу под его управлением
Переименовать группу и задать её состав только у групп, отданных ему
Видеть группу в каталоге и в отборе только отданные ему; остальных для него не существует
Присвоить себе группу, которую ему не отдали нет — её не видно, а имя занято: 409 с объяснением
Удалить группу только отданную и только пустую вместе с подгруппами — иначе отказ 409
Внести участника с ролью выше «разработчик» нет — присланный состав записывается ролью developer, атрибута роли в схеме нет
Снять из группы владельца нет — строки с ролью «владелец» сверка не трогает
Сделать учётную запись администратором установки нет — признака администратора в схеме SCIM нет, и внешнее управление его не пишет
Отключить последнего действующего администратора нет — отказ 400 с причиной в detail

Отключение закрывает вход целиком: пароль, токены, разрешения OAuth и OIDC, доступ по SSH, — а вернуть признак active изнутри продукта нечем, его меняет только само внешнее управление. Чтобы отключить единственного администратора, сначала назначьте администратором вторую учётную запись — тогда отключение первой пройдёт.

Про удаление групп — то же соображение и то же правило, что и в интерфейсе GitRiver: группу с репозиториями не удаляют, причём считаются репозитории не только самой группы, но и всех её подгрупп — удаление родителя сносит их вместе с ним. Учтите: Okta при отвязке выгруженной группы штатно предлагает «удалить группу в приложении», и это её рекомендованный порядок. Удаление группы в GitRiver откатить нечем — репозитории отвязываются от неё и меняют адрес все разом, а групповые переменные сборки, квоты, правила доступа по сетевому адресу и свои роли уходят вместе с ней. Поэтому перенесите репозитории, а потом снимайте группу в поставщике.

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

Отказы видны в консоли поставщика

Пишущие операции (POST, PATCH, PUT, DELETE) не отвечают 200, если изменение не применилось:

Что случилось Ответ
Негодный адрес, негодный идентификатор участника, неразбираемое active 400 c scimType: invalidValue и названием поля в detail
displayName длиннее 255 символов (у группы — ещё и пустой) 400 c scimType: invalidValue и названием поля в detail
Операция не из add / remove / replace 400 c scimType: invalidSyntax
remove без path 400 c scimType: noTarget
Ссылка на несуществующего пользователя в members 400 c перечислением ненайденных идентификаторов
Адрес уже занят другой учётной записью 409 c scimType: uniqueness
Отключение последнего действующего администратора 400 c scimType: invalidValue и причиной в detail
Удаление группы, в которой (или в её подгруппах) есть репозитории 409 с числом репозиториев в detail
Создание группы с именем, занятым не отданной внешнему управлению группой 409 c scimType: uniqueness и объяснением в detail
Любое обращение к группе, которую внешнему управлению не отдали 404
Учётной записи или группы с таким id нет 404
Сбой хранилища 500

PATCH разбирается целиком до первой записи: запрос с негодным значением в любой операции отклоняется, не применив ни одной из остальных (RFC 7644 §3.5.2). Атрибуты, которых GitRiver не хранит (name.givenName, title, phoneNumbers), пропускаются без отказа — поставщик присылает объект целиком.

Не применяются (запись в журнал сервера, ответ при этом успешный): операции remove над атрибутами пользователя — учётная запись не бывает без адреса, без признака активности и без имени входа. Для групп remove работает: и по фильтру пути members[value eq "…"], и списком в значении, и по всему атрибуту members (тогда состав очищается целиком).

Замена userName применяется — см. «Переименование в поставщике переносит и пространство имён».

При обновлении. Замена userName переименовывает учётную запись. Первая же синхронизация после обновления переименует всех, у кого имя в GitRiver расходится с приведённым userName поставщика, — вместе с адресами их репозиториев. Прежде чем включать внешнее управление после обновления, сверьте по GET /scim/v2/Users, чьи имена разойдутся, и предупредите этих сотрудников: прежние адреса работают, пока имена не займут, но клоны лучше перенастроить (git remote set-url).

При обновлении. displayName уже заведённых групп в ответах SCIM меняется с пути группы (komanda-platformy) на её название (Команда платформы). Если в поставщике группы сопоставлены по displayName, а не по id, сверьте сопоставление после обновления.

Учтите разницу между добавлением и заменой состава: add вносит участников, не трогая остальных, а replace атрибута members задаёт состав ровно таким, каким его прислал поставщик, — включая удаление тех, кого поставщик не знает. Единственное исключение — владелец группы: его полная сверка не выносит (см. «Владелец группы переживает сверку состава»).