Headscale: свой control-сервер для клиентов Tailscale
Базовая установка Headscale — дело получаса: пакет, конфиг, TLS, первый клиент подключился. Но реальная работа с self-hosted control-сервером начинается дальше — когда нужно решить, кто кому виден в сети, как узлы находят друг друга по имени и как пустить весь трафик ноутбука через сервер в другой стране. Если Headscale у вас ещё не поднят, начните с установки на Ubuntu 24.04 — этот материал предполагает, что сервис уже запущен и клиент хотя бы раз успешно подключился, а разбирает именно то, что остаётся за кадром типового мануала.
Содержание
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →ACL-политики: за пределами простого allow
В базовой установке ACL обычно выглядит как пара правил «группа А видит тег Б». На практике политика быстро обрастает нюансами: протоколами, портами, вложенными группами и порядком применения правил.
Формат политики — HuJSON (JSON с комментариями) либо YAML, путь задаётся в config.yaml:
policy:
path: /etc/headscale/acl.yaml
Headscale обрабатывает правила как белый список: всё, что не разрешено явно, запрещено по умолчанию. Отдельного action: deny в классическом смысле нет — вместо этого политику строят через сужение dst и src. Пример с протоколами и портами:
groups:
group:admins:
- ivan
- petr
group:devs:
- anna
tagOwners:
tag:db:
- group:admins
tag:web:
- group:admins
- group:devs
acls:
- action: accept
proto: tcp
src:
- group:admins
dst:
- tag:db:5432
- tag:db:22
- action: accept
proto: tcp
src:
- group:devs
dst:
- tag:web:80,443
- action: accept
proto: icmp
src:
- group:admins
- group:devs
dst:
- "*:*"
Поле proto (tcp/udp/icmp) сужает правило до конкретного типа трафика — без него разрешены все протоколы на указанных портах. Обратите внимание на третье правило: icmp с портом *:* часто добавляют отдельно, потому что ping удобен для диагностики, а держать его в одном правиле с TCP неудобно с точки зрения читаемости.
Более новые версии Headscale поддерживают autogroup:internet (доступ ко всему внешнему интернету — актуально для exit-node, ниже) и autogroup:self (узел видит только собственные другие узлы того же владельца). На старых версиях эти автогруппы могут быть недоступны — проверьте headscale version перед тем, как на них рассчитывать.
Отдельный блок политики — SSH-доступ через сам Tailscale (Tailscale SSH), если вы им пользуетесь:
ssh:
- action: accept
src:
- group:admins
dst:
- tag:db
users:
- "autogroup:nonroot"
- root
Это разрешает участникам group:admins заходить по tailscale ssh на узлы с тегом db от имени любого непривилегированного пользователя или root — отдельно от обычных ACL для сетевого трафика.
Перед применением всегда проверяйте синтаксис:
headscale policy check
Ошибка в политике не уронит уже работающие соединения (Headscale не в пути трафика), но новые узлы не смогут зарегистрироваться, а обновлённые правила не применятся — узлы продолжат жить по старой карте до следующего опроса control-сервера.
MagicDNS: как реально работает и что настроить руками
MagicDNS резолвит имена узлов вида laptop.ts.example.com в их Tailscale-адреса — но по умолчанию это работает только для доменов внутри вашей base_domain. Всё остальное клиент резолвит через свой обычный DNS. Раздел конфигурации:
dns:
magic_dns: true
base_domain: ts.example.com
nameservers:
global:
- 1.1.1.1
- 8.8.8.8
override_local_dns: true
extra_records:
- name: "gitlab.ts.example.com"
type: "A"
value: "100.64.0.20"
nameservers.global — это резолверы, которые Headscale раздаёт клиентам как основные DNS-серверы для всего интернета (не только для .ts.example.com). override_local_dns: true означает, что клиент Tailscale полностью подменяет системный DNS этими серверами; false — они добавляются как дополнительные, а системные остаются приоритетными для остального трафика.
extra_records — фактически замена /etc/hosts, только раздаётся по сети всем клиентам через MagicDNS: удобно для внутренних сервисов, у которых нет отдельного узла Tailscale (например, IP за NAT-роутом), но нужно человекочитаемое имя.
Split-DNS (когда конкретный домен резолвится через отдельный сервер, а остальное — как обычно) настраивается через nameservers.split:
dns:
nameservers:
split:
internal.corp:
- 10.0.0.53
Это полезно, если у вас уже есть корпоративный DNS для внутреннего домена (например, Active Directory) и вы не хотите, чтобы Tailscale-клиенты резолвили его через публичные резолверы. Проверить, что клиент реально получил нужные записи, можно на самом устройстве:
tailscale status --json | grep -A5 DNS
resolvectl status tailscale0 # на Linux с systemd-resolved
Если MagicDNS не резолвит имена — почти всегда причина в том, что override_local_dns конфликтует с локальным DNS-менеджером устройства (особенно на Windows и в некоторых сборках NetworkManager) или клиент подключён с флагом --accept-dns=false, который отключает приём DNS-настроек от control-сервера целиком.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверExit-node: сервер как единая точка выхода в интернет
Exit-node — режим, при котором весь интернет-трафик клиента (не только трафик до других узлов Tailscale) идёт через один узел сети. Полезно как замена классическому VPN «для всего»: подключились из недоверенной Wi-Fi-сети — весь трафик пошёл через ваш сервер.
На узле, который станет exit-node, нужно включить пересылку пакетов и объявить себя:
echo 'net.ipv4.ip_forward = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | tee -a /etc/sysctl.d/99-tailscale.conf
sysctl -p /etc/sysctl.d/99-tailscale.conf
tailscale up --login-server https://hs.example.com \
--advertise-exit-node
По умолчанию Headscale не пускает узел в роли exit-node автоматически — нужно одобрение со стороны control-сервера:
headscale nodes list
headscale routes list
headscale routes enable -r <route-id>
(В части версий Headscale команда называется headscale nodes approve-routes -i <node-id> --routes ... — синтаксис менялся между релизами, сверяйтесь с headscale --help на своей версии перед тем, как копировать команду один в один.)
На клиенте, который хочет выходить в интернет через exit-node:
tailscale set --exit-node=<hostname-или-IP-exit-node>
tailscale set --exit-node-allow-lan-access=true
Флаг --exit-node-allow-lan-access оставляет доступ к локальной сети клиента (принтеры, NAS) открытым даже при активном exit-node — без него весь трафик, включая локальный, уйдёт на удалённый узел.
Важный нюанс приватности: без отдельной настройки DNS клиент при активном exit-node может продолжать резолвить имена через локальные DHCP-серверы, а не через сам exit-node — это DNS-утечка, которая частично сводит на нет смысл всей затеи. Решение — задать nameservers.global в MagicDNS (см. выше) и включить override_local_dns: true, тогда DNS-запросы тоже пойдут через туннель.
Subnet-роуты: доступ к сети за NAT без второго VPN
Subnet-роуты решают другую задачу: не «весь интернет через узел», а «дать участникам Tailscale-сети доступ к локальной подсети за конкретным узлом» — например, к сети 192.168.1.0/24 за домашним роутером или к внутренней подсети облачного провайдера.
На узле-шлюзе:
tailscale up --login-server https://hs.example.com \
--advertise-routes=192.168.1.0/24,10.20.0.0/16
Одобрение — так же через control-сервер:
headscale routes list
headscale routes enable -r <route-id>
На клиентах, которые должны видеть эту подсеть, нужно принять маршруты (часть систем делает это автоматически, часть — только с флагом):
tailscale up --accept-routes
Практический момент: если в подсети 192.168.1.0/24 уже что-то резолвится через локальный DNS (например, nas.local), эти имена не станут доступны автоматически — только IP-адреса из объявленного диапазона. Для имён заводите extra_records в MagicDNS или настраиваете split-DNS на внутренний DNS-сервер подсети.
Autoapprovers: без ручного клика на каждый узел
Ручное одобрение через headscale routes enable нормально для одного-двух узлов, но не масштабируется на десятки серверов. autoApprovers в файле политики позволяет доверить одобрение конкретным группам или тегам заранее:
autoApprovers:
routes:
"192.168.1.0/24":
- tag:gateway
"10.20.0.0/16":
- group:admins
exitNode:
- tag:exit
Теперь узел с тегом gateway может объявить маршрут на 192.168.1.0/24 и он включится сразу, без ручной команды headscale routes enable; аналогично для exit-node с тегом exit. Это особенно ценно, если серверы поднимаются автоматически (Terraform, cloud-init) — маршрут появляется в сети в момент запуска, без ручного шага администратора.
Оборотная сторона: autoApprovers — это доверие на уровне тега, а не конкретного узла. Убедитесь, что tagOwners для tag:gateway и tag:exit ограничены узкой группой — иначе скомпрометированный ключ с правом назначать теги превращается в право автоматически стать exit-node всей сети.
Эксплуатация: логи, метрики, бэкап и обновления
Headscale хранит всё состояние сети в одной базе — SQLite-файле по умолчанию. Резервная копия — это резервная копия всей сети (узлы, ключи, пользователи, история регистраций):
sqlite3 /var/lib/headscale/db.sqlite ".backup '/var/backups/headscale-$(date +%F).sqlite'"
Снимок можно снимать «на горячую» через .backup — SQLite сам гарантирует консистентность, останавливать сервис не обязательно, но для критичных сред разумно делать это по расписанию через cron и хранить копии вне сервера.
Логи сервиса — через journalctl, как у любого systemd-юнита:
journalctl -u headscale --since "1 hour ago" | grep -i error
Метрики Prometheus отдаются на metrics_listen_addr (в базовом конфиге — 127.0.0.1:9090) — их можно подключить к своему Grafana/Prometheus или снять разово через SSH-туннель:
curl -s http://127.0.0.1:9090/metrics | grep headscale_
Обновление версии — замена .deb-пакета (та же схема, что при первой установке, но с уже существующим конфигом и базой):
HS_VERSION=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest | grep '"tag_name"' | cut -d '"' -f4)
wget "https://github.com/juanfont/headscale/releases/download/${HS_VERSION}/headscale_${HS_VERSION#v}_linux_amd64.deb"
systemctl stop headscale
dpkg -i "headscale_${HS_VERSION#v}_linux_amd64.deb"
systemctl start headscale
Перед обновлением на мажорную версию сверьтесь с changelog проекта — формат политики и флаги команд между релизами Headscale менялись не раз (в частности, переход к более совместимому с Tailscale «policy v2» формату), и старые ACL или скрипты с конкретными именами команд не всегда переживают апгрейд без правок. Бэкап базы перед обновлением — не формальность, а страховка на случай миграции схемы.
Арендуйте сервер под свои задачи!
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли одновременно использовать exit-node и subnet-роуты на одном сервере?
Да, это независимые функции. Один и тот же узел может быть и exit-node для всего трафика, и шлюзом для конкретной подсети — ограничений на совмещение нет, только на порядок объявления флагов при tailscale up.
ACL применяется мгновенно после правки файла?
Нет мгновенного push — клиент получает обновлённую политику при следующем опросе control-сервера (обычно это происходит быстро, в пределах десятков секунд, но не гарантированно мгновенно). Для проверки конкретного правила надёжнее сделать tailscale down && tailscale up на тестовом клиенте.
Что будет, если tagOwners настроен неправильно и узел не может получить нужный тег?
Регистрация узла не упадёт — он просто останется без тега и будет виден в ACL только по правилам для обычных пользователей/групп, а не по тегу. Проверить фактически присвоенные теги: headscale nodes list покажет колонку с тегами узла.
Нужен ли отдельный exit-node в каждой стране или можно один на всю команду?
Один exit-node технически обслуживает любое количество клиентов, ограничение — только пропускная способность самого сервера. Для разных стран выхода в интернет нужны разные физические серверы — Headscale лишь управляет тем, кто к какому exit-node может подключаться.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →