Доступ к админке и API
Пароль администратора и сессия, два статических токена API с разными правами, закрытый /metrics — и почему ни один из них не является доступом к видео.
Гейт управления закрывает панель управления, весь /api/v1/* и /metrics. Это совершенно
отдельная подсистема от авторизации просмотра: у неё свой блок конфигурации, свои
учётные данные и свои коды ответов.
Ни пароль администратора, ни статические токены API не открывают ни одного медиа-пути. Обратное
тоже верно: токеном зрителя нельзя обратиться к /api/v1. Это разные уровни доступа, и они не
конвертируются друг в друга.
Блок auth { }#
auth { admin_password "длинный-пароль-администратора"; session_secret "общий-секрет-32-и-более-символов"; admin_ip_bind off; api_read_token "случайный-токен-только-на-чтение"; api_write_token "случайный-токен-на-запись"; }
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
admin_password |
строка, непустая | не задан | горячее |
session_secret |
строка, непустая | случайный на загрузку | горячее |
admin_ip_bind |
флаг on / off; голое admin_ip_bind; = on |
off |
горячее |
api_read_token |
строка, ≥ 16 символов | не задан | горячее |
api_write_token |
строка, ≥ 16 символов, отличается от read | не задан | горячее |
Гейт /api/v1 включается, если задана хотя бы одна из трёх учёток — пароль либо любой из
токенов. Настроить один токен и получить открытую панель нельзя.
Если session_secret не задан, он генерируется случайно на каждую загрузку: токены сессий не
переживут перезапуск и не примутся другим узлом.
Что печатается при старте#
Ровно одна строка о состоянии гейта и ровно одна о /metrics. Проверено на стенде:
hastreamer: WARNING admin UI + API are UNAUTHENTICATED — anyone who can reach the HTTP port has full read + control access. Set auth.admin_password (or auth.api_read_token / auth.api_write_token) to close the gate.
hastreamer: /metrics is CLOSED (it was public before 2026.08) — it accepts an admin session bearer only. Set auth.api_read_token to give a scraper its own non-admin credential.
hastreamer: /metrics is CLOSED (it was public before 2026.08) — it accepts `Authorization: Bearer` with auth.api_read_token, auth.api_write_token, an admin session token.
Есть и третий вариант первой строки — когда токены заданы, а пароля нет:
hastreamer: note auth.admin_password is unset — /api/v1 is gated by the static API token(s) alone and the admin UI cannot log in (POST /api/v1/auth/login 404s). Set auth.admin_password to restore the UI login.
Ошибки проверки конфигурации#
auth.api_read_token must not be empty (omit it entirely to leave that tier unconfigured) auth.api_read_token must be at least 16 characters (use a generated random token) auth.api_read_token and auth.api_write_token must differ (an equal pair silently grants write to the read token)
Порог длины существует потому, что перед этим гейтом нет задержки за неудачные попытки — в отличие от входа по паролю. Единственная защита от перебора — энтропия самого токена:
head -c 24 /dev/urandom | base64 | tr -d '=+/'
kQ8mVt3ZpR7bLxNfA2wYuJd0
Ни в журнале, ни в ответе API, ни в тексте ошибки конфигурации. Сервер вообще не хранит токены — только их отпечатки, и сравнивает константным временем.
Сессия администратора#
POST /api/v1/auth/login#
Без аутентификации — единственный маршрут /api/v1, который
никогда не закрыт гейтом.
Меняет пароль на bearer-токен сессии со сроком 12 часов.
TOKEN=$(curl -sS -X POST https://media.example.com/api/v1/auth/login \ -H 'Content-Type: application/json' \ -d '{"password":"длинный-пароль-администратора"}' \ | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])') curl -sS https://media.example.com/api/v1/streams -H "Authorization: Bearer $TOKEN"
| Код | Когда |
|---|---|
200 |
пароль верный, токен выдан |
400 |
в теле нет ключа password либо это не JSON |
401 |
неверный пароль; ответ отдаётся после безусловной задержки 600 мс |
404 |
auth.admin_password не задан — входить некуда |
413 |
тело больше 4 КиБ |
429 |
открыто окно торможения для этого адреса |
admin_ip_bind on дополнительно привязывает выданный токен к адресу, с которого выполнен вход:
токен, украденный из журнала прокси, с другого адреса не сработает. За обратным прокси это требует
корректно заполненного trusted_proxies — иначе привязка закрепится на адресе самого прокси.
WWW-AuthenticateТело отказа — {"error":"unauthorized"}, заголовка WWW-Authenticate нет намеренно: иначе браузер
показал бы поверх панели своё системное окно ввода пароля вместо формы входа.
Статические токены API#
Отдельная учётка для мониторинга и интеграций: не пароль администратора и не полный доступ. Два уровня, где write включает в себя read.
| Уровень | Методы /api/v1/* |
/metrics |
GET /api/v1/config |
POST /api/v1/auth/media-grants |
|---|---|---|---|---|
api_read_token |
только GET, HEAD, OPTIONS |
да | нет — 403 |
нет — 403 |
api_write_token |
все | да | да | нет — 403 |
| сессия администратора | все | да | да | да |
Проверенная матрица:
R='случайный-токен-только-на-чтение' W='случайный-токен-на-запись' B=https://media.example.com curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $R" $B/api/v1/streams curl -s -o /dev/null -w '%{http_code}\n' "$B/api/v1/streams?token=$R" curl -s -H "Authorization: Bearer $R" $B/api/v1/config curl -s -X PATCH -d '{}' -H "Authorization: Bearer $R" $B/api/v1/config/globals curl -s -X POST -d '{"stream":"cam","intent":"live","protocols":["hls"]}' \ -H "Authorization: Bearer $R" $B/api/v1/auth/media-grants curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $W" $B/api/v1/config
200
401
{"error":"forbidden","reason":"read_token_cannot_read_config"}
{"error":"forbidden","reason":"read_token_cannot_write"}
{"error":"forbidden","reason":"api_token_is_not_a_media_credential"}
200
Три правила, о которые спотыкаются#
1. Только заголовок Authorization: Bearer. ?token= для статических токенов не принимается —
никогда и ни на одном маршруте. Запасной ход через строку запроса существует исключительно для
короткоживущего токена сессии и только на GET /api/v1/events, потому что EventSource в браузере
не умеет задавать заголовки. Статический токен живёт вечно и не ротируется при использовании: в URL
он осядет в журналах прокси, в истории браузера и в заголовке Referer.
Проверено ровно на том маршруте, где послабление есть:
curl -s -o /dev/null -w '%{http_code}\n' "$B/api/v1/events?token=$R" curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $R" "$B/api/v1/events"
401 200
2. Уровень чтения не видит GET /api/v1/config. Этот ответ отдаёт текст конфигурации
дословно — вместе с admin_password, api_write_token, секретом auth_backend и учётными данными
камер в URL источников. Прочитать его значило бы за один запрос выйти из режима «только чтение».
Уровню записи чтение оставлено: он и так может переписать этот файл.
Правило применяется к фактической цели запроса, а не к его пути: обращение через
маршрут-перенаправитель GET /api/v1/fleet/self/config даёт тот же 403 с той же причиной
(проверено).
3. Отказ по недостатку прав — 403, а не 401. Учётные данные подлинные, не хватает уровня;
401 заставил бы корректный клиент вечно повторять тот же самый токен. 401 остаётся за «ничего
не предъявлено или не совпало».
auth { } панель управления перезаписывает целикомСохранение вкладки Security и прохождение мастера первого запуска переиздают весь блок
auth { }. Токен, выставленный руками в файле и не введённый в форму, будет удалён вместе с
закрытием гейта. Правьте токены либо только в файле, либо только через панель.
/metrics закрыт теми же учётными данными#
curl -s -o /dev/null -w '%{http_code}\n' https://media.example.com/metrics curl -s -o /dev/null -w '%{http_code}\n' -H "Authorization: Bearer $R" https://media.example.com/metrics
401 200
Экспозиция называет каждый поток, состояние его источника, число зрителей и загрузку хоста — то же
операционное раскрытие, ради которого закрыт /api/v1. Арки «не настроено — значит открыто» здесь
нет: узел без единой учётки отвечает 401 каждому запросу.
Настройте сборщику метрик собственный api_read_token и передавайте его заголовком: ?token= не
принимается и здесь.
/metricsGET /metrics в корне хоста — это экспозиция в формате Prometheus. GET /api/v1/metrics — это
полный JSON-дамп внутреннего состояния, совсем другая поверхность с другим форматом и своим
ограничением на размер. Не перепутайте их в настройках сборщика.
Выпуск медиа-гранта для встроенного плеера#
POST /api/v1/auth/media-grants#
Администратор — только сессия; оба статических токена
получают 403.
Единственный встроенный в продукт способ выпустить медиа-credential. Им пользуется плеер панели управления, чтобы посмотреть закрытый поток, не превращая админ-сессию в доступ к видео.
Тело запроса — не более 4 КиБ, все поля обязательны, лишние отвергаются:
curl -sS -X POST https://media.example.com/api/v1/auth/media-grants \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -d '{"stream":"cam","intent":"live","protocols":["hls"]}'
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6Imhhc3RyZWFtZXItbWVkaWEtZ3JhbnQrand0IiwidiI6MX0…", "expires_at":1787143783,"server_time":1787143483,"ttl_secs":300, "session_expires_at":1787143783}
| Поле | Допустимые значения |
|---|---|
stream |
путь потока; для intent: "vod" обязан начинаться с vod/ |
intent |
live · publish · dvr · vod · preview · export · timeshift |
protocols |
1…8 уникальных значений, без api; live не может включать whip; publish обязан быть ровно ["whip"]; dvr, vod, preview, export, timeshift — только hls |
Срок жизни фиксирован — 300 секунд, изменить его нельзя. Грант привязан к точному ресурсу, к точному набору протоколов и к адресу клиента; подписан он отдельным ключом со своим типом токена, поэтому в клиентский бэкенд авторизации никогда не проваливается.
| Код | Причина |
|---|---|
400 |
invalid_request — лишнее или отсутствующее поле, недопустимое сочетание |
403 |
предъявлен статический токен API (api_token_is_not_a_media_credential) |
409 |
admin_auth_disabled — не задан admin_password; media_auth_disabled — не настроен auth_backend |
413 |
тело больше 4 КиБ |
Пять минут и жёсткая привязка к адресу делают такой грант непригодным для раздачи. Для зрителей выпускайте токены своим кодом — Генераторы токенов.
Аварийный отзыв всех сессий просмотра#
POST /api/v1/auth/rotate-session-keys#
Администратор
Заводит новый ключ подписи билетов. Предыдущий остаётся в ограниченном окне проверки (до трёх), чтобы уже идущие сессии не оборвались мгновенно; билеты под выпавшим ключом перестают работать. Тело запроса не требуется.
curl -sS -X POST https://media.example.com/api/v1/auth/rotate-session-keys \ -H "Authorization: Bearer $TOKEN"
| Код | Причина |
|---|---|
409 |
узел входит в кластер — согласованно меняйте session_keys { secret } вместо ротации; либо на узле нет яруса билетов (не настроен auth_backend) |
Ротация обесценивает билеты, то есть уже установленные сессии. Токен зрителя, у которого не
истёк exp, немедленно установит сессию заново. Чтобы отозвать сами токены, меняйте секрет
подписи auth_backend (для hs256) либо ключ поставщика (для jwks).
Что публично намеренно#
| Поверхность | Почему |
|---|---|
оболочка /admin и её файлы, шрифты |
страница обязана загрузиться, чтобы показать форму входа; данных в ней нет |
/{поток}/embed.html |
тестовый плеер; само видео закрывается авторизацией просмотра |
| вся медиа-выдача | закрывается блоком auth_backend, а не гейтом управления |
Проверка#
curl -s -o /dev/null -w '%{http_code}\n' https://media.example.com/api/v1/streams curl -s -o /dev/null -w '%{http_code}\n' https://media.example.com/metrics curl -s -H "Authorization: Bearer $R" https://media.example.com/api/v1/config
401
401
{"error":"forbidden","reason":"read_token_cannot_read_config"}
Если первый запрос вернул 200 — не задана ни одна из трёх учёток блока auth { }; при старте об
этом было напечатано предупреждение.