Headscale: własny serwer sterujący Tailscale
Uruchom własny serwer sterujący Tailscale na VPS. Zainstaluj headscale z oficjalnego pliku .deb, ustaw server_url przed startem i dołącz pierwszy węzeł.
Czym jest headscale
Headscale to samodzielnie hostowana implementacja serwera sterującego Tailscale. Oznacza to, że maszyną koordynującą prywatną sieć jest należący do użytkownika VPS. Jest to projekt społecznościowy i nie jest rozwijany ani obsługiwany przez Tailscale Inc. Na każdej maszynie nadal działa oficjalny klient tailscale, wskazujący ten serwer za pomocą jednej flagi: --login-server.
Serwer sterujący określa, kto należy do sieci. Przydziela każdemu węzłowi adres z zakresu 100.64.0.0/10, rozsyła klucze publiczne i informuje węzły, gdzie mogą się wzajemnie znaleźć. Tunele nadal wykorzystują WireGuard i są zestawiane bezpośrednio między węzłami. Ruch między dwiema maszynami użytkownika nie przechodzi przez serwer headscale, chyba że nie można utworzyć bezpośredniej ścieżki i węzły muszą użyć przekaźnika.
Każda instancja headscale obsługuje jedną sieć tailnet (jedną sieć Tailscale). Projekt określa to rozwiązanie jako odpowiednie do użytku osobistego lub dla małej organizacji. W przypadku trzech lub czterech maszyn zwykła sieć VPN WireGuard na własnym VPS oznacza mniej oprogramowania do uruchomienia i mniej elementów, które mogą ulec awarii. Headscale jest przydatny, gdy użytkownik nie chce już ręcznie tworzyć bloku [Peer] dla każdego nowego laptopa. Szersze porównanie obu modeli przedstawiono w artykule różnice między WireGuard i Tailscale.
Wymagania przed instalacją
- VPS z systemem Ubuntu 24.04, publicznym adresem IPv4 i dostępem przez sudo. Jeśli serwer jest nowy, najpierw wykonaj instrukcje z sekcji pierwsze dziesięć minut na nowym VPS.
- Rekord DNS A wskazujący ten adres. W tym przewodniku używany jest
headscale.example.com. - Druga domena lub subdomena na potrzeby MagicDNS. W tym przewodniku używany jest
tailnet.example.net. Nie może to być ta sama domena co domena wserver_url. - Jedno urządzenie klienckie do dołączenia, działające pod kontrolą Linux, macOS, Windows, Android lub iOS.
Instalacja headscale z oficjalnego pakietu .deb
Projekt publikuje pakiety .deb na stronie wydań w serwisie GitHub. W lipcu 2026 bieżącym wydaniem jest 0.29.3. Najpierw sprawdź architekturę, ponieważ nazwa pliku ją zawiera.
sudo apt update
sudo apt install -y wget
dpkg --print-architecturePolecenie wyświetla amd64 na zwykłym VPS z architekturą x86 oraz arm64 w planie opartym na Ampere lub Graviton. Wstaw wynik do poniższej zmiennej.
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale versionElement ./ przed nazwą pliku jest wymagany. Bez niego apt wyszukuje w repozytoriach pakiet o nazwie headscale.deb i kończy działanie błędem.
Pakiet tworzy użytkownika systemowego headscale, zapisuje domyślny plik /etc/headscale/config.yaml i instaluje jednostkę systemd. Nie uruchamia usługi, co jest prawidłową kolejnością działań. Dostarczona konfiguracja kieruje server_url do http://127.0.0.1:8080. Nie jest to adres osiągalny przez żadnego klienta, dlatego usługa uruchomiona w tym momencie miałaby nieprawidłową konfigurację, nawet gdyby się uruchomiła. Wykonanie sudo systemctl is-active headscale na tym etapie wyświetla inactive. Jest to oczekiwane zachowanie, a nie błąd.
Skonfiguruj server_url przed uruchomieniem usługi
Edytuj /etc/headscale/config.yaml za pomocą sudo nano /etc/headscale/config.yaml albo wprowadź te same trzy zmiany za pomocą sed. Zachowaj kopię oryginału, ponieważ plik jest długi i zawiera wiele komentarzy. Jest to najlepsze źródło informacji o pozostałych ustawieniach.
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_url to adres, który headscale zapisuje w każdej rejestracji klienta. Klienci będą później łączyć się dokładnie z tym ciągiem, dlatego musi on zawierać publiczną nazwę z https:// na początku, a nie 127.0.0.1.
listen_addr określa adres, na którym nasłuchuje proces. Pozostaw wartość loopback. Reverse proxy na tym samym serwerze kończy połączenie TLS (transport layer security) i przekazuje ruch do tego procesu, dlatego nic spoza serwera nie musi mieć dostępu do portu 8080.
base_domain to sufiks MagicDNS, czyli domena, w której węzły otrzymują nazwy. Musi to być w pełni kwalifikowana nazwa domenowa bez końcowej kropki. Musi także różnić się od domeny określonej w server_url, ponieważ w przeciwnym razie obie przestrzenie nazw kolidowałyby ze sobą.
Pozostaw sekcję bazy danych bez zmian. Domyślnie używana jest baza SQLite w /var/lib/headscale/db.sqlite, w katalogu utworzonym i zarządzanym przez pakiet. SQLite wystarcza dla tailnetu tej wielkości.
Uruchomienie headscale i sprawdzenie, czy działa
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active wyświetla active, a curl wyświetla 200. enable --now wykonuje oba działania: uruchamia usługę i konfiguruje jej uruchamianie po ponownym uruchomieniu systemu.
Jeśli is-active wyświetla failed, odczytaj dziennik za pomocą sudo journalctl -u headscale -n 50 --no-pager. Na tym etapie przyczyną jest niemal zawsze plik konfiguracyjny, ponieważ headscale analizuje cały plik przed otwarciem gniazda. Nieprawidłowe wcięcie lub nieznany klucz zatrzymuje proces, zanim zacznie nasłuchiwać. Popraw plik, a następnie wykonaj sudo systemctl restart headscale. Każda późniejsza zmiana konfiguracji wymaga takiego samego ponownego uruchomienia. Po nim klienci łączą się ponownie automatycznie. Jeśli jednostki systemd są nowe, informacje o uruchamianiu własnych usług i timerów za pomocą systemd znajdują się w uruchamianiu własnych usług i timerów za pomocą systemd.
Sprawdź pliki stanu w powłoce:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.keyOba wiersze zaczynają się od headscale, czyli nieuprzywilejowanego użytkownika utworzonego przez pakiet. noise_private.key jest tożsamością serwera przekazywaną klientom. Nie usuwaj go. Po usunięciu headscale wygeneruje nową tożsamość i każdy węzeł będzie musiał zarejestrować się ponownie.
Umieść TLS przed headscale
Klienci muszą łączyć się z server_url przez HTTPS. Caddy to najkrótsza droga, ponieważ samodzielnie żąda certyfikatu i odnawia go.
sudo apt install -y caddyZastąp /etc/caddy/Caddyfile blokiem z dokumentacji headscale:
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddyvalidate wyświetla adapted config to JSON, gdy plik ma poprawną składnię. Ostrzeżenie, że plik nie jest sformatowany, ma charakter kosmetyczny. Z laptopa curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health również powinno wyświetlić 200. Ta pojedyncza kontrola potwierdza, że DNS, zapora sieciowa, certyfikat i serwer proxy działają razem.
Istnieje jeden szczegół konfiguracji proxy, który często powoduje wielogodzinne problemy. Połączenie sterujące Tailscale jest aktualizacją HTTP, jest inicjowane za pomocą POST, a wartość nagłówka Upgrade wynosi tailscale-control-protocol. Caddy przekazuje to bez dodatkowej konfiguracji. nginx tego nie robi, dlatego interfejs nginx wymaga mapy aktualizacji:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}Jeśli te wiersze zostaną pominięte, zwykłe żądania nadal będą działać. Dlatego /health zwraca 200 i wszystko wygląda poprawnie, podczas gdy długotrwałe połączenie sterujące nigdy nie zostaje ustanowione, a węzły rejestrują się, po czym pozostają offline. W przypadku wybrania nginx sekcja Certbot w Ubuntu 24.04 z nginx opisuje konfigurację certyfikatu.
Które porty należy otworzyć w UFW
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbosePort 443 obsługuje całą komunikację z klientami. Port 80 służy wyłącznie do wyzwania HTTP ACME (automatic certificate management environment) oraz przekierowania do HTTPS. Caddy potrzebuje tego portu, aby w ogóle uzyskać certyfikat.
Port 8080 pozostaje zamknięty. listen_addr to 127.0.0.1:8080, dlatego proxy łączy się z headscale za pośrednictwem interfejsu loopback i nie jest potrzebna żadna reguła zapory. Otwarcie portu 8080 dla Internetu udostępnia klientom nieszyfrowany kanał sterowania i nie przynosi żadnych korzyści. Należy pamiętać, że większość dostawców uruchamia w panelu sterowania drugą zaporę, niezależną od UFW. W efekcie port może być otwarty na serwerze, a jednocześnie zamknięty na brzegu sieci. W artykule Podstawy zapory UFW na VPS dokładniej omówiono składnię reguł.
Utworzenie użytkownika i klucza preauth
sudo headscale users create alice
sudo headscale users listPolecenie headscale jest klientem. Komunikuje się z działającym demonem za pośrednictwem gniazda Unix w lokalizacji /var/run/headscale/headscale.sock, które ma tryb 0770 i należy do grupy headscale. Wynikają z tego dwie kwestie. Polecenie kończy się niepowodzeniem, gdy usługa jest zatrzymana. To kolejny powód, dla którego kolejność w tym przewodniku ma znaczenie. Polecenie wymaga także sudo, chyba że bieżące konto zostanie dodane do grupy headscale.
users list wyświetla identyfikator obok każdej nazwy. Ten numer jest potrzebny, ponieważ polecenie tworzenia klucza przyjmuje numeryczny identyfikator użytkownika, a nie nazwę.
sudo headscale preauthkeys create --user 1 --expiration 24hKlucz jest wyświetlany tylko raz. Należy skopiować go od razu. Klucz preauth jest jednorazowy i ważny przez jedną godzinę, chyba że zostanie ustawiony inny czas, dlatego podczas testów warto ustawić --expiration 24h. Należy dodać --reusable w przypadku klucza używanego do rejestracji kilku maszyn. Taki klucz należy traktować jak hasło, ponieważ każda osoba, która go posiada, może dołączyć do sieci.
Połącz pierwszego klienta za pomocą --login-server
Na maszynie, która ma zostać dołączona:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 wyświetla adres przypisany przez headscale, na przykład 100.64.0.1. Następnie na serwerze sudo headscale nodes list wyświetla węzeł wraz z jego identyfikatorem, użytkownikiem i stanem online.
Wartość --login-server musi dokładnie odpowiadać wartości server_url, łącznie ze schematem i bez końcowego ukośnika. Wartości są porównywane jako ciągi znaków. Niezgodność powoduje, że klient rejestruje się pod jednym adresem, a następnie otrzymuje polecenie komunikacji z innym adresem.
Maszyna, na której wcześniej zalogowano się do hostowanej usługi Tailscale, zachowuje to logowanie. Najpierw uruchom na niej sudo tailscale logout, a następnie tailscale up z --login-server.
Po pominięciu --auth-key klient wyświetla adres URL. Otwórz go. Strona wyświetla identyfikator tej próby rejestracji. Zatwierdź ją na serwerze:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGETa metoda jest wygodniejsza na własnym laptopie. Klucze preauth lepiej sprawdzają się w przypadku zadań wykonywanych za pomocą skryptów, ponieważ nie wymagają obecności użytkownika.
DERP i przekaźniki obsługujące ruch w przypadku braku połączenia bezpośredniego
DERP (designated encrypted relay for packets) to ścieżka awaryjna. Gdy dwa węzły nie mogą nawiązać bezpośredniego połączenia WireGuard, zwykle dlatego, że oba znajdują się za restrykcyjnym NAT (network address translation), przesyłają pakiety przez przekaźnik. Przekaźnik nie ma kluczy, więc nie może odczytać ruchu. Widzi jednak, które węzły się komunikują i ile danych jest przesyłanych.
Należy jasno określić działanie konfiguracji domyślnej. Headscale jest dostarczany ze wskazaniem na https://controlplane.tailscale.com/derpmap/default oraz z auto_update_enabled: true i update_frequency: 3h, dlatego płaszczyzna sterowania należy do użytkownika, a przekaźniki należą do Tailscale. Dla większości użytkowników jest to rozsądny kompromis. Jeśli nie jest to akceptowalne, należy uruchomić własne przekaźniki.
Aby uruchomić własny przekaźnik, należy ustawić enabled: true w sekcji derp.server pliku config.yaml, ponownie uruchomić headscale i otworzyć port STUN (session traversal utilities for NAT) za pomocą sudo ufw allow 3478/udp. Plik konfiguracyjny jasno określa wymaganie: server_url musi używać https, ponieważ DERP wymaga TLS. Opróżnienie listy derp.urls usuwa przekaźniki Tailscale z mapy. Jeśli zostanie to wykonane bez działającego wbudowanego przekaźnika, każda para węzłów, która nie może połączyć się bezpośrednio, nie będzie mogła połączyć się w ogóle.
Po stronie klienta tailscale netcheck wyświetla opóźnienie do każdego znanego regionu przekaźników, a tailscale status oznacza każdego peera jako direct z adresem albo relay z kodem regionu. Peer pozostający w stanie relay wskazuje na problem z NAT, a nie z headscale.
Dlaczego węzeł jest oznaczony jako offline?
Proxy odrzuca żądanie upgrade. To najczęstszy przypadek. Jego charakterystyczną cechą jest to, że wszystko inne działa prawidłowo: /health zwraca kod 200, headscale nodes list pokazuje węzeł, a węzeł nigdy nie przechodzi w stan online. Połączenie sterujące jest żądaniem POST zawierającym Upgrade: tailscale-control-protocol. Proxy, które go nie przekazuje, przerywa jedyny kanał raportujący stan węzła. Należy porównać konfigurację nginx z blokiem map powyżej albo przełączyć się na Caddy, aby wykluczyć proxy jako przyczynę.
server_url zmieniło się po zarejestrowaniu węzłów. Węzły nadal łączą się z wartością otrzymaną podczas rejestracji. Po jej edycji należy uruchomić sudo tailscale up --login-server https://headscale.example.com --force-reauth na każdym węźle.
Klient nie jest uruchomiony. Na węźle należy wykonać sudo systemctl is-active tailscaled i sudo journalctl -u tailscaled -n 50 --no-pager. Klient, który nie może rozwiązać nazwy domeny lub połączyć się z domeną, zapisuje tam informacje o kolejnych próbach.
Klucz wygasł. Ten przypadek opisano w następnej sekcji.
Aby monitorować stronę serwera podczas testu, należy uruchomić sudo journalctl -u headscale -f na VPS i ponownie uruchomić tailscaled na kliencie. Węzeł, który dociera do headscale, natychmiast generuje wpisy w logu. Brak wpisów oznacza, że żądanie nie dociera do serwera. W takim przypadku przed sprawdzeniem headscale należy sprawdzić DNS, zaporę sieciową i proxy.
Wygaśnięcie kluczy i węzeł, który przestaje działać po kilku tygodniach
Występują dwa niezależne mechanizmy wygaśnięcia. Pomylenie ich powoduje stratę czasu.
Klucze preauth wygasają szybko, zgodnie z założeniami. Wartość domyślna to jedna godzina i jedno użycie. Jeśli tailscale up odrzuca klucz, należy wygenerować nowy na serwerze zamiast modyfikować cokolwiek po stronie klienta.
Klucze węzłów są trwałą częścią konfiguracji. Sekcja node w pliku config.yaml ustawia expiry: 0, a 0 oznacza brak domyślnego terminu wygaśnięcia: zarejestrowany węzeł pozostaje ważny do momentu jego wygaśnięcia. Węzły z tagami nigdy nie wygasają. Należy ustawić expiry: 180d, jeśli rejestracje mają wygasać po określonym czasie. Trzeba rozumieć konsekwencje: każdy węzeł bez tagów będzie wtedy wymagał sudo tailscale up --login-server https://headscale.example.com --force-reauth zgodnie z tym harmonogramem, a bezobsługowy serwer, na którym nikt ponownie nie przeprowadza uwierzytelniania, samoczynnie wypadnie z sieci.
W przypadku utraty laptopa należy wykonać te czynności ręcznie. sudo headscale nodes list wyświetla identyfikator, następnie sudo headscale nodes expire -i 3 wylogowuje ten węzeł, a sudo headscale nodes delete -i 3 całkowicie usuwa go z sieci.
Kopie zapasowe i aktualizacje
/var/lib/headscale i /etc/headscale razem stanowią cały serwer. Przed ich skopiowaniem należy zatrzymać usługę, ponieważ SQLite może mieć niezapisane operacje, a kopia bazy danych wykonana podczas obciążenia może być niespójna.
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgzOba pliki należy przenieść poza serwer. Zawierają klucze prywatne i wszystkie rejestracje, dlatego wymagają takiej samej ochrony jak sam serwer. kopie zapasowe restic z VPS opisuje wykonywanie tej czynności zgodnie z harmonogramem i z szyfrowaniem.
Aktualizacje powtarzają proces instalacji: należy pobrać nowy .deb i sudo apt install ./headscale.deb, a następnie ponownie uruchomić usługę i ponownie wykonać kontrole is-active oraz /health. Od wersji 0.29 ścieżka aktualizacji jest ściśle określona. Pomijanie wersji pośredniej jest blokowane. Blokowane jest również obniżanie wersji do starszej wersji pośredniej. Należy przechodzić przez kolejne wersje pośrednie, przed każdym krokiem wykonać kopię zapasową i najpierw przeczytać informacje o wydaniu danej wersji, ponieważ w tej samej wersji zmieniono sposób działania zasad ACL oraz przeniesiono kilka kluczy konfiguracji.
FAQ
Dlaczego headscale nie uruchamia się od razu po zainstalowaniu pakietu .deb?
Pakiet instaluje jednostkę, ale pozostawia usługę zatrzymaną, a domyślny /etc/headscale/config.yaml jest szablonem, a nie działającą konfiguracją. Najpierw należy zmodyfikować server_url, listen_addr i base_domain, a następnie uruchomić sudo systemctl enable --now headscale i potwierdzić wynik za pomocą sudo systemctl is-active headscale. Jeśli usługa nadal się nie uruchamia, sudo journalctl -u headscale -n 50 --no-pager wskaże problem. Na tym etapie jest to niemal zawsze błąd YAML, ponieważ headscale analizuje cały plik przed rozpoczęciem nasłuchiwania na porcie.
Czy na komputerach nadal należy instalować standardowego klienta Tailscale?
Tak. Headscale zastępuje tylko serwer sterujący. Każdy węzeł uruchamia oficjalnego klienta Tailscale, któremu należy wskazać serwer za pomocą sudo tailscale up --login-server https://headscale.example.com. Ta flaga jest dostępna w standardowym kliencie, więc nie trzeba wprowadzać poprawek ani przebudowywać oprogramowania.
Czy mój ruch przechodzi przez serwer headscale?
Zwykle nie. Headscale koordynuje sieć oraz przekazuje klucze i adresy, natomiast ścieżka danych wykorzystuje protokół WireGuard bezpośrednio między węzłami. Ruch jest przekierowywany tylko wtedy, gdy dwa węzły nie mogą połączyć się bezpośrednio i korzystają z przekaźnika DERP. W dostarczonej konfiguracji są to publiczne przekaźniki Tailscale. Uruchomienie tailscale status na węźle pozwala sprawdzić, czy dany element równorzędny ma stan direct, czy korzysta z relay.
Dlaczego mój węzeł pozostaje offline po zarejestrowaniu?
Węzeł, który jest widoczny w headscale nodes list, ale nigdy nie przechodzi do trybu online, zwykle utracił połączenie sterujące z odwrotnym serwerem proxy. Jest to aktualizacja HTTP wysyłana za pomocą POST z nagłówkiem Upgrade: tailscale-control-protocol. nginx odrzuca takie połączenie, jeśli nie zostanie dodany blok map $http_upgrade $connection_upgrade oraz odpowiednie wiersze proxy_set_header. Caddy przekazuje to połączenie bez dodatkowej konfiguracji, dlatego można szybko sprawdzić, czy przyczyną problemu jest serwer proxy.
Czy headscale wymaga nazwy domenowej i TLS?
W praktyce tak. Klienci łączą się z wartością podaną w server_url, certyfikaty są wystawiane dla nazw, a nie dla samych adresów IP, a plik konfiguracyjny określa, że DERP wymaga TLS. Skonfigurowanie domeny i Caddy zajmuje około 5 minut oraz zapewnia punkt końcowy HTTPS, dla którego certyfikat jest automatycznie odnawiany. Uruchomienie serwera sterującego przez zwykły HTTP oznacza, że cała komunikacja każdego klienta z serwerem przechodzi przez internet w postaci jawnej.