Headscale 自架 Tailscale 控制伺服器教學
在 VPS 上自架 Tailscale control server:從官方 .deb 安裝 headscale,設定 server_url 後重新啟動服務,再加入第一台節點。
headscale 是什麼
Headscale 是 Tailscale 控制伺服器的自架實作,因此協調私有網路的機器會是你擁有的 VPS。這是社群專案,不由 Tailscale Inc. 維運。每台機器仍執行官方 tailscale client,並透過一個旗標指向你的伺服器:--login-server。
控制伺服器負責識別哪些裝置屬於該網路。它會從 100.64.0.0/10 配置位址給各個節點、分發公開金鑰,並告知節點彼此的位置。通道仍使用 WireGuard,並由節點彼此建立。你的兩台機器之間的流量不會經過 headscale 主機,除非無法建立直接路徑,節點才會改用 relay。自行執行這項協調工作,改變的是由誰負責,而不是它能執行的功能。因此,在你單純把這次遷移視為安全性提升之前,應先了解 控制伺服器在此模型中能與不能存取的範圍。
Headscale 每個執行個體提供一個 tailnet(單一 Tailscale network)。專案文件指出,這適合個人使用或小型組織。若只有三或四台機器,在自有 VPS 上使用純 WireGuard VPN 所需執行的軟體較少,出錯的環節也較少。當你不想再為每台新筆記型電腦手動撰寫 [Peer] 區塊時,Headscale 才開始展現價值。成本通常是人們開始尋找方案的原因,因此在承擔伺服器之前,值得先閱讀 託管免費方案實際涵蓋的內容,因為少量個人機器通常可納入其中。若你已超過這項上限,請將費用與 付費方案按每位使用者及每項標記資源計算的成本 比較,因為使用者裝置數量不限,但標記資源有個別的額度與費用。若你想要自架控制平面,但希望使用自有的用戶端及 Web 介面來管理對等端,而不是直接替代 Tailscale,在單一 VPS 上部署 NetBird 是值得評估的替代方案。兩者都透過 WireGuard 傳送網路流量,因此選擇取決於你撰寫存取規則的方式,以及 relay 的運作方式;在安裝任一方案前,值得先比較 NetBird 與 Tailscale 在這些方面的差異。如需比較這兩種模型的整體差異,請參閱 WireGuard 與 Tailscale 的差異。
安裝前的準備
- 一台執行 Ubuntu 24.04、具備公開 IPv4 位址與 sudo 存取權的 VPS。若伺服器是新的,請先完成新 VPS 的前 10 分鐘。
- 一筆指向該位址的 DNS A 記錄。本指南使用
headscale.example.com。 - 供 MagicDNS 使用的另一個網域或子網域。本指南使用
tailnet.example.net。此網域不得與server_url中的網域相同。 - 一台要加入的用戶端機器,作業系統可為 Linux、macOS、Windows、Android 或 iOS。Headscale 0.29.6 需要 Tailscale 用戶端版本 1.80.0 或更新版本。
從官方 .deb 安裝 headscale
專案會在 GitHub releases page 發布 .deb 套件。截至 2026 年 10 月 27 日,最新的穩定版本是 0.29.6,於 2026 年 10 月 24 日發布。請先確認架構,因為檔名會包含架構資訊。
sudo apt update
sudo apt install -y wget
dpkg --print-architecture在一般的 x86 VPS 上,該指令會輸出 amd64;在 Ampere 或 Graviton 類型的方案上,則會輸出 arm64。請將結果填入下方的變數。
HEADSCALE_VERSION="0.29.6"
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 version檔名前的 ./ 是必要的。若省略它,apt 會在套件庫中尋找名為 headscale.deb 的套件,並因此失敗。
套件會建立 headscale system user、寫入預設的 /etc/headscale/config.yaml、安裝 systemd unit,然後啟動服務。因此 apt 返回時,headscale 已在執行,而 sudo systemctl is-active headscale 會輸出 active。不過,在全新安裝時,套件不會啟用該 unit;在重新開機後,服務不會自動恢復,直到你啟用它。下方的重新啟動步驟會完成這項設定。服務也會使用隨附的設定執行,而該設定會將 server_url 指向 http://127.0.0.1:8080;你的任何 client 都無法連線到這個位址。目前尚未暴露任何服務,因為隨附的 listen_addr 也只監聽 loopback。不過,在設定 server_url 並重新啟動服務前,請勿加入任何 client;接下來的兩個步驟會完成這些操作。
在任何用戶端加入前設定 server_url
使用 sudo nano /etc/headscale/config.yaml 編輯 /etc/headscale/config.yaml,或使用 sed 套用相同的 3 項變更。請保留原始檔案的副本,因為此檔案內容很長且註解完整,是後續設定的最佳參考。
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 是 headscale 建立註冊 URL 時使用的公開位址。用戶端會使用 --login-server 設定的控制位址,因此兩處都應在 https:// 使用相同的公開名稱,絕不能使用 127.0.0.1。headscale 文件指出,可以使用純 HTTP,或在非 443 埠上使用 HTTPS,但正式環境強烈建議在 443 埠上使用 HTTPS。本指南的其餘內容也以此為前提。
listen_addr 是程序繫結的位址。請維持使用 loopback。相同主機上的反向代理會終止 TLS(傳輸層安全性),再將流量轉送到此處,因此伺服器外部不需要連線到 8080 埠。
base_domain 是 MagicDNS 後綴,也就是節點取得名稱時所使用的網域。它必須是完整網域名稱,且末尾不得有句點;此外,必須與 server_url 使用不同的網域,否則兩個名稱空間會發生衝突。
請保留資料庫區段的預設設定。預設值是在 /var/lib/headscale/db.sqlite 使用 SQLite,該目錄由套件建立並擁有。對於這種規模的 tailnet,SQLite 已經足夠。
重新啟動 headscale 並確認其正在執行
sudo systemctl restart headscale
sudo systemctl enable headscale
sudo systemctl is-active headscale
sudo systemctl is-enabled headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active 會輸出 active,is-enabled 會輸出 enabled,而 curl 會輸出 200。重新啟動是載入修改內容的步驟。headscale 會在啟動時讀取一次設定,因此套件啟動的程序會持續使用隨套件提供的 server_url,直到重新啟動為止。sudo systemctl enable --now headscale 無法完成這項工作:服務已在執行,因此 --now 的啟動動作不會變更任何內容,舊值仍會生效。開機時啟動是 enable 的工作,而且不會自動完成:在全新安裝中,套件會啟動服務但不會啟用服務,因此請如上所述執行一次 sudo systemctl enable headscale。
如果 is-active 輸出的內容不是 active,或 curl 沒有輸出 200,請使用 sudo journalctl -u headscale -n 50 --no-pager 讀取 journal。此階段的失敗幾乎總是由設定檔造成,因為 headscale 會在開啟 socket 前解析完整檔案;因此,縮排錯誤或無效的必要設定會在程序開始監聽前就使其停止。修正檔案後,再執行 sudo systemctl restart headscale。之後每次修改設定都需要相同的重新啟動。用戶端會在之後自行重新連線。如果你不熟悉 systemd unit,使用 systemd 執行自有服務與計時器涵蓋了此處使用的命令。
在 shell 中時,同時檢查狀態檔案:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.key兩行都以 headscale 開頭,這是套件建立的非特權使用者。noise_private.key 是伺服器提供給用戶端識別自身的身分。請保留它。如果刪除該檔案,headscale 會產生新的身分,而用戶端必須先取得新的伺服器金鑰才能重新連線;現有註冊仍會保留在資料庫中。
在 headscale 前方加入 TLS
用戶端應透過 HTTPS 連線至 server_url。Caddy 是最簡單的方式,因為它會自行申請並續期憑證。
sudo apt install -y caddy將 /etc/caddy/Caddyfile 替換為 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 caddy檔案解析成功時,validate 會輸出 adapted config to JSON。檔案格式未整理的警告不影響功能。在筆記型電腦上,curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health 也應輸出 200。這項單一檢查即可確認 DNS、防火牆、憑證與代理伺服器均能正常配合。
以下是最容易讓人排查一晚的代理細節。Tailscale 控制連線使用 HTTP upgrade,建立連線時會使用 POST,而不是 GET;Upgrade 標頭的值是 tailscale-control-protocol。Caddy 不需額外設定即可轉送這些內容。nginx 則不會自動轉送,因此 nginx 前端必須明確轉送 Upgrade 與 Connection 標頭。headscale 文件使用 upgrade map 處理:
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;
}
}如果省略這些行,一般請求仍會成功。因此 /health 會回傳 200,表面上一切正常,但控制連線始終無法建立:新節點無法註冊,現有節點也無法重新連線並回報上線狀態。若選擇 nginx,在 Ubuntu 24.04 搭配 nginx 使用 Certbot 說明憑證設定部分。
要在 UFW 開放哪些連接埠
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbose連接埠 443 承載所有用戶端與 headscale 的控制連線。若之後啟用內嵌的 DERP relay,還需要 3478/udp,相關設定如下。連接埠 80 用於 ACME(自動憑證管理環境)的 HTTP challenge,以及將請求重新導向至 HTTPS。即使關閉連接埠 80,Caddy 仍可使用連接埠 443 上的 TLS-ALPN challenge 取得憑證;但開放 80 後,兩種 challenge 都可使用,且一般的 http:// 請求會被重新導向。
連接埠 8080 維持關閉。listen_addr 是 127.0.0.1:8080,因此 proxy 會透過 loopback interface 連線至 headscale,不涉及任何防火牆規則。在 UFW 開放 8080 不會暴露任何服務,因為外部沒有服務監聽該連接埠,也沒有實際用途。請注意,多數供應商會在控制面板中另外提供一層防火牆,與 UFW 分開管理。因此,某個連接埠可能在主機上開放,但在網路邊界仍然關閉。VPS 上的 UFW 防火牆基礎會更詳細說明規則語法。
建立使用者與 preauth 金鑰
sudo headscale users create alice
sudo headscale users listheadscale 指令是用戶端。它會透過位於 /var/run/headscale/headscale.sock 的 Unix socket,與執行中的 daemon 通訊。該 socket 的模式為 0770,且擁有者群組為 headscale。這會產生兩個結果。服務停止時,指令會失敗;這也是本指南必須依照順序操作的另一個原因。此外,除非將自己的帳號加入 headscale 群組,否則執行時需要 sudo。
users list 會在每個名稱旁列印 ID。你需要這個數字,因為 key 指令接受數值使用者 ID,而不是名稱。
sudo headscale preauthkeys create --user 1 --expiration 24h金鑰只會列印一次。請立即複製。preauth 金鑰只能使用一次,除非另行指定,否則有效期為 one hour。因此,在仍處於測試階段時,值得設定 --expiration 24h。加入 --reusable 可建立能註冊多台機器的金鑰。請像保護密碼一樣保管該金鑰,因為持有它的任何人都能加入你的網路。
使用 --login-server 連接第一個用戶端
在要加入的機器上:
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 會顯示 headscale 指派的位址,例如 100.64.0.1。回到伺服器後,sudo headscale nodes list 會顯示該節點的 ID、使用者及線上狀態。
對 --login-server 使用 server_url 中的公開 HTTPS 位址。用戶端會移除控制 URL 尾端的斜線;不會進行要求省略斜線的精確字串比對。
先前登入 Tailscale 代管服務的機器會保留該登入狀態。先在該機器上執行 sudo tailscale logout,再搭配 --login-server 執行 tailscale up。
如果省略 --auth-key,用戶端會改為輸出 URL。開啟該 URL,頁面會顯示此次註冊嘗試的識別碼。接著在伺服器上核准:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGE這種表單較適合自己的筆記型電腦。對於需要以指令碼處理的項目,預授權金鑰較實用,因為不需要有人持續查看。VPS 本身成為節點後,也能承載其他機器的網際網路流量,這就是 出口節點設定;差別在於,您必須在伺服器上使用 headscale 指令核准廣告的路由,而不是在代管管理主控台中核准。若您要的是連入該 VPS 後方的私有網路,而不是透過它連出網際網路,則相同的核准步驟也適用於 向 tailnet 的其他節點廣告該子網路。從節點發布單一應用程式,而不是讓整個網路經由該節點路由,則是另一項工作;serve 和 funnel 是兩種作法。不過,兩者都依賴 Tailscale 自有的憑證與入口機制,因此應將它們視為代管 tailnet 功能,而不是 headscale 提供的功能。
DERP,以及直接路徑失效時轉送網路流量的機制
DERP(designated encrypted relay for packets,封包專用加密中繼)是備援路徑。當兩個節點無法建立直接的 WireGuard 連線時,通常是因為兩者都位於嚴格的 NAT(network address translation,網路位址轉換)之後,便會改由中繼轉送封包。中繼不持有任何金鑰,因此無法讀取您的網路流量。但它可以知道哪些節點正在通訊,以及傳輸了多少資料。
請明確了解預設設定的行為。Headscale 預設指向 https://controlplane.tailscale.com/derpmap/default,並使用 auto_update_enabled: true 與 update_frequency: 3h,因此控制平面由您管理,但中繼由 Tailscale 提供。對多數人而言,這是合理的取捨。如果您不接受,請自行執行中繼。
若要執行自有中繼,請在 config.yaml 的 derp.server 下設定 enabled: true,將隨附的 ipv4: 198.51.100.1 與 ipv6: 2001:db8::1 替換為中繼的公開位址,或移除這兩項設定以使用 DNS,然後重新啟動 headscale,並使用 sudo ufw allow 3478/udp 開放 STUN(session traversal utilities for NAT,NAT 工作階段穿越工具)連接埠。這些位址設定會覆寫 DNS,因此只要保留其中任何一個預留值,用戶端就會連線到無法到達的位址。設定檔已明確說明此要求:server_url 必須使用 https,因為 DERP 需要 TLS。清空 derp.urls 清單會從 map 中移除 Tailscale 的中繼;執行此操作前,請先保留可運作的內建或自訂中繼。Headscale 不允許使用空的中繼 map 啟動,而只包含無法到達之中繼的 map,會使無法直接連線的節點無法通訊。
在用戶端執行 tailscale netcheck,可列出其已知各中繼區域的延遲;執行 tailscale status,可查看作用中對等連線,其中 direct 會顯示位址,或 relay 會顯示區域代碼。若對等節點停留在 relay,請檢查其直接路徑,包括 NAT 與防火牆規則。若對等節點處於 direct 但仍然緩慢,則是另一個問題;通常應檢查 MTU,而不是隧道本身。
為什麼節點顯示為離線?
代理伺服器捨棄了升級請求。 其特徵是 /health 回傳 200,但新的註冊要求失敗,或現有節點無法重新連線。控制連線是攜帶 Upgrade: tailscale-control-protocol 的 POST 請求。未轉送此內容的代理伺服器會同時阻擋節點註冊,以及回報節點狀態的通道。請將 nginx 設定與上方的升級相關設定逐項比對,或改用 Caddy,以排除代理伺服器造成的問題。
公開控制位址已變更。 節點會持續連線到以 --login-server 設定的位址;變更 server_url 不會更新該用戶端設定。如果舊位址已無法連線到伺服器,請在每個節點上執行 sudo tailscale up --login-server https://headscale.example.com --force-reauth。
用戶端未執行。 在節點上執行 sudo systemctl is-active tailscaled 和 sudo journalctl -u tailscaled -n 50 --no-pager。如果用戶端無法解析您的網域或連線到該網域,重試記錄會顯示在這些位置。
金鑰已過期。 下一節會說明此問題。
測試時,請在 VPS 上執行 sudo journalctl -u headscale -f 監看伺服器端,並在用戶端重新啟動 tailscaled。也請檢查用戶端 journal:請求抵達 headscale,不代表預設日誌層級一定會產生對應記錄。因此,僅憑伺服器日誌沒有輸出,無法證明問題來自 DNS、防火牆或代理伺服器。
金鑰到期,以及數週後停止運作的節點
這裡有兩種不同的到期機制。混淆兩者會浪費時間。
Preauth 金鑰設計上會快速到期。預設有效時間為 one hour,且只能使用 one time。如果 tailscale up 拒絕該金鑰,請直接在伺服器上產生新的金鑰,不要修改用戶端的任何設定。
Node 金鑰則是長期有效的部分。config.yaml 的 node 區段會設定 expiry: 0;0 表示沒有預設到期時間,但仍可能套用明確指定的用戶端到期時間或 OIDC 到期時間。已標記的節點不受自動到期影響,但管理員仍可明確使其到期。若要為未標記的註冊設定 180-day 預設期限,請設定 expiry: 180d;用戶端要求的到期時間優先,OIDC 也可能覆寫預設值。變更此設定不會追溯修改現有註冊。具有到期時間的節點必須在期限前使用 sudo tailscale up --login-server https://headscale.example.com --force-reauth 重新驗證;無人重新驗證的無頭伺服器,會在其註冊到期時從網路中斷線。
有人遺失筆電時,請手動處理。sudo headscale nodes list 會提供該節點的 ID,接著 sudo headscale nodes expire -i 3 會將該節點登出,sudo headscale nodes delete -i 3 則會將其從網路中完全移除。
備份與升級
/var/lib/headscale 與 /etc/headscale 合起來就是整台伺服器。複製這些檔案前先停止服務,因為 SQLite 可能仍有寫入作業,服務運作期間複製資料庫可能導致內容不一致。
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-*.tgz將這兩個檔案移出伺服器。檔案內含私密金鑰與所有註冊資料,因此應比照伺服器本身妥善保護。來自 VPS 的 restic 備份說明如何定期執行加密備份。
升級流程與安裝相同:下載新的 .deb、sudo apt install ./headscale.deb,然後重新啟動,再次執行 is-active 與 /health 檢查。自 0.29 起,升級路徑受到嚴格限制。不得略過次要版本,也不得降級至較舊的次要版本。每次僅升級一個次要版本,每個步驟前先建立備份,並先閱讀該版本的發行說明,因為同一版本變更了 ACL 政策行為,也移動了數個設定金鑰。Version 0.29.6 也包含憑證驗證與相依套件的安全性更新,因此現有的 0.29 安裝應升級至此修補版本。
FAQ
安裝 .deb 後,headscale 是否已立即執行?
是。套件安裝完成後,會立即啟動服務,使用隨附的 /etc/headscale/config.yaml;該服務僅監聽 loopback,並將 server_url 設為 http://127.0.0.1:8080。全新安裝不會啟用服務,因此執行 sudo systemctl enable headscale 前,服務不會在開機時啟動。隨附的位址對用戶端沒有作用,因此請編輯 server_url、listen_addr 和 base_domain,接著執行 sudo systemctl restart headscale,並使用 sudo systemctl is-active headscale 確認。這個步驟必須使用 restart:headscale 只會在啟動時讀取檔案,而對已在執行的服務執行 enable --now 只會啟用開機自動啟動,不會重新啟動服務,因此舊值仍會生效。若重新啟動後失敗,sudo journalctl -u headscale -n 50 --no-pager 會指出問題所在;在此階段,幾乎總是 YAML 錯誤,因為 headscale 會在綁定埠之前解析完整檔案。
我是否仍需在各台機器上安裝一般的 Tailscale 用戶端?
是。Headscale 只取代控制伺服器。每個節點都執行 Tailscale 提供的官方用戶端,並使用 sudo tailscale up --login-server https://headscale.example.com 將其指向你的伺服器。標準用戶端已提供這個旗標,因此不需要修補或重新建置。
我的網路流量會經過 headscale 伺服器嗎?
通常不會。Headscale 協調網路並分配金鑰與位址,資料路徑則由 WireGuard 在節點之間直接建立。只有在兩個節點無法直接連線,改用 DERP relay 時,流量才會繞行;使用隨附設定時,這些 relay 是 Tailscale 的公開 relay。請在節點上執行 tailscale status,查看指定 peer 是 direct,還是使用 relay。
為什麼我的節點完成註冊後仍維持離線?
若節點出現在 headscale nodes list 中,卻始終不上線,請檢查其控制連線,包括 reverse proxy。該連線是以 POST 傳送並包含 Upgrade: tailscale-control-protocol 標頭的 HTTP upgrade;除非明確使用 proxy_set_header 轉送 Upgrade 和 Connection 標頭,否則 nginx 會捨棄這些標頭。上述設定會透過 map $http_upgrade $connection_upgrade 區塊完成轉送。捨棄這些標頭也會阻止新節點註冊。Caddy 不需額外設定即可轉送這些內容,因此可快速用來確認問題是否出在 proxy。
headscale 是否需要網域名稱和 TLS?
就這個設定而言,需要:請使用網域名稱和 HTTPS。憑證也可以涵蓋裸 IP 位址,但設定檔明確指出 DERP 需要 TLS。網域搭配 Caddy 約需五分鐘即可完成,並提供能自動續期的 HTTPS endpoint。Plain HTTP 也可使用,但 headscale 文件強烈建議在正式環境中於 port 443 使用 HTTPS,並指出 Tailscale 用戶端在某些情況下會假設 port 443 使用 HTTPS。