SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Веб-интерфейс для Headscale: Headplane и альтернативы

У Headscale нет своей панели. Ставим Headplane 0.7.1 в Docker, выдаём ему ключ API со сроком действия и закрываем доступ: SSH-туннель или tailnet, а снаружи только OIDC.

Есть ли у Headscale веб-интерфейс

Своего веб-интерфейса у Headscale нет. Сервер управляется командой headscale и через API. Самая полная сторонняя панель для него на сегодня называется Headplane. Она показывает узлы и маршруты, редактирует ACL, меняет настройки DNS и пускает администраторов через OIDC. Ниже мы ставим Headplane 0.7.1 рядом с Headscale в Docker. Вторая, более важная часть статьи объясняет, как не превратить панель в открытую дверь во всю вашу сеть.

Текст продолжает руководство по установке Headscale на VPS, где всё делается из командной строки. Здесь предполагается, что Headscale уже работает в Docker Compose и к нему подключено хотя бы несколько устройств. Без живых узлов панель покажет пустой список, и проверить её будет нечем.

Что умеет Headplane

Headplane развивается на GitHub как проект tale/headplane под лицензией MIT. По документации проекта он умеет:

  • управлять узлами: имя, владелец, срок действия ключа, маршруты;
  • редактировать ACL (access control list, список правил доступа) и теги;
  • менять настройки DNS в конфигурации Headscale;
  • пускать администраторов через OIDC (OpenID Connect) и раздавать им роли внутри панели.

У панели два режима. В ограниченном режиме (Limited Mode) Headplane говорит с Headscale только через API. Узлы и маршруты видны, но настройки сервера поменять нельзя. Документация называет этот режим тестовым. В полном режиме Headplane дополнительно читает и пишет config.yaml от Headscale, а после изменений перезапускает Headscale. Под Docker перезапуск идёт через сокет Docker. Чего это стоит с точки зрения безопасности, разберём ниже.

Какая версия Headplane подходит к вашей версии Headscale

Headplane работает поверх API Headscale, а этот API меняется между минорными версиями. Поэтому тег образа мы фиксируем, а совместимость проверяем по примечаниям к выпуску.

На октябрь 2026 года последний выпуск Headplane имеет номер 0.7.1 (август 2026). В его примечаниях нового заявления о совместимости нет. Последнее такое заявление сделано в 0.7.0: Headplane требует Headscale 0.27.0 или новее и работает с Headscale 0.29.0. Страницы установки на headplane.net при этом всё ещё пишут «0.26.0 или новее». Эта цифра устарела, потому что выпуск 0.7.0 поднял минимум до 0.27.0. Верьте примечаниям к выпуску, а не странице установки.

Узнайте свою версию Headscale:

docker exec headscale headscale version

Если версия ниже 0.27.0, сначала обновите Headscale. Ветку Headplane 0.6.x ниже 0.6.3 не используйте: в 0.6.3 закрыта уязвимость обхода пути (path traversal).

Последняя версия самого Headscale на октябрь 2026 года 0.29.4. Отдельного заявления о патчах 0.29.x у Headplane нет. Поэтому после обновления любой из двух программ проверяйте панель руками: открывается ли список узлов и сохраняется ли изменение DNS. Меняйте версии по одной, а не обе сразу. Тогда при поломке ясно, какая из двух виновата.

Тег latest не берите. Он сдвигается сам при каждом docker compose pull, и Headplane может уехать на версию, которую ваш Headscale не поддерживает.

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

Любой ключ API Headscale даёт полный административный доступ. Разграничения прав в Headscale нет. У команды headscale apikeys create есть только один параметр, срок действия --expiration. Тот, у кого есть ключ, может:

  • создать ключ предварительной авторизации (pre-auth key) и подключить своё устройство к вашей сети;
  • одобрить чужой маршрут или выходной узел (exit node);
  • переписать политику ACL, если она хранится в базе данных;
  • удалить или переименовать любой узел.

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

docker exec headscale headscale apikeys create --expiration 30d

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

docker exec headscale headscale apikeys list

Когда ключ больше не нужен или мог утечь, отзовите его по префиксу:

docker exec headscale headscale apikeys expire --prefix <PREFIX>

Второй источник власти: право записи в config.yaml. Через интерфейс Headplane меняет в этом файле настройки DNS: MagicDNS, серверы имён, домены поиска, дополнительные записи. Но процесс с правом записи не ограничен тем, что показывает интерфейс. Взломанный Headplane может, например, сменить oidc.issuer на провайдера злоумышленника. После перезапуска Headscale новые узлы будет регистрировать тот, кто управляет этим провайдером. Если смонтировать файл только для чтения, Headplane покажет настройки, но сохранить изменения не сможет. Так прямо написано в примере конфигурации проекта.

Третий источник власти: сокет Docker. Полный режим под Docker требует /var/run/docker.sock, чтобы перезапускать контейнер Headscale. Доступ к этому сокету равен правам root на хосте, потому что через него можно запустить новый контейнер с примонтированным корнем / хоста. Флаг :ro на монтировании сокета не помогает. Он запрещает менять сам файл сокета, но не запрещает отправлять через него запросы.

Отсюда модель угроз. Панель с полным режимом означает вход в tailnet плюс root на VPS. Публиковать её в интернет за одним паролем нельзя. Подходят два варианта: доступ только изнутри (SSH-туннель или сама сеть tailnet) либо вход через OIDC с отключённым входом по ключу. Если редактировать DNS из браузера вам не нужно, оставьте интеграцию с Docker выключенной. Тогда у Headplane остаётся только ключ API, и сокет Docker ему не нужен.

Установка Headplane в Docker Compose

Дальше предполагается, что Headscale описан в compose-файле как служба headscale, а его конфиг лежит на хосте в ./headscale/config/config.yaml. Если у вас другие пути, поправьте их. Если сам формат compose-файла вам пока непривычен, начните с основ Docker Compose на VPS.

Шаг 1. Headscale должен слушать не только loopback

grep listen_addr ./headscale/config/config.yaml

В примере конфигурации Headscale стоит 127.0.0.1:8080. Внутри контейнера это loopback самого контейнера, поэтому Headplane из соседнего контейнера до Headscale не достучится. Нужна строка listen_addr: 0.0.0.0:8080. Публиковать порт 8080 наружу ради панели не нужно, контейнеры общаются по внутренней сети Docker.

mkdir -p ./headplane/data
openssl rand -base64 24

Пример конфигурации Headplane требует секрет длиной 32 символа. Команда openssl rand -base64 24 печатает ровно 32 символа, потому что 24 байта в кодировке base64 занимают 32 знака.

Шаг 3. Файл ./headplane/config.yaml

server:
  host: "0.0.0.0"
  port: 3000
  base_url: "http://localhost:3000"
  cookie_secret: "<32 символа из openssl>"
  cookie_secure: false
  data_path: "/var/lib/headplane"

headscale:
  url: "http://headscale:8080"
  config_path: "/etc/headscale/config.yaml"

integration:
  docker:
    enabled: true
    container_label: "me.tale.headplane.target=headscale"
    socket: "unix:///var/run/docker.sock"

base_url пишется без /admin на конце, так требует пример конфигурации. cookie_secure: false нужен, пока панель открывается по обычному HTTP. За обратным прокси с HTTPS поставьте true. Имя headscale в headscale.url указывает на службу из того же compose-файла. Встроенный DNS Docker находит его, только если оба контейнера в одной сети, а в одном compose-файле так и есть по умолчанию.

Шаг 4. Служба в docker-compose.yml

Добавьте службу headplane в тот же файл, где описан Headscale, а к службе headscale добавьте метку:

services:
  headscale:
    # существующие настройки службы оставьте как есть
    labels:
      me.tale.headplane.target: headscale

  headplane:
    image: ghcr.io/tale/headplane:0.7.1
    container_name: headplane
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
    volumes:
      - "./headplane/config.yaml:/etc/headplane/config.yaml"
      - "./headplane/data:/var/lib/headplane"
      - "./headscale/config/config.yaml:/etc/headscale/config.yaml"
      - "/var/run/docker.sock:/var/run/docker.sock"

По метке Headplane находит контейнер, который нужно перезапустить после изменения настроек. Конфиг Headscale монтируется в оба контейнера по одному и тому же пути /etc/headscale/config.yaml.

Запись 127.0.0.1:3000:3000 здесь главная строка. Docker сам пишет правила iptables для опубликованных портов, и эти правила обходят ufw. Запись 3000:3000 без адреса откроет панель всему интернету, даже если ufw запрещает порт 3000. Привязка к 127.0.0.1 оставляет порт только на loopback хоста.

Шаг 5. Запуск и проверка

docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs --tail 50 headplane

docker compose config --quiet ничего не печатает, если файл корректен. Ошибку отступа в YAML вы увидите здесь, а не после запуска. docker compose ps должен показать headplane в состоянии running. Теперь проверьте, на каком адресе слушает порт:

ss -tln | grep 3000

В выводе должен быть 127.0.0.1:3000. Если там 0.0.0.0:3000, значит, в ports осталась запись без адреса, и панель видна из интернета.

Как открыть панель Headplane, не публикуя её в интернет

Самый простой путь: SSH-туннель с вашего компьютера.

ssh -N -L 3000:127.0.0.1:3000 user@your-vps

Пока команда работает, откройте http://localhost:3000/admin и вставьте ключ API. Сессия по умолчанию живёт сутки (cookie_max_age: 86400). Закрыли туннель, и панель снова недоступна никому снаружи.

Второй путь: сеть tailnet. Если сам VPS подключён к Headscale как узел, опубликуйте порт на его адресе в tailnet. Узнайте адрес командой tailscale ip -4, затем замените в ports строку на "100.64.0.1:3000:3000" (подставьте свой адрес) и поменяйте base_url на http://100.64.0.1:3000. Добавьте в политику ACL правило, которое пускает к порту 3000 этого узла только группу администраторов. Иначе панель увидит любое устройство вашей сети.

У этого пути есть известная ловушка. Docker привязывает порт в момент старта контейнера. Если после перезагрузки контейнер стартует раньше, чем поднялся интерфейс tailscale0, адреса ещё нет, и Docker сообщает bind: cannot assign requested address. Выполните docker compose up -d ещё раз, когда tailscale ip -4 начнёт печатать адрес. У SSH-туннеля этой проблемы нет.

Вход через OIDC, если панель нужна снаружи

Иногда панель нужна людям, которым неудобно поднимать туннель. Тогда ставьте её за обратный прокси с HTTPS и включайте OIDC. Провайдер удостоверений можно держать у себя: подойдёт Authentik на своём сервере. Если выбираете между двумя популярными вариантами, посмотрите сравнение Authelia и Authentik.

В режиме OIDC панель хранит один ключ API на стороне сервера. Документация Headplane советует ключ с долгим сроком, например на год. Каждый вошедший пользователь действует через этот ключ, а ограничивают его роли внутри Headplane. Добавьте в config.yaml:

server:
  base_url: "https://headplane.example.com"
  cookie_secure: true

headscale:
  api_key: "<ключ из headscale apikeys create>"

oidc:
  issuer: "https://auth.example.com"
  client_id: "headplane"
  client_secret_path: "/var/lib/headplane/oidc_client_secret"
  use_pkce: true
  disable_api_key_login: true

У провайдера зарегистрируйте адрес возврата https://headplane.example.com/admin/oidc/callback. Пример конфигурации советует использовать тот же client_id, что и у самого Headscale, если Headscale тоже входит через OIDC.

Порядок важен. Первый вошедший через OIDC пользователь получает роль Owner с полным доступом. Значит, войдите сами сразу после включения, до того как откроете адрес другим. Новые пользователи получают роль member, и интерфейс им недоступен, пока владелец не назначит им другую роль. Параметр disable_api_key_login: true убирает с публичной страницы входа поле для ключа API. Тогда попасть в панель можно только через провайдера, и подбирать ключ на странице входа бессмысленно.

Туннель Cloudflare здесь не замена OIDC. Cloudflare Tunnel без открытых портов убирает входящий порт на VPS, но сама панель после этого доступна из интернета. Проверку личности всё равно должен делать кто-то перед ней.

Чем отличаются headscale-ui и другие интерфейсы

Документация Headscale ведёт список сторонних интерфейсов и прямо предупреждает: авторы Headscale их не поддерживают. Коротко о каждом:

  • headscale-ui (gurucomputing): статический сайт на SvelteKit без своего сервера. Браузер обращается к API Headscale напрямую с ключом, который вы вставляете в настройках. Сайт нужно отдавать по пути /web на том же поддомене, что и Headscale, либо настроить CORS на прокси. Для Headscale 0.28 и новее нужна версия headscale-ui от 2026-03-17 или новее. Править конфиг Headscale и входить через OIDC он не умеет.
  • HeadscaleUi: тоже статическая панель администратора без серверной части.
  • headscale-admin: простой современный интерфейс администратора.
  • ouroboros: сделан для пользователей, которые управляют своими устройствами, а не для администраторов.
  • headscale-console: клиент на WebAssembly с доступом к узлам по SSH, VNC и RDP.
  • headscale-piying: упор на визуальную настройку ACL.
  • HeadControl: минимальная панель на Go и HTMX.
  • Headscale Manager: приложение для Android.

Выбор простой. Если нужен только список узлов и маршрутов, хватит headscale-ui: у него нет доступа ни к конфигу, ни к Docker. Но ключ в браузере всё равно полный, так что правило короткого срока действует и здесь. Если нужны DNS, редактор ACL и вход через OIDC, берите Headplane. Если вы ещё решаете, нужен ли вам Headscale вообще, сначала посмотрите обзор альтернатив Tailscale.

Что ломается чаще всего

Панель не видит Headscale. Проверьте listen_addr из шага 1 и то, что обе службы в одном compose-файле. Если Headscale описан в другом проекте Compose, имя headscale в сети Headplane не разрешается, потому что у каждого проекта своя сеть по умолчанию.

Вход проходит, но через месяц перестаёт работать. Истёк ключ с --expiration 30d. Создайте новый ключ, войдите с ним, затем отзовите старый через apikeys expire. Это ожидаемое поведение, ради него и задаётся срок.

Изменения DNS не применяются. Обычно причина в одном из двух. Либо конфиг Headscale смонтирован только для чтения, либо Headplane не знает, какой контейнер перезапустить: у контейнера Headscale нет метки me.tale.headplane.target или выключен integration.docker.enabled.

Редактор ACL не сохраняет политику. Headscale принимает изменения политики через API только при policy.mode: database. В режиме file политика живёт в файле HuJSON, и править её нужно в этом файле.

FAQ

Есть ли у Headscale встроенный веб-интерфейс?

Нет. Headscale управляется командой headscale и через API. Веб-интерфейсы пишет сообщество, и документация Headscale ведёт их список с предупреждением, что авторы Headscale их не поддерживают. Самый полный из них называется Headplane: он показывает узлы и маршруты, редактирует ACL и DNS и поддерживает вход через OIDC.

Какую версию Headscale поддерживает Headplane 0.7.1?

В примечаниях к 0.7.1 нового заявления о совместимости нет. Последнее заявление сделано в выпуске 0.7.0: нужен Headscale 0.27.0 или новее, проверена работа с Headscale 0.29.0. Фиксируйте тег образа ghcr.io/tale/headplane:0.7.1 и после каждого обновления Headscale проверяйте список узлов и сохранение DNS.

Можно ли выдать Headplane ключ API только на чтение?

Нет. Ключи API в Headscale не имеют областей действия, любой ключ даёт полный административный доступ. Ограничить его можно только сроком: headscale apikeys create --expiration 30d, а отозвать командой headscale apikeys expire --prefix <PREFIX>. Запретить панели менять настройки сервера можно иначе: смонтировать config.yaml от Headscale только для чтения и не давать ей сокет Docker.

Безопасно ли открыть Headplane в интернет?

С одним ключом API на странице входа нет. В полном режиме панель управляет всей сетью tailnet, а через сокет Docker получает права root на VPS. Держите порт на 127.0.0.1 и заходите через SSH-туннель или через tailnet. Если панель всё же нужна снаружи, ставьте её за HTTPS, включайте OIDC и задайте disable_api_key_login: true.