SIRINVIDEO CCTV Документация администратора и пользователя На сайт 2026.08

plane.toml — все параметры

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

Файл /etc/hastreamer-cctv/plane.toml — единственный конфигурационный файл контрол-плейна. Его читает бинарь /usr/local/bin/hastreamer-plane, запускаемый юнитом hastreamer-cctv-plane.service.

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

Продуктовые настройки лежат не здесь

Срок хранения аудита, лимиты приёма событий, порог вынесения вложений в блобы, таймаут ONVIF-интервалов и предельная длительность экспорта архива хранятся в таблице settings базы PostgreSQL и правятся из админки. Список — в разделе Чего в этом файле НЕТ. Настройки записи видео (DVR) принадлежат ноде и задаются во Fleet, а не здесь.

Три правила, которые действуют на весь файл#

1. Неизвестный ключ верхнего уровня — фатальная ошибка. Модель конфигурации объявлена с deny_unknown_fields, поэтому опечатка (lissten = …) не игнорируется молча, а останавливает запуск. То же строгое поведение действует внутри таблиц [tls], [event_storage_policy], [keys] и [jwks].

Единственное исключение — [attachment_storage]

Эта таблица разобрана как перечисление с тегом backend, а такой тип не поддерживает deny_unknown_fields. Опечатка внутри неё (regoin = "us-east-1" вместо region) игнорируется молча, и применяется значение по умолчанию. Проверяйте написание ключей в этой таблице особенно внимательно.

2. Файл читается ровно один раз — при старте процесса. Горячей перечитки нет ни у одного параметра. Любая правка вступает в силу только после:

sudo systemctl restart hastreamer-cctv-plane

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

3. Валидация громкая. Любое отвергнутое значение прерывает запуск сообщением в stderr (то есть в журнале systemd):

hastreamer-plane: fatal: invalid plane network configuration: <причина>

Юнит объявлен как Restart=on-failure с RestartSec=3s, поэтому ошибочная правка даёт не «частично работающий» Plane, а цикл перезапусков. Причина всегда в строке fatal::

sudo journalctl -u hastreamer-cctv-plane -n 50 --no-pager | grep -i fatal

Как бинарь читает файл#

hastreamer-plane [<путь-к-конфигу>] [--reset-superadmin]

Первый аргумент без дефисов — путь к конфигу; если его не передать, берётся plane.toml в текущем каталоге. Штатный юнит всегда передаёт /etc/hastreamer-cctv/plane.toml явно. Флаг --reset-superadmin выпускает и печатает новый пароль суперадминистратора, записывает это в аудит и завершает процесс — это разовая команда восстановления доступа, а не режим работы.

Что меняется из админки, а что только руками#

Суперадминистратор может переписать часть этого файла из админки: Настройки → параметры Plane (PATCH /api/v1/settings/plane). Разделение жёсткое:

Правится из админки Только правкой файла
listen, public_url, log_level database_url
вся таблица [tls] db_pool_max
вся таблица [attachment_storage] [keys] session_key_pem
вся таблица [event_storage_policy] [keys] tunnel_control_key_pem
[jwks] session_ttl_secs

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

Сохранение из админки стирает комментарии внутри трёх таблиц

Запись файла атомарна (временный файл рядом → fsyncrename) и сохраняет режим доступа (записанный установщиком 0640 останется 0640). Байты и комментарии вне затронутых ключей сохраняются дословно.

Но каждая переписываемая таблица сначала очищается целиком. Сохранение сетевых настроек стирает и заново формирует всю таблицу [tls]; сохранение настроек хранилища — всю [attachment_storage] и всю [event_storage_policy]. Любой ваш комментарий внутри этих трёх таблиц будет уничтожен без предупреждения.

Правило: пишите свои комментарии над заголовком таблицы или рядом с параметрами верхнего уровня, но не внутри [tls], [attachment_storage] и [event_storage_policy].

Ещё три особенности записи из админки, о которых полезно знать:

  • Неприменимые ключи выбрасываются. Таблица формируется заново из типизированной модели, поэтому cert и key остаются только при mode = "pem", hosts — только если список непустой, store_dir — только если задан явно, а три ключа acme_* — только при mode = "acme".
  • Файл проверяется дважды — сначала в памяти, затем повторным разбором ровно тех байтов, которые будут записаны. Через админку нельзя оставить незапускаемый конфиг.
  • Защита от одновременной правки. Если файл изменился после того, как форма настроек была загружена, сохранение отклоняется с сообщением "/etc/hastreamer-cctv/plane.toml changed after the settings form was loaded" — ваша ручная правка не будет затёрта молча, админка попросит перезагрузить форму. Одновременные сохранения из админки выстраиваются в очередь на внутренней блокировке и через 10 секунд ожидания возвращают 409 settings_busy.
Порядок совместной работы

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

Полный перечень параметров#

# Параметр Тип По умолчанию Обязателен Из UI
1 listen адрес сокета "127.0.0.1:8080" нет да
2 database_url строка да нет
3 log_level перечисление "info" нет да
4 db_pool_max целое ≥ 5 16 нет нет
5 public_url origin-URL да да
6 [attachment_storage] backend "local" или "s3" "local" если таблица есть да
7 [attachment_storage] dir абсолютный путь "/var/lib/hastreamer-cctv/attachments" нет да
8 [attachment_storage] endpoint host[:port] для s3 да
9 [attachment_storage] bucket строка для s3 да
10 [attachment_storage] region строка "us-east-1" нет да
11 [attachment_storage] https логический true нет да
12 [attachment_storage] access_key строка "" для s3 да, только запись
13 [attachment_storage] secret_key строка "" для s3 да, только запись
14 [attachment_storage] addressing "path" или "virtual_host" "path" нет да
15 [event_storage_policy] max_bytes целое, байты не задан для s3 да
16 [event_storage_policy] volume_retention перечисление "disabled" нет да
17 [event_storage_policy] low_watermark_percent 1–97 75 нет да
18 [event_storage_policy] high_watermark_percent целое 85 нет да
19 [event_storage_policy] critical_watermark_percent ≤ 99 95 нет да
20 [keys] session_key_pem путь <каталог конфига>/signing.pem нет нет
21 [keys] tunnel_control_key_pem путь <каталог конфига>/tunnel-control.pem нет нет
22 [jwks] session_ttl_secs целое, секунды 3600 нет нет
23 [tls] mode перечисление из 5 значений "off" нет да
24 [tls] cert путь не задан для pem да
25 [tls] key путь не задан для pem да
26 [tls] hosts массив строк [] для self_signed и acme да
27 [tls] store_dir путь <каталог конфига>/plane-tls нет да
28 [tls] acme_directory URL https:// "https://acme-v02.api.letsencrypt.org/directory" нет да
29 [tls] acme_contact строка, e-mail "" нет да
30 [tls] acme_listen адрес сокета "0.0.0.0:80" нет да

Обязательных без значения по умолчанию — ровно два: database_url и public_url. Отсутствие любого из них — ошибка разбора файла, Plane не стартует вообще. Остальные «обязательные» в таблице обязательны условно: только если выбран соответствующий режим хранилища или TLS.

Параметры верхнего уровня#

listen#

Параметр Тип и допустимые значения По умолчанию Применение
listen Строка IP:порт. IPv4 (0.0.0.0:8080, 127.0.0.1:8080) или IPv6 в квадратных скобках ([::]:8443). Порт 1–65535 "127.0.0.1:8080" рестарт

TCP-сокет, который Plane открывает для собственного /api/v1, админки, мобильного интерфейса и плеера. Это адрес привязки, а не адрес, который набирают пользователи — последний задаётся в public_url. Процесс работает от root, поэтому привилегированные порты 80 и 443 доступны.

Имя хоста не принимается — только IP-адрес. Иначе запуск прерывается:

invalid plane network configuration: listen must be an IP socket address (hostnames are not bind addresses)
Значение по умолчанию — не то, что пишет установщик

Если удалить строку listen, действует зашитое значение 127.0.0.1:8080только петлевой интерфейс. Установщик и пример конфигурации пишут 0.0.0.0:<порт>, поэтому удаление строки не «вернёт как было», а закроет Plane для всех, кроме локального хоста.

В режиме acme действует дополнительное правило: listen обязан отличаться от [tls] acme_listen — см. acme_listen.

Значение не является секретом. Привязка к 0.0.0.0 открывает админку на всех интерфейсах; если TLS терминирует реверс-прокси на том же хосте, привязывайтесь к 127.0.0.1.

database_url — обязателен, значения по умолчанию нет#

Параметр Тип и допустимые значения По умолчанию Применение
database_url Строка подключения PostgreSQL: postgres://пользователь:пароль@хост:порт/база, допустимы параметры вида ?sslmode=require нет — параметр обязателен рестарт

Строка подключения к PostgreSQL. Plane владеет этой схемой: при старте применяет свои миграции, создаёт организацию по умолчанию, суперадминистратора и системные шаблоны, а дальше хранит там все камеры, ноды, пользователей, гранты, события, записи аудита и продуктовые настройки. Подключения помечаются как application_name=hastreamer-plane.

Синтаксис строки при разборе конфигурации не проверяется — она передаётся драйверу как есть. Ошибочный или недоступный адрес фатален при старте и, с учётом Restart=on-failure, даёт цикл перезапусков с интервалом 3 секунды, пока PostgreSQL не станет доступен. Если ключ вообще отсутствует, файл не разбирается:

parsing /etc/hastreamer-cctv/plane.toml: missing field `database_url`

Сколько подключений планировать. Пул ограничен параметром db_pool_max. Дополнительно Plane держит одно отдельное подключение вне пула для своей аренды-одиночки (application_name=hastreamer-plane-singleton) — это рекомендательная блокировка, гарантирующая ровно один процесс Plane на одну базу. Закладывайте db_pool_max + 1 подключений PostgreSQL на каждый Plane. Потеря этой аренды намеренно завершает процесс.

Самая чувствительная строка в файле

database_url содержит пароль базы открытым текстом. Именно из-за неё файл должен быть 0640 root:root: его нельзя делать доступным для чтения всем, вставлять в заявку в техподдержку или коммитить в git. Смена пароля базы — это правка этой строки плюс перезапуск сервиса. Через API этот параметр не читается и не записывается никогда.

log_level#

Параметр Тип и допустимые значения По умолчанию Применение
log_level Ровно одно из "debug", "info", "warn", "error" (строчными буквами) "info" горячее из админки / рестарт при правке файла

Минимальный уровень сообщений, попадающих в stderr (то есть в журнал systemd) и в ограниченное по объёму кольцо оперативного журнала в памяти, которое показывает админка. Кольцо вычищает учётные данные из сообщений.

Это закрытое перечисление, а не выражение RUST_LOG. Значения "trace", "verbose" или "cctv_plane=debug" отвергаются при разборе файла:

parsing /etc/hastreamer-cctv/plane.toml: unknown variant `verbose`, expected one of `debug`, `info`, `warn`, `error`

Пофильтровать журнал по конкретным подсистемам можно только переменной окружения RUST_LOG — см. Переменные окружения. Пока RUST_LOG задана и непуста, она полностью замещает log_level и блокирует соответствующий элемент управления в админке: попытка сохранить уровень журнала вернёт 409 rust_log_override.

Это единственный параметр, изменение которого из админки применяется немедленно, без перезапуска. Уровень debug безопасно включить ненадолго для диагностики (учётные данные вычищаются), но он многословен — не оставляйте его включённым постоянно.

db_pool_max#

Параметр Тип и допустимые значения По умолчанию Применение
db_pool_max Целое без знака, минимум 5. Верхнего предела в Plane нет 16 рестарт

Максимальное число подключений PostgreSQL в пуле. Оно ограничивает параллелизм всех API-запросов, транзакций приёма событий и фоновых задач.

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

invalid plane network configuration: db_pool_max must be at least 5 so short event-ingest transactions leave four database connections available

Реальный потолок сверху задаёт max_connections самого PostgreSQL; не забудьте про +1 подключение аренды-одиночки. Сопутствующие тайм-ауты пула зашиты и параметрами не настраиваются: ожидание подключения — 10 секунд, простой — 2 минуты, предельное время жизни подключения — 30 минут.

Из админки не меняется — только правкой файла.

public_url — обязателен, значения по умолчанию нет#

Параметр Тип и допустимые значения По умолчанию Применение
public_url Только origin: схема (http или https) + хост + необязательный порт. Без учётных данных, пути, параметров запроса и якоря. Допустимы и DNS-имя, и IP-литерал (кроме режима acme) нет — параметр обязателен рестарт

Канонический внешний адрес Plane — то, по чему до него достучатся браузеры и ноды. Из него собираются ссылки, перенаправления, публичные ссылки на фрагменты и адрес обратного вызова, который сообщается нодам. Он законно может отличаться от listen: реверс-прокси, NAT, проброс портов контейнера, внешний терминатор TLS. Завершающий / нормализуется при старте.

Ошибочное значение прерывает запуск одним из двух сообщений:

invalid plane network configuration: public_url must be an HTTP(S) origin
invalid plane network configuration: public_url must be an HTTP(S) origin without credentials, path, query or fragment

Отсутствие ключа — такая же ошибка разбора, как и у database_url:

parsing /etc/hastreamer-cctv/plane.toml: missing field `public_url`

Перекрёстная проверка с [tls] mode — самая частая причина невзлетевшего Plane:

tls.mode Требуемая схема public_url Сообщение при несовпадении
off http:// tls.mode=off requires an http:// public_url
external, pem, self_signed, acme https:// tls.mode=<режим> requires an https:// public_url

Дополнительно для режимов self_signed и acme хост из public_url обязан присутствовать в списке tls.hosts, иначе:

invalid plane network configuration: tls.hosts must include the public_url hostname "cctv.example.com"

Сравнение регистронезависимое, скобки IPv6 снимаются перед сравнением.

Ошибка в public_url не ломает собственный слушатель Plane — сервис поднимется и будет отвечать. Ломаются обратные вызовы нод, публичные ссылки и перенаправления в браузере, поэтому симптом выглядит как «всё запустилось, но ничего не работает».

Хранилище материалов событий#

Таблицы [attachment_storage] и [event_storage_policy] настраивают материалы событий, принадлежащие Plane: кадры по детекции движения и ONVIF, загруженные вложения к событиям и маленькие генерируемые превью (<ключ>.preview).

Видеоархив здесь не настраивается

Записи DVR принадлежат ноде и настраиваются на каждой ноде отдельно в разделе Fleet. Ни один параметр plane.toml не влияет на хранилище видеоархива.

Обе таблицы можно не писать вообще: по умолчанию используется локальный каталог /var/lib/hastreamer-cctv/attachments, стандартные пороги заполнения и отсутствие байтового бюджета.

[attachment_storage] backend#

Параметр Тип и допустимые значения По умолчанию Применение
backend "local" или "s3" "local" (вся таблица необязательна) рестарт

Это ключ-тег перечисления: если таблица присутствует, backend в ней обязателен. Неизвестное значение — ошибка разбора:

parsing /etc/hastreamer-cctv/plane.toml: unknown variant `obj-store`, expected `local` or `s3`

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

[attachment_storage] dir — локальный бэкенд#

Параметр Тип и допустимые значения По умолчанию Применение
dir Абсолютный путь, выделенный под эту задачу. Не /, без компонентов . и .., без пробелов по краям "/var/lib/hastreamer-cctv/attachments" рестарт

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

Недопустимый путь прерывает запуск:

invalid plane network configuration: local event storage directory must be a dedicated absolute path below the filesystem root

Содержимое каталога — доказательная база по событиям; установщик создаёт его с правами 0750 root:root.

[attachment_storage] — бэкенд S3#

[attachment_storage]
backend     = "s3"
endpoint    = "s3.example.com"       # без схемы: host[:port]
bucket      = "cctv-evidence"
region      = "us-east-1"
https       = true
access_key  = "ЗАМЕНИТЕ"
secret_key  = "ЗАМЕНИТЕ"
addressing  = "path"                 # или "virtual_host"
Параметр Тип и допустимые значения По умолчанию Применение
endpoint host[:port] без схемы: без префикса https://, без пути, учётных данных, параметров запроса и якоря. IP-литерал допустим только при addressing = "path" — (обязателен) рестарт
bucket Непустая строка до 255 символов, без пробелов и без разделителей / и \ — (обязателен) рестарт
region Непустая строка, используется при подписи запросов SigV4 "us-east-1" рестарт
https Логический. Выбирает схему обращения к endpoint. false — открытый HTTP, допустим только в изолированной локальной сети true рестарт
access_key Непустая строка. Секрет "" (пустая отвергается) рестарт
secret_key Непустая строка. Секрет "" (пустая отвергается) рестарт
addressing "path" — адрес вида хост/бакет/ключ; "virtual_host" — адрес вида бакет.хост/ключ "path" рестарт

Все проверки фатальны при старте и возвращаются кодом 400 при сохранении из админки:

S3 event storage endpoint must be schemeless host[:port]
invalid S3 event storage endpoint
S3 event storage endpoint must be schemeless host[:port] without credentials or path
S3 event storage bucket must be a non-empty bucket name without path separators
S3 event storage region must not be empty
S3 event storage credentials must not be empty
virtual-host S3 addressing requires a DNS hostname endpoint and a DNS-compatible bucket name

Последняя проверка требует одновременно DNS-имени в endpoint (не IP) и совместимого с DNS имени бакета: от 3 до 63 символов, только строчные буквы, цифры, - и ., каждая метка между точками начинается и заканчивается буквой или цифрой, и само имя не является IP-адресом.

У https и addressing отдельных сообщений нет: неверный тип (https = "true" вместо https = true) или неизвестное значение addressing останавливают уже разбор файла — parsing /etc/hastreamer-cctv/plane.toml: <ошибка toml>. Помните также, что опечатка в имени ключа именно в этой таблице не диагностируется вообще, а молча заменяется значением по умолчанию.

Для S3 обязателен байтовый бюджет

У объектного хранилища нет переносимого способа спросить «сколько осталось места», поэтому вместе с backend = "s3" обязательно задавать [event_storage_policy] max_bytes. Иначе:

invalid plane network configuration: S3 event storage requires an explicit max_bytes budget

Отдельного переключателя проверки сертификатов (tls_verify и подобных) у этого бэкенда нет — единственный транспортный переключатель — https.

Ключи доступа лежат в файле открытым текстом

access_key и secret_key — вторая причина, по которой файл должен быть 0640 root:root. API устроен намеренно асимметрично: записать пару можно через настройки суперадминистратора, но прочитать её обратно нельзя никогда — наружу отдаются только признаки "access_key_configured": true/false и "secret_key_configured": true/false, и сами значения не попадают в запись аудита.

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

S3 access and secret keys must be supplied together
S3 event storage credentials are required when selecting the backend

Выдавайте этим ключам доступ строго к одному бакету.

Перенос хранилища#

Прочитайте этот раздел перед тем, как менять dir или backend на работающей установке.

  • Запуск не ломается никогда. Настроенный бэкенд применяется при старте всегда; смена каталога не приводит к циклу перезапусков. Если в прежнем хранилище остались блобы, которых новое не видит, Plane пишет об этом в журнал и поднимает постоянное оповещение — «материалы событий лежат в ПРЕДЫДУЩЕМ каталоге, они не потеряны, верните прежний каталог в plane.toml, чтобы восстановить доступ», — продолжая работать с новым каталогом. В журнале это сообщение вида:

    event-storage evidence in bucket … is in the PREVIOUS directory … It is not lost — revert the directory in plane.toml to recover it
    

    Возврат прежнего значения восстанавливает доступ. Пустое прежнее хранилище не помечается.

  • Через админку правило строже, чем при правке файла. Смена каталога или бэкенда из админки отклоняется с 409 event_storage_rotation_blocked, пока хоть в одном незакрытом хранилище ещё лежат физические блобы. Дополнительно выполняется живая проверка нового бэкенда: если он недоступен или недоступен на запись, возвращается 502 с сообщением самого бэкенда.

  • Переезжают только материалы событий Plane. Видеоархив нод это не затрагивает.

[event_storage_policy] — политика заполнения#

Параметр Тип и допустимые значения По умолчанию Применение
max_bytes Целое, байты. Не менее 64 МиБ; должно помещаться в тип bigint PostgreSQL не задан (для локального бэкенда — ёмкость файловой системы) рестарт
volume_retention "disabled" или "oldest_purge_day" "disabled" рестарт
low_watermark_percent Целое 1–97. Ниже этого порога давление снимается 75 рестарт
high_watermark_percent Целое. На этом пороге и выше давление включается 85 рестарт
critical_watermark_percent Целое ≤ 99. Порог жёсткого давления 95 рестарт

max_bytes — это учётный потолок для материалов событий, а не квота организации. Для локального бэкенда его можно не задавать: ёмкость берётся из файловой системы. Для S3 он обязателен.

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

event storage watermarks must satisfy 1 <= low < high < critical <= 99
event storage max_bytes must be at least 64 MiB
event storage max_bytes must fit the PostgreSQL bigint accounting domain
S3 event storage requires an explicit max_bytes budget
volume_retention = "oldest_purge_day" удаляет историю

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

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

Байтовый бюджет сам по себе никогда не даёт права на удаление — удаляет только эта настройка.

Ключи подписи и время жизни сессии#

[keys] session_key_pem#

Параметр Тип и допустимые значения По умолчанию Применение
session_key_pem Абсолютный путь к приватному ключу ES256 (P-256) в формате PKCS#8 PEM <каталог plane.toml>/signing.pem, то есть /etc/hastreamer-cctv/signing.pem рестарт

Ключ, которым подписываются JWT сессий браузера и API. Его открытая половина публикуется по адресу /.well-known/jwks.json, и ноды проверяют билеты сессий по ней автономно — именно поэтому недоступность Plane не прерывает уже идущее видео. Обратная сторона: замена этого ключа обесценивает все выданные сессии и заставляет ноды обновить закешированный JWKS.

Установщик записывает ключ явно в plane-session.pem, поэтому имя по умолчанию signing.pem встречается только в конфигурациях, написанных вручную.

Поведение при старте:

Состояние файла Что делает Plane
Файла нет Генерирует новый ключ ES256, сохраняет его с правами 0600, создавая при необходимости родительские каталоги, и пишет в журнал generated a new ES256 plane session key (0600)
Файл повреждён или это не ES256 Прерывает запуск — молча подменять личность недопустимо
Права шире, чем 0600 Прерывает запуск: private key /etc/hastreamer-cctv/plane-session.pem must have mode 0600 (проверка — mode & 0o077 != 0)
Потерянный ключ сессий — это не ошибка запуска, а новая личность

Если файл ключа пропал, Plane не остановится: он сгенерирует новый. Все пользователи будут разлогинены, а закешированный нодами JWKS станет недействительным. Ключ обязателен к резервному копированию — см. Что обязательно бэкапить.

Файл-спутник. Рядом с ключом Plane ведёт эпоху идентификатора ключа: тот же путь с расширением, заменённым на .kid (/etc/hastreamer-cctv/plane-session.kid). В нём целое число (1 при первом запуске), а публикуемый идентификатор имеет вид k<эпоха>. Копируйте его вместе с ключом.

Ротация — это действие API, а не параметр конфигурации. Запрос POST /api/v1/admin/keys/rotate (суперадминистратор) либо увеличивает эпоху — старый идентификатор исчезает из публикуемого JWKS, и выданные ранее токены перестают проходить выбор ключа, — либо, в форме восстановления после компрометации, заменяет сам ключевой материал (новая пара ключей с правами 0600 плюс увеличение эпохи). Ключи управления нодами при этом не затрагиваются.

Через API этот параметр не редактируется: приватный ключевой материал не ездит по настройкам. Файл обязан быть 0600 root:root — тот, кто может его прочитать, может выпустить действительную сессию от имени любого пользователя.

[keys] tunnel_control_key_pem#

Параметр Тип и допустимые значения По умолчанию Применение
tunnel_control_key_pem Абсолютный путь к приватному ключу ES256 <каталог plane.toml>/tunnel-control.pem рестарт

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

Правила старта те же, что и у ключа сессий: нет файла — сгенерируется, повреждён — запуск прерывается, права шире 0600 — запуск прерывается.

Второй файл-спутник обязателен к резервному копированию

Рядом Plane создаёт 32-байтный корень выдачи туннелей: тот же путь с расширением, заменённым на .provisioning.key (/etc/hastreamer-cctv/tunnel-control.provisioning.key, сырые байты, права 0600). Из этого корня выводится каждая конфигурация ретранслятора и агента, которую выдаёт Plane. При его потере уже развёрнутые туннели продолжат работать, но их конфигурации больше нельзя воспроизвести: часть конфигураций невозможно вывести из текущего корня выдачи, и такие экземпляры остаются изолированными. В журнале это предупреждение вида:

some tunnel configs cannot be reproduced with the current provisioning root; affected instances remain isolated
Устаревшие имена ключей теперь отвергаются

[keys] signing_key_pem и [keys] node_bearer_key_pem больше не существуют, и из-за deny_unknown_fields в таблице [keys] они прерывают разбор файла. При переносе старой конфигурации переименуйте signing_key_pem в session_key_pem, а node_bearer_key_pem просто удалите: подписанты управления для каждой ноды теперь хранятся в её собственной строке базы данных.

[jwks] session_ttl_secs#

Параметр Тип и допустимые значения По умолчанию Применение
session_ttl_secs Целое без знака, секунды. Проверки диапазона нет 3600 (1 час) рестарт

Срок жизни выданного JWT сессии — фактически то, сколько времени вход остаётся действительным до повторной аутентификации.

Значение не проверяется — выбирайте осознанно

Разбор принимает 0 (каждый выданный токен окажется просроченным немедленно) и сколь угодно большие значения. Типичный выбор: 3600 — один час; 28800 — восемь часов для дежурной смены.

Индивидуального отзыва сессий на сервере нет: список выданных токенов не хранится. Если нужно сократить окно для украденного токена, есть два рычага — уменьшить это значение либо провести ротацию ключа сессий (POST /api/v1/admin/keys/rotate). Из админки параметр не меняется.

TLS собственного слушателя Plane#

[tls] mode — пять режимов доступа#

Параметр Тип и допустимые значения По умолчанию Применение
mode "off", "external", "pem", "self_signed", "acme" "off" (в том числе если таблицы [tls] нет вовсе) рестарт
Значение Слушатель Схема public_url Ещё требуется Когда применять
"off" открытый HTTP http:// внутренняя сеть, TLS не используется нигде
"external" открытый HTTP https:// TLS терминирует реверс-прокси или балансировщик впереди
"pem" HTTPS силами Plane https:// cert, key у вас есть собственный сертификат: корпоративного УЦ, wildcard или купленный
"self_signed" HTTPS силами Plane https:// hosts HTTPS без удостоверяющего центра; браузер будет предупреждать
"acme" HTTPS силами Plane https:// hosts, желательно acme_contact публичный домен и автоматические сертификаты Let's Encrypt
Режим external есть в системе, но его нет в примере конфигурации

Поставляемый файл-пример упоминает четыре режима, однако external — полноценный поддерживаемый режим, доступный в том числе в админке. В нём Plane отдаёт открытый HTTP на listen, но объявляет https:// в public_url, поэтому все генерируемые ссылки, перенаправления и обратные вызовы нод получаются HTTPS, а шифрует трафик прокси впереди.

За терминирующим TLS прокси используйте именно external, а не off: режим off откажется принимать https:// в public_url с сообщением tls.mode=off requires an http:// public_url.

Неизвестное значение — ошибка разбора файла. Несовпадение схемы public_url разобрано в public_url.

[tls] cert и [tls] key — режим pem#

Параметр Тип и допустимые значения По умолчанию Применение
cert Путь к цепочке сертификатов в PEM: сначала конечный сертификат, затем промежуточные не задан рестарт
key Путь к приватному ключу этой цепочки в PEM не задан рестарт
[tls]
mode = "pem"
cert = "/etc/hastreamer-cctv/plane-cert.pem"
key  = "/etc/hastreamer-cctv/plane-key.pem"

Plane читает оба файла один раз при старте, каждый — с ограничением 1 МиБ. При mode = "pem" оба ключа обязательны и не должны быть пустыми:

invalid plane network configuration: tls.mode=pem requires cert and key paths

Отсутствующий файл или несоответствие сертификата ключу — фатальная ошибка старта. Параметр hosts в этом режиме не требуется и не проверяется: имена берутся из SAN вашего сертификата.

Продление сертификата в режиме pem — ваша задача

Никакой логики обновления в этом режиме нет. Заменили файлы — перезапустите сервис. Права на файл ключа Plane здесь не проверяет (проверка 0600 касается только таблицы [keys]), поэтому выставьте их сами:

sudo chown root:root /etc/hastreamer-cctv/plane-key.pem
sudo chmod 0600 /etc/hastreamer-cctv/plane-key.pem

Из админки редактируются только пути, но не содержимое файлов.

[tls] hosts#

Параметр Тип и допустимые значения По умолчанию Применение
hosts Массив строк. DNS-имя: до 253 символов, каждая метка до 63 символов, только буквы, цифры и -, без - в начале и конце. Для self_signed допустимы IP-литералы, для acme — нет. Маски вида *.example.com отвергаются в обоих режимах [] рестарт

Набор имён, под которыми Plane выпускает сертификат: SAN для self_signed и список доменов, запрашиваемых у удостоверяющего центра, для acme.

hosts = ["cctv.example.com", "cctv-alt.example.com"]

Проверки действуют только в режимах self_signed и acme:

tls.mode=self_signed requires at least one hostname
tls.hosts must include the public_url hostname "cctv.example.com"
invalid TLS DNS hostname "cctv_example.com"
ACME HTTP-01 requires DNS hostnames, not IP literals

В режиме self_signed сохранённая личность, набор SAN которой перестал соответствовать hosts, перевыпускается при следующем запуске. Так же перевыпускается сертификат, оставшийся от предыдущей работы в режиме acme. В админке список задаётся через запятую.

[tls] store_dir#

Параметр Тип и допустимые значения По умолчанию Применение
store_dir Путь к каталогу, куда Plane складывает выпущенный им TLS-материал <каталог plane.toml>/plane-tls, то есть /etc/hastreamer-cctv/plane-tls рестарт

Каталог используется только в режимах self_signed и acme, в off, external и pem он не задействован. Внутри:

  • identity.pem — обслуживаемая сейчас пара «сертификат + приватный ключ», права 0600;
  • account.pem — ключ учётной записи ACME, только в режиме acme, права 0600.

Каталог создаётся при отсутствии и принудительно приводится к правам 0700. Он содержит живой приватный ключ TLS — не делайте его доступным для чтения всем.

[tls] acme_listen#

Параметр Тип и допустимые значения По умолчанию Применение
acme_listen Адрес сокета IP:порт, по тем же правилам, что и listen. Обязан отличаться от listen "0.0.0.0:80" рестарт

Отдельный нешифрованный слушатель, единственная задача которого — отвечать на проверку ACME HTTP-01 по адресу /.well-known/acme-challenge/<токен>. Удостоверяющий центр обращается за этим URL строго по порту 80: для HTTP-01 это не опция, и именно поэтому Plane работает от root.

tls.acme_listen must be an IP socket address
tls.acme_listen and the main listen address must be different
Порт 80 должен быть свободен и доступен снаружи

На порту 80 не должно быть другого веб-сервера, а межсетевой экран и NAT обязаны пропускать к нему запросы из интернета — в том числе после установки, при каждом продлении сертификата. Слушатель проверок ограничен: одновременно обслуживается не более 64 токенов, и он останавливается вместе с Plane.

[tls] acme_directory#

Параметр Тип и допустимые значения По умолчанию Применение
acme_directory URL, обязан начинаться с https://. Единственное исключение — http://127.0.0.1… для локальных тестовых центров "https://acme-v02.api.letsencrypt.org/directory" рестарт

Адрес каталога ACME, в котором Plane регистрируется и заказывает сертификаты.

ACME directory must use HTTPS (loopback HTTP is test-only)
Тестовый прогон — на staging-каталоге

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

[tls] acme_contact#

Параметр Тип и допустимые значения По умолчанию Применение
acme_contact Строка с адресом электронной почты. Формат самим Plane не проверяется "" рестарт

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

Что в режиме ACME не настраивается#

Эти величины зашиты в код, параметров для них нет:

Поведение Значение
Обновлять сертификат, если до истечения осталось менее 30 суток
Периодичность проверки необходимости обновления каждые 12 часов
Пауза после неудачной попытки обновления 1 час
Минимальная пауза, пока обслуживается временный сертификат 2 минуты
Тайм-аут рукопожатия TLS 10 секунд
Одновременных рукопожатий 256
Одновременных проверок ACME 64
Предельный размер чтения identity.pem и account.pem 1 МиБ и 128 КиБ
Plane не падает из-за неудавшегося выпуска сертификата

Если в режиме acme сертификат не удалось получить при старте (домен не резолвится, порт 80 закрыт, центр недоступен), Plane всё равно поднимется: он отдаст временный самоподписанный сертификат из памяти (браузер покажет предупреждение) и продолжит попытки. Админка отражает это признаком temporary_self_signed: true. Временный сертификат никогда не сохраняется в identity.pem, поэтому в режиме acme в этом файле лежит только настоящий сертификат от удостоверяющего центра.

Чего в этом файле НЕТ#

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

Что обычно ищут Как обстоит дело
Секрет подписи сессий Такого ключа нет. Сессии подписываются файлом ключа ES256 из [keys] session_key_pem
Расписание ротации ключа Такого ключа нет. Ротация — действие суперадминистратора через API: POST /api/v1/admin/keys/rotate
Ограничение числа сессий или одновременных входов Не реализовано. Единственное ограничение сессии — [jwks] session_ttl_secs
Политика паролей: длина, сложность, срок, блокировка Параметров нет. Чистая база создаёт суперадминистратора admin с паролем SirinVideoсмените его при первом входе. Восстановление доступа: sudo hastreamer-plane /etc/hastreamer-cctv/plane.toml --reset-superadmin
Список доверенных прокси или доверенных IP Такого ключа нет — см. предупреждение ниже
CORS и список разрешённых источников Такого ключа нет, и слоя CORS тоже нет: API по построению работает в пределах одного источника
Тайм-ауты запроса, чтения, записи, keep-alive Параметров нет. Зашиты: ожидание подключения к базе 10 с, простой подключения 2 мин, предельное время жизни подключения 30 мин, мягкое завершение HTTP 5 с, блокировка записи настроек 10 с, рукопожатие TLS 10 с, исходящее подключение 5 с
Точка сбора метрик Prometheus (/metrics) Не публикуется. Метрики собираются внутри и отдаются через аутентифицированный API и админку, а не через открытый порт для сбора
Срок хранения событий, обслуживание секций базы Не здесь. Обслуживание секций выполняется одним проходом при старте, дальше ежечасно, и это не настраивается. Срок хранения — продуктовая настройка в базе
Путь к файлу журнала, выбор приёмников журнала Параметров нет. Журнал уходит в stderr и далее в journald (journalctl -u hastreamer-cctv-plane), плюс ограниченное кольцо в памяти для админки. Фильтры по подсистемам — только переменная RUST_LOG
Число рабочих потоков или ядер Такого ключа нет. Ограничивайте процесс средствами юнита: MemoryHigh, MemoryMax, CPUQuota, TasksMax
Настройки DVR, записи и медиа Принадлежат ноде и задаются во Fleet
Первый заголовок X-Forwarded-For принимается безусловно

Plane берёт первый переход из заголовка X-Forwarded-For как IP-адрес клиента для записей аудита и для ограничения частоты запросов. Списка доверенных прокси не существует, и проверить этот заголовок нечем.

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

  • ставьте Plane только за тот прокси, которым управляете сами;
  • настройте прокси так, чтобы он перезаписывал X-Forwarded-For, а не дописывал в него;
  • привязывайте listen к 127.0.0.1, если прокси работает на том же хосте, чтобы напрямую Plane не отвечал никому.

Аутентификация ретрансляторов намеренно игнорирует X-Forwarded-For и использует адрес сокета-собеседника, поэтому на неё подделка заголовка не влияет.

Продуктовые настройки, которые живут в базе#

Эти значения создаются при первом запуске, правятся из админки и в plane.toml не выносятся:

Настройка Значение по умолчанию
audit.retention_days — срок хранения аудита 365 суток
ingest.rate_default — частота приёма событий на один токен 100 событий в секунду
blob.threshold_kib — порог вынесения вложения в блоб 16 КиБ
undeclared_blob.policy — что делать с необъявленным вложением store
onvif.interval_timeout_secs — тайм-аут ONVIF-интервала 10 секунд
dvr_export_max_secs — предельная длительность экспорта архива задаётся через тот же API настроек, допустимо от 60 с до абсолютного предела экспорта

Переменные окружения#

Переменные установщика#

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

Переменные влияют только на первую установку

Установщик никогда не перезаписывает существующий /etc/hastreamer-cctv/plane.toml — он печатает plane.toml exists — left unchanged (delete it to re-run guided setup). При запуске без терминала и без заданных переменных используются значения по умолчанию.

Переменная Какой параметр задаёт Допустимые значения Если не задана
HASTREAMER_PLANE_TLS_MODE [tls] mode httpoff, self-signedself_signed, own-certpem, acmeacme; принимается и номер пункта меню от 1 до 4 http. Неизвестное значение прерывает установку: invalid HASTREAMER_PLANE_TLS_MODE='…' (choose one of: http self-signed own-cert acme)
HASTREAMER_PLANE_HOSTNAME [tls] hosts (одна запись, для self_signed и acme) и хост в собираемом public_url Имя хоста или IP Результат hostname -f; если пусто или localhost — первый адрес из hostname -I; иначе 127.0.0.1
HASTREAMER_PLANE_PORT Порт в listen (всегда записывается как 0.0.0.0:<порт>) Целое 1–65535, иначе invalid port: … 8080 для http; 443 для self-signed, own-cert и acme
HASTREAMER_PLANE_PUBLIC_URL public_url Полный origin-URL <схема>://<хост>, если порт стандартный для схемы, иначе <схема>://<хост>:<порт>
HASTREAMER_PLANE_CERT [tls] cert, только для own-cert Путь /etc/hastreamer-cctv/plane-cert.pem
HASTREAMER_PLANE_KEY [tls] key, только для own-cert Путь /etc/hastreamer-cctv/plane-key.pem
HASTREAMER_PLANE_ACME_EMAIL [tls] acme_contact, только для acme Адрес почты admin@<хост>
HASTREAMER_PLANE_EVIDENCE_DIR [attachment_storage] dir Абсолютный путь, иначе must be an absolute path /var/lib/hastreamer-cctv/attachments. Учитывается только если plane.toml ещё не существует

Значения, которые установщик пишет всегда и переменной не переопределяются: database_url (собирается из сгенерированного пароля), log_level = "info", db_pool_max = 16, [keys] session_key_pem, [jwks] session_ttl_secs = 3600, [tls] acme_listen = "0.0.0.0:80".

Установка без вопросов:

HASTREAMER_PLANE_TLS_MODE=acme \
HASTREAMER_PLANE_HOSTNAME=cctv.example.com \
HASTREAMER_PLANE_ACME_EMAIL=admin@example.com \
HASTREAMER_PLANE_EVIDENCE_DIR=/srv/cctv-evidence \
sudo -E installers/hastreamer-cctv-plane/install.sh

Переменные, читаемые работающим сервисом#

Переменная Что делает По умолчанию
RUST_LOG Полностью замещает log_level выражением фильтра журнала, например info,cctv_plane::reconciler=debug. Пока переменная задана и непуста, элемент управления уровнем журнала в админке заблокирован и возвращает 409 rust_log_override. Некорректное выражение фатально при старте не задана — действует log_level

Задаётся дополнением к юниту:

sudo systemctl edit hastreamer-cctv-plane
# в открывшемся редакторе:
#   [Service]
#   Environment=RUST_LOG=info,cctv_plane::tls=debug
sudo systemctl restart hastreamer-cctv-plane
Две переменные, которые Plane не читает

DATABASE_URL работающим сервисом не используется: она нужна только тестовому стенду самой сборки. Адрес базы берётся исключительно из plane.toml.

HASTREAMER_ACTIVATION_CODE существует в общих библиотеках установщиков, но установщик Plane не вызывает помощник активации: активация относится к лицензируемым продуктам — ноде, агенту и ретранслятору, — а не к контрол-плейну.

Файлы и каталоги, которыми владеет Plane#

Путь Права и владелец Кто создаёт Содержимое
/etc/hastreamer-cctv/ 0755 root:root установщик каталог конфигурации
/etc/hastreamer-cctv/plane.toml 0640 root:root установщик и админка СЕКРЕТ — пароль базы, ключ доступа S3
/etc/hastreamer-cctv/plane-session.pem 0600 root:root установщик или Plane при первом запуске СЕКРЕТ — ключ подписи сессий ES256
/etc/hastreamer-cctv/plane-session.kid 0644 root:root Plane эпоха идентификатора ключа — не секрет, но восстанавливать вместе с ключом
/etc/hastreamer-cctv/tunnel-control.pem 0600 root:root Plane при первом запуске СЕКРЕТ — личность управления ретрансляторами и агентами
/etc/hastreamer-cctv/tunnel-control.provisioning.key 0600 root:root Plane при первом запуске СЕКРЕТ — 32-байтный корень выдачи туннелей
/etc/hastreamer-cctv/plane-tls/ 0700 root:root Plane в режимах self_signed и acme хранилище TLS, путь задаётся в [tls] store_dir
/etc/hastreamer-cctv/plane-tls/identity.pem 0600 root:root Plane СЕКРЕТ — обслуживаемый сертификат вместе с приватным ключом
/etc/hastreamer-cctv/plane-tls/account.pem 0600 root:root Plane в режиме acme СЕКРЕТ — ключ учётной записи ACME
/etc/hastreamer-cctv/plane-cert.pem и plane-key.pem ключ — 0600, за права отвечает оператор оператор, режим pem СЕКРЕТ (ключ) — ваши сертификат и ключ
/etc/hastreamer-cctv/.dbpass 0600 root:root установщик СЕКРЕТ — сгенерированный пароль роли PostgreSQL
/var/lib/hastreamer-cctv/ 0750 root:root установщик рабочий каталог Plane
/var/lib/hastreamer-cctv/attachments/ или каталог из [attachment_storage] dir 0750 root:root установщик и Plane блобы материалов событий и превью
/usr/local/bin/hastreamer-plane 0755 root:root установщик исполняемый файл
/etc/systemd/system/hastreamer-cctv-plane.service 0644 root:root установщик юнит systemd
База cctv и роль hastreamer_cctv в PostgreSQL установщик и миграции Plane всё состояние системы

Права 0600 Plane проверяет сам — на обоих файлах из [keys] и на tunnel-control.provisioning.key; при более широких правах запуск прерывается сообщением private key <путь> must have mode 0600. Своё хранилище TLS он создаёт сам с правами 0700 и 0600. За остальные файлы — plane.toml, .dbpass и ваши собственные сертификат с ключом в режиме pem — отвечает оператор.

Ни один файл не доступен посторонним
sudo find /etc/hastreamer-cctv -type f \( -perm /o+rwx -o -perm /g+w \) -ls

Ожидаемый вывод — пустой. Любая напечатанная строка означает файл, который читает или может изменить кто-то кроме root.

Что обязательно бэкапить#

Минимальный набор, которого достаточно, чтобы восстановить работающий Plane. Полная процедура — в разделе Резервное копирование.

# Что Почему без этого не восстановиться
1 База PostgreSQL cctv (pg_dump -Fc) Все организации, пользователи, гранты, камеры, ноды, события, интервалы, аудит и продуктовые настройки
2 /etc/hastreamer-cctv/plane.toml Пароль базы, адрес и режим TLS, путь к материалам событий, ключ доступа S3
3 /etc/hastreamer-cctv/plane-session.pem и plane-session.kid Без них Plane не остановится — он молча создаст новую личность, разлогинив всех пользователей и обесценив закешированный нодами JWKS
4 /etc/hastreamer-cctv/tunnel-control.pem и tunnel-control.provisioning.key Без корня выдачи Plane не может воспроизвести конфигурации ретрансляторов и агентов; такие экземпляры остаются изолированными
5 Каталог материалов событий — [attachment_storage] dir, по умолчанию /var/lib/hastreamer-cctv/attachments Блобов нет в базе, в ней только метаданные. Восстановление одной базы даёт битые кадры и вложения у каждого события. Пропустить можно только при бэкенде S3, если бакет копируется отдельно

Копия каталога конфигурации целиком закрывает пункты 2, 3 и 4 сразу:

sudo tar czf /root/cctv-etc-$(date +%F).tgz -C / etc/hastreamer-cctv
sudo chmod 0600 /root/cctv-etc-*.tgz     # в архиве приватные ключи и пароли

Настоятельно рекомендуется добавить:

  • /etc/hastreamer-cctv/.dbpass;
  • /etc/hastreamer-cctv/plane-tls/account.pem — восстановим сам, но повторное использование учётной записи ACME экономит лимиты на создание новых;
  • ваши plane-cert.pem и plane-key.pem в режиме pem — Plane их не воспроизведёт;
  • юнит /etc/systemd/system/hastreamer-cctv-plane.service или его дополнение, если вы его правили.

Не нужны: plane-tls/identity.pem (самоподписанный перевыпустится, ACME закажет заново), исполняемый файл и превью (они выводятся заново из сохранённых блобов).

Ловушка восстановления: .dbpass

Если восстановить plane.toml, но не .dbpass, и затем заново запустить установщик, он сгенерирует новый пароль и выполнит ALTER ROLE … PASSWORD …, при этом отказавшись трогать уже существующий plane.toml. В результате восстановленный database_url перестанет аутентифицироваться. Либо сохраняйте .dbpass, либо после повторного запуска установщика верните роли пароль из plane.toml:

sudo -u postgres psql -c "ALTER ROLE hastreamer_cctv LOGIN PASSWORD 'пароль-из-plane.toml'"

Полный пример: боевая установка с ACME на публичном домене#

Локальное хранилище материалов, PostgreSQL на том же сервере, сертификаты Let's Encrypt. Прокомментирована каждая строка. Все секреты — заглушки: замените их и никогда не коммитьте этот файл.

# /etc/hastreamer-cctv/plane.toml — контрол-плейн SirinVideo CCTV, боевой пример.
# Владелец root:root, права 0640. Содержит пароль базы: не давать доступ на чтение всем.
# Читается ОДИН раз при старте — после любой правки: systemctl restart hastreamer-cctv-plane

# ── Слушатель ───────────────────────────────────────────────────────────────────────────────
# Сокет, который открывает Plane. Только IP:порт (имя хоста отвергается). 0.0.0.0 — все
# интерфейсы; 127.0.0.1 — только если впереди локальный реверс-прокси. Если удалить строку,
# действует зашитое 127.0.0.1:8080 (петля), а НЕ 0.0.0.0.
listen = "0.0.0.0:443"

# Канонический адрес для браузеров и нод. Только origin — без пути, параметров запроса и
# учётных данных. Обязан быть https://, потому что ниже включён режим TLS, и его хост обязан
# присутствовать в tls.hosts. Отсюда строятся ссылки, перенаправления и обратные вызовы нод.
public_url = "https://cctv.example.com"

# ── База данных (обязательно, значения по умолчанию нет) ────────────────────────────────────
# Plane владеет этой схемой и применяет к ней миграции при старте. Пароль лежит здесь открытым
# текстом — из-за этого права 0640. Помимо пула Plane открывает ОДНО дополнительное соединение
# для аренды-одиночки, поэтому max_connections в PostgreSQL считайте как db_pool_max + 1.
database_url = "postgres://hastreamer_cctv:ЗАМЕНИТЕ-ПАРОЛЬ@127.0.0.1:5432/cctv"

# Предельный размер пула подключений. Минимум 5: четыре подключения резервируются под пути
# контрол-плейна, пока идёт приём событий. По умолчанию 16. Из админки не меняется.
db_pool_max = 16

# ── Журнал ──────────────────────────────────────────────────────────────────────────────────
# Одно из debug | info | warn | error — закрытый набор, а не выражение RUST_LOG. Уходит в
# stderr → journald и в ограниченное кольцо в памяти, которое показывает админка. Заданная
# переменная RUST_LOG замещает этот параметр И блокирует управление уровнем в админке.
log_level = "info"

# ── Материалы событий (принадлежат Plane; видеоархив принадлежит ноде и настраивается во Fleet)
# Сюда попадают кадры по движению и ONVIF, вложения к событиям и их превью. Разместите на
# просторном диске. Путь обязан быть абсолютным и выделенным (без "." и "..", никогда не "/").
# Перенести каталог позже можно, но сначала прочитайте правила переноса: админка откажет,
# пока в текущем каталоге лежат блобы, а правка файла руками стартует на новом каталоге и
# поднимет оповещение об оставшихся в старом материалах (обратимо).
[attachment_storage]
backend = "local"
dir = "/var/lib/hastreamer-cctv/attachments"

# Политика заполнения для каталога выше. Пороги — проценты от ёмкости, обязателен строгий
# порядок 1 <= low < high < critical <= 99 (зазор между ними — намеренный гистерезис).
[event_storage_policy]
# Необязательный жёсткий потолок в байтах. Для локального бэкенда можно не задавать — тогда
# берётся ёмкость файловой системы; если задан, то не меньше 64 МиБ. Для S3 обязателен.
# Здесь показано 200 ГиБ:
max_bytes = 214748364800
# "disabled" — давление никогда не удаляет историю (безопасное значение по умолчанию).
# "oldest_purge_day" — РАЗРУШИТЕЛЬНО: при цикле давления «высокий → низкий» списывается целиком
# самое старое поколение суток очистки. Байтовый бюджет сам по себе удалять не разрешает.
volume_retention = "disabled"
low_watermark_percent = 75        # ниже этого порога давление снимается
high_watermark_percent = 85       # на этом пороге и выше давление включается
critical_watermark_percent = 95   # жёсткое давление

# ── Личности подписи (файлы приватных ключей; через API не передаются никогда) ───────────────
[keys]
# Ключ ES256 в PKCS#8 PEM, которым подписываются JWT сессий; ноды проверяют билеты по его
# открытой половине через /.well-known/jwks.json. Если файла нет — будет создан с правами 0600,
# но ПОТЕРЯННЫЙ файл означает НОВУЮ личность (все пользователи разлогинены), поэтому его надо
# бэкапить. При правах шире 0600 Plane откажется стартовать.
# По умолчанию, если строку убрать: <каталог конфига>/signing.pem
session_key_pem = "/etc/hastreamer-cctv/plane-session.pem"
# Отдельная личность ES256 для управления ретрансляторами и агентами, чтобы ротация ключа
# сессий не отрезала ретрансляторы. Создаётся автоматически с правами 0600 рядом с plane.toml
# вместе с 32-байтным tunnel-control.provisioning.key, из которого выводятся ВСЕ конфигурации
# туннелей — бэкапьте оба файла.
# По умолчанию, если строку убрать: <каталог конфига>/tunnel-control.pem
tunnel_control_key_pem = "/etc/hastreamer-cctv/tunnel-control.pem"

# ── Время жизни сессии ──────────────────────────────────────────────────────────────────────
[jwks]
# Срок жизни выданного JWT сессии в секундах. По умолчанию 3600 (1 час). Диапазон не
# проверяется — 0 просрочит каждый токен немедленно. Индивидуального отзыва сессий нет:
# сокращайте это значение либо делайте ротацию ключа (POST /api/v1/admin/keys/rotate).
session_ttl_secs = 3600

# ── TLS собственного слушателя API и админки ────────────────────────────────────────────────
# Режимы: off (открытый HTTP, public_url http://) · external (открытый HTTP, но public_url
# https:// — для терминирующего TLS реверс-прокси) · pem (свои сертификат и ключ) ·
# self_signed (самоподписанный, браузер предупреждает) · acme (автоматический Let's Encrypt).
# Любой режим, кроме off, требует https:// в public_url.
[tls]
mode = "acme"
# Набор имён сертификата. Для acme это ОБЯЗАТЕЛЬНО публичные DNS-имена, указывающие на ЭТОТ
# сервер (IP-литералы и маски отвергаются), и хост из public_url обязан быть в списке.
hosts = ["cctv.example.com"]
# Куда Plane складывает выпущенный им TLS-материал: identity.pem (обслуживаемая пара
# «сертификат + ключ») и account.pem (ключ учётной записи ACME). Каталог приводится к 0700,
# файлы — к 0600. По умолчанию, если строку убрать: <каталог конфига>/plane-tls
store_dir = "/etc/hastreamer-cctv/plane-tls"
# Контакт для уведомлений удостоверяющего центра об истечении срока. Пустая строка допустима,
# но тогда предупреждений не будет.
acme_contact = "admin@example.com"
# ОТДЕЛЬНЫЙ нешифрованный слушатель, отвечающий на проверку HTTP-01 по адресу
# /.well-known/acme-challenge/<токен>. Порт 80 должен быть доступен из интернета и свободен от
# любого другого веб-сервера. Обязан отличаться от listen. По умолчанию "0.0.0.0:80".
acme_listen = "0.0.0.0:80"
# Каталог ACME. Только https:// (исключение — http://127.0.0.1 для тестовых центров). Для
# пробного прогона укажите здесь staging-каталог, чтобы не расходовать боевые лимиты.
# По умолчанию — боевой каталог Let's Encrypt, показанный ниже.
acme_directory = "https://acme-v02.api.letsencrypt.org/directory"
# Продление автоматическое и НЕ настраивается: обновление при остатке менее 30 суток, проверка
# каждые 12 часов, повтор раз в час после неудачи. Если сертификат не удалось получить при
# старте, Plane отдаёт ВРЕМЕННЫЙ самоподписанный сертификат из памяти и продолжает попытки.

Минимальный пример: за реверс-прокси, режим external#

Прокси терминирует TLS на https://cctv.example.com и передаёт запросы в Plane по петлевому интерфейсу. Все не указанные параметры берут значения по умолчанию.

# /etc/hastreamer-cctv/plane.toml — минимальная внутренняя установка (TLS терминирует прокси).
# Права 0640 root:root — пароль базы лежит здесь так же, как и в боевом примере.

# Привязка только к петле: напрямую до Plane не должен доставать никто, кроме локального
# прокси. Это важно ещё и потому, что Plane принимает ПЕРВЫЙ переход X-Forwarded-For как адрес
# клиента для аудита и ограничения частоты — настройте прокси на ПЕРЕЗАПИСЬ этого заголовка.
listen = "127.0.0.1:8080"

# Адрес, который набирают пользователи, — адрес ПРОКСИ, https, хотя сам Plane говорит по HTTP.
public_url = "https://cctv.example.com"

# Обязательно. Значения по умолчанию нет.
database_url = "postgres://hastreamer_cctv:ЗАМЕНИТЕ-ПАРОЛЬ@127.0.0.1:5432/cctv"

[tls]
# "external": Plane отдаёт открытый HTTP, объявляя при этом https:// в public_url, потому что
# шифрует прокси впереди. За терминирующим TLS прокси нужен именно этот режим, а НЕ "off":
# "off" отверг бы https://-адрес сообщением "tls.mode=off requires an http:// public_url".
mode = "external"

# Всё остальное — по умолчанию:
#   log_level    = "info"
#   db_pool_max  = 16
#   [attachment_storage] backend = "local", dir = "/var/lib/hastreamer-cctv/attachments"
#   [event_storage_policy] пороги 75/85/95, max_bytes не задан, volume_retention = "disabled"
#   [keys] session_key_pem        = "/etc/hastreamer-cctv/signing.pem"        (создастся 0600)
#          tunnel_control_key_pem = "/etc/hastreamer-cctv/tunnel-control.pem" (создастся 0600)
#   [jwks] session_ttl_secs = 3600

Если прокси нет вообще и установка живёт в изолированной локальной сети по открытому HTTP, уберите таблицу [tls] целиком и используйте http:// в public_url:

listen       = "0.0.0.0:8080"
public_url   = "http://10.0.0.10:8080"   # http://, потому что tls.mode по умолчанию "off"
database_url = "postgres://hastreamer_cctv:ЗАМЕНИТЕ-ПАРОЛЬ@127.0.0.1:5432/cctv"

Справочник сообщений об ошибках#

Каждое сообщение из списка ниже печатается в виде hastreamer-plane: fatal: invalid plane network configuration: <сообщение> и видно в journalctl -u hastreamer-cctv-plane. Те же строки возвращаются с кодом 400 при сохранении из админки.

db_pool_max must be at least 5 so short event-ingest transactions leave four database connections available
listen must be an IP socket address (hostnames are not bind addresses)
public_url must be an HTTP(S) origin
public_url must be an HTTP(S) origin without credentials, path, query or fragment
tls.mode=off requires an http:// public_url
tls.mode=<mode> requires an https:// public_url
tls.mode=pem requires cert and key paths
tls.mode=<mode> requires at least one hostname
tls.hosts must include the public_url hostname "<host>"
invalid TLS DNS hostname "<host>"
tls.acme_listen must be an IP socket address
tls.acme_listen and the main listen address must be different
ACME HTTP-01 requires DNS hostnames, not IP literals
ACME directory must use HTTPS (loopback HTTP is test-only)
local event storage directory must be a dedicated absolute path below the filesystem root
S3 event storage endpoint must be schemeless host[:port]
S3 event storage endpoint must be schemeless host[:port] without credentials or path
invalid S3 event storage endpoint
S3 event storage bucket must be a non-empty bucket name without path separators
S3 event storage region must not be empty
S3 event storage credentials must not be empty
virtual-host S3 addressing requires a DNS hostname endpoint and a DNS-compatible bucket name
event storage watermarks must satisfy 1 <= low < high < critical <= 99
event storage max_bytes must be at least 64 MiB
event storage max_bytes must fit the PostgreSQL bigint accounting domain
S3 event storage requires an explicit max_bytes budget

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

reading /etc/hastreamer-cctv/plane.toml: <ошибка ввода-вывода>   # файла нет или он нечитаем
parsing /etc/hastreamer-cctv/plane.toml: <ошибка toml>           # синтаксис, неизвестный ключ,
                                                                 # отсутствующий database_url или
                                                                 # public_url, неверное значение
                                                                 # перечисления
private key /etc/hastreamer-cctv/plane-session.pem must have mode 0600

Ошибки, возможные только при сохранении из админки (PATCH /api/v1/settings/plane) и никогда не встречающиеся при старте:

400 no_settings_supplied                                    400 invalid_public_url
400 dvr_export_max_secs_out_of_range                        409 settings_busy
409 rust_log_override                                       409 event_storage_rotation_blocked
400 destructive_volume_retention_acknowledgement_required   502 <детали проверки бэкенда>
500 plane_config_unreadable:<ошибка>
"/etc/hastreamer-cctv/plane.toml changed after the settings form was loaded"
S3 access and secret keys must be supplied together
S3 event storage credentials are required when selecting the backend

После правки — проверка#

sudo systemctl restart hastreamer-cctv-plane
systemctl is-active hastreamer-cctv-plane                # ожидается: active
sudo journalctl -u hastreamer-cctv-plane -n 80 --no-pager
Признаки успешного старта

Журнал исправного запуска содержит по порядку: hastreamer-plane bootingattachment storageconnected to PostgreSQLmigrations appliedseeds appliedevent evidence backend readyinitial partition maintenance okPlane signing identities readyserving с фактическими listen и tls_mode.

Ключевая строка — последняя:

INFO hastreamer_plane: serving listen=0.0.0.0:443 tls_mode="acme"

Если её нет, а сервис перезапускается каждые 3 секунды — в файле ошибка, её текст находится в строке fatal:.

Смежные разделы: Установка с нуля: Plane, Резервное копирование.