之前 CyberQ 介紹過的 Tailscale ,它讓 WireGuard 這種 VPN 連線多裝置到一個可彼此互聯的虛擬內網這件事,變得幾乎零設定,但它的控制伺服器始終是在 Tailscale 公司的雲端執行。對於重視資料主權、需要符合稽核要求,或單純不想被席次計價綁住的使用者來說,Headscale 提供了另一條路。這篇文章說明 Headscale 是什麼、為什麼 QNAP NAS 是合適的宿主,並提供一套可以直接套用的 Container Station 部署流程。
Headscale 是什麼
Tailscale 的架構可以拆成兩層。資料平面是節點之間的 WireGuard 點對點加密通道,控制平面則負責交換公鑰、分配 100.64.0.0/10 網段位址、下發 ACL 與 DNS 設定,以及協調 NAT 穿透。Tailscale 的用戶端與大部分元件都是開源的,唯獨控制伺服器沒有公開。
Headscale 則是由社群維護、以 BSD-3 授權釋出的 Tailscale 控制伺服器開源實作,目標明確定位在自架使用者、愛好者與小型開源組織。一個 Headscale 實例只服務一個 tailnet,不支援多租戶。專案主要維護者之一任職於 Tailscale,並獲准在工作時間內貢獻,官方與社群之間維持一種公開的協作關係。
截至本文撰寫時,Headscale 最新穩定版為 v0.29.3,於 2026 年 7 月 29 日釋出,最低支援的 Tailscale 用戶端版本為 v1.80.0。GitHub 專案累積超過 43,000 顆星。
為什麼要用 Headscale 自建 Tailscale Server ?
資料主權與稽核需求
控制伺服器掌握整個網路的節點清單、公鑰、ACL 政策與登入紀錄。這些資料放在第三方雲端,對多數個人用途無妨,但在 ISO 27001 或 ITGC 稽核情境下,「網路存取控制的決策點在哪裡」是會被問到的題目。Headscale 讓這個決策點回到自己的機房,所有節點註冊、金鑰輪替與政策變更的紀錄都留在本地資料庫,可直接納入日誌管理流程。
席次成本
Tailscale 在 2026 年 4 月 8 日調整方案,免費的 Personal 方案開放至 6 位使用者,付費方案改為以指派席次計費,使用者一旦加入 tailnet 便佔用一個席次。第三方整理的定價資料顯示 Standard 方案約為每位使用者每月 8 美元,Premium 約 18 美元。對於家庭或十人以下的小團隊,超過 6 位使用者就會進入付費區間。Headscale 沒有使用者數量限制,成本只有 NAS 本身的電費與維護時間。
不依賴外部服務可用性
自建控制平面後,節點註冊與政策下發不再受外部服務中斷影響。需要留意的是,Tailscale 的資料平面本來就是點對點,控制伺服器短暫離線時既有連線仍可維持,但新節點無法加入,金鑰到期後也無法續期。
功能完整度
Headscale 支援 Tailscale 的核心功能,包括 MagicDNS、ACL、預授權金鑰、OIDC 單一登入、子網路路由與出口節點、暫時性節點、Taildrop 檔案傳輸,以及內建 DERP 中繼伺服器。v0.29 系列新增了 autogroup:member、autogroup:tagged 與實驗性的 autogroup:self,並支援 SSH 政策中的 check 動作。
為什麼放在 QNAP NAS 上
控制伺服器的負載極輕。它不轉送任何使用者流量,只在節點上線、政策變更或金鑰輪替時做少量協調工作。這種常駐、低負載、需要持久儲存的服務,正好是 7 x 24 運作 NAS 的強項。
QNAP 的 Container Station 內建 Docker Engine 與 Compose V2,可以直接以 YAML 建立多容器應用,容器資料預設放在 /share/Container 下,重開機後仍會保留。相較於另外付費租一台雲端主機 VPS,既有的 NAS 設備通常已經全天候運轉,也已經有 RAID 與快照保護,把 Headscale 的 SQLite 資料庫放在上面,備份策略可以直接沿用。
QNAP 官方另有 Tailscale QPKG 套件,NAS 本身可以同時是控制伺服器與網路中的一個節點,對外提供子網路路由,讓 LAN 內無法安裝 Tailscale 的裝置也能被存取。
部署前準備
需要準備的項目如下。
一台可執行 Container Station 的 QNAP NAS,本文以 QuTS hero h6.0.2.3591 搭配 Container Station 為例
一個公開網域,例如 headscale.example.com,DNS A 紀錄指向家中或機房的公網 IP
路由器上將 80/TCP、443/TCP 與 3478/UDP 轉發至 NAS
若 NAS 的 443 已被 QTS 管理介面佔用,先在控制台將管理埠改到其他埠號
關於連接埠,有三點要說清楚。443/TCP 同時承載控制協定與 DERP 中繼,是必要的。3478/UDP 給內建 DERP 的 STUN 使用,它無法被反向代理,必須直接對外。80/TCP 則是給 Let’s Encrypt 的 HTTP-01 挑戰用,如果只轉發 443,Caddy 會改用 TLS-ALPN-01 挑戰,那也可行,但保留 80 會讓憑證申請的成功率高一些。
控制伺服器必須以 HTTPS 提供服務,Tailscale 用戶端不接受純 HTTP 的 login server。本文使用 Caddy 作為反向代理並自動申請 Let’s Encrypt 憑證。
反向代理的兩個特殊要求
這一段值得先看,因為它會決定你能不能用現成的代理方案。
Tailscale 控制協定用 POST 方法做連線升級,而且 Upgrade 標頭的值是 tailscale-control-protocol(DERP 則是 Upgrade: derp),都不是標準的 Upgrade: websocket。任何只處理標準 websocket 值的代理都會把這個升級吃掉,症狀是用戶端執行 tailscale up 之後卡住,或收到 400 / 502。
實務上最常踩到的是 Cloudflare。Cloudflare 的 proxy(橘雲)與 Cloudflare Tunnel 都不支援這兩個自訂 upgrade 值,所以控制伺服器的網域只能用灰雲純 DNS。這代表你的公網 IP 會直接暴露在 DNS 上,而且 Let’s Encrypt 憑證會讓這個子網域名稱進入公開的憑證透明度日誌,取難猜的名字藏不住。這是自架必須接受的代價,值得在開始之前就想清楚。
Caddy 對 upgrade 是泛型處理,所以本文的組合可以正常運作。Nginx 需要正確設定 upgrade header 對應與 proxy_buffering off,Apache 需要 upgrade=any。
部署步驟
建立目錄結構
透過 SSH 登入 NAS,建立設定與資料目錄。
mkdir -p /share/Container/headscale/config
mkdir -p /share/Container/headscale/data
mkdir -p /share/Container/headscale/caddyBASH
撰寫 Headscale 設定檔
將以下內容存為 /share/Container/headscale/config/config.yaml,並把 server_url、base_domain 與 derp.server.ipv4 改成自己的值。
server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
# metrics 與 gRPC 只綁 container loopback,等同對外關閉
metrics_listen_addr: 127.0.0.1:9090
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false
# 只信任同一個 compose 網路裡的 Caddy,讓 log 記到真實客戶端 IP。
# 留空的話 Caddy 送的 True-Client-IP 會被忽略,所有連線都會記成代理的位址;
# 亂填的話則等於讓客戶端可以偽造自己的來源 IP。
trusted_proxies:
- 172.31.250.0/24
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
derp:
server:
enabled: true
region_id: 999
region_code: "qnap"
region_name: "QNAP NAS DERP"
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
# 對外公告用,必須填公網 IP 而不是 LAN IP。
# 沒有 IPv6 就整行不要寫,填空字串會讓解析失敗
ipv4: 203.0.113.10
# headscale 用自己的節點資料庫驗證,不需要本機 tailscaled。
# 保持 true,否則自家 DERP 會變成任何人都能用的開放中繼
verify_clients: true
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
auto_update_enabled: true
update_frequency: 3h
disable_check_updates: true
node:
expiry: 0
ephemeral:
inactivity_timeout: 30m
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
dns:
magic_dns: true
base_domain: ts.example.com
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
policy:
mode: file
path: /etc/headscale/acl.hujson
log:
format: text
level: infoCyberQ 實作時,幾個重點說明如下
derp.server.enabled 打開內建 DERP 中繼,讓 NAT 穿透失敗時流量仍有備援路徑,同時保留 Tailscale 官方 DERP 清單作為補充。update_frequency 設為 3h 是 v0.29 升級指南的建議值。
base_domain 必須與 server_url 的網域不同。精確的限制是:server_url 的主機名稱不可以位於 base_domain 之下。以本文為例,headscale.example.com 不在 ts.example.com 底下,合法;但如果把 base_domain 寫成 example.com,headscale 會拒絕啟動。另外 base_domain 只在 tailnet 內部解析,不需要也不應該為它建立任何公開 DNS 記錄。
trusted_proxies 這一項的網段要對應到下面 compose 裡固定的 bridge 子網。少了它,headscale 的日誌裡每一個客戶端都會顯示成 Caddy 容器的位址,出事時追不到來源。
注意 v0.29 移除的設定鍵。 網路上不少舊教學仍帶著
randomize_client_port與ephemeral_node_inactivity_timeout,這兩個在 v0.29.3 都已經移除(後者併入node.ephemeral.inactivity_timeout)。留著舊鍵 headscale 會直接 FATAL 拒絕啟動,而錯誤訊息只出現在容器日誌裡,從docker ps只看得到容器不斷 Restarting。建議動手前先抓一次上游範本比對頂層鍵:curl -fsSL https://raw.githubusercontent.com/juanfont/headscale/v0.29.3/config-example.yaml
撰寫 Caddyfile
存為 /share/Container/headscale/caddy/Caddyfile。
{
email [email protected]
# 首次上線建議先開 staging 跑一輪,確認 challenge 流程沒問題再註解掉,
# 避免失敗時撞上 Let's Encrypt 正式環境的速率限制
# acme_ca https://acme-staging-v02.api.letsencrypt.org/directory
servers {
# 控制協定走 HTTP/1.1 upgrade,用不到 h3,順便省掉 443/udp 的曝露
protocols h1 h2
}
}
headscale.example.com {
# 不要對 headscale 加 encode 或壓縮,會干擾 upgrade 之後的原始串流
reverse_proxy headscale:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}reverse_proxy headscale:8080 走的是 compose 內部網路的 Docker embedded DNS。不要寫成 NAS 的 LAN IP,bridge 網路的容器連回宿主機自己的 LAN 位址在 QNAP 上會逾時,這是很容易卡住的一個點。
Caddy 會自動申請並續期憑證,前提是 80/TCP 或 443/TCP 能從網際網路抵達 NAS。
在 Container Station 建立應用
開啟 Container Station,進入「應用程式」,點選「建立」,貼上以下 Compose 內容。
name: headscale
services:
headscale:
image: docker.io/headscale/headscale:0.29.3
container_name: headscale
restart: unless-stopped
command: serve
volumes:
- /share/Container/headscale/config:/etc/headscale
- /share/Container/headscale/data:/var/lib/headscale
- /share/Container/headscale/run:/var/run/headscale
ports:
# 只曝露 STUN。8080 / 9090 / 50443 一律不 publish
- "3478:3478/udp"
networks: [headscale-net]
caddy:
image: docker.io/library/caddy:2-alpine
container_name: headscale-caddy
restart: unless-stopped
depends_on: [headscale]
volumes:
- /share/Container/headscale/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- /share/Container/headscale/caddy/data:/data
- /share/Container/headscale/caddy/config:/config
ports:
- "80:80"
- "443:443"
networks: [headscale-net]
networks:
headscale-net:
driver: bridge
ipam:
config:
# 固定子網,config.yaml 的 trusted_proxies 要對得上。
# 選一段沒有被既有容器佔用的網段
- subnet: 172.31.250.0/24映像檔版本建議釘在明確的版本號,甚至進一步鎖 digest,避免自動更新時跨過需要手動遷移的版本。Headscale 官方明確要求跨版本升級時逐一經過每個穩定版,並在升級前備份資料庫。
如果 NAS 上跑著 Watchtower 之類的自動更新工具,務必把 headscale 排除。 預設沒有標籤過濾的 Watchtower 會更新機器上所有容器,一旦它把 headscale 換成新的主版本,資料庫會走一次不可逆的 migration。在兩個服務都加上
com.centurylinklabs.watchtower.enable: "false"標籤即可排除。
按下「驗證」確認 YAML 無誤後建立應用。等待數十秒,在 Caddy 的日誌中應可看到憑證申請成功的訊息。
建立使用者與預授權金鑰
Headscale 沒有官方 Web UI,日常管理透過 CLI 進行。
官方映像是 distroless,沒有 shell,所以 docker exec headscale sh 一定會失敗,但可以直接執行裡面的二進位檔:
docker exec headscale headscale users create myuser
docker exec headscale headscale users list記下使用者的 ID,接著建立 24 小時內有效的預授權金鑰。
docker exec headscale headscale preauthkeys create --user 1 --expiration 24hv0.29 起 preauthkeys 系列指令改以 數字 ID 操作,--user 不接受使用者名稱。另外 preauthkeys list 這個子指令沒有 --user 旗標,直接列出全部。
金鑰建議短效且不加 --reusable,一把金鑰配一台裝置,用完以 preauthkeys expire -i <id> 作廢。
讓 QNAP NAS 自己加入網路
安裝 Tailscale QPKG 後,透過 SSH 使用 CLI 指向自建的控制伺服器。QPKG 的網頁介面只能連線至官方伺服器,自訂 login server 必須走命令列。
export PATH=$PATH:$(getcfg SHARE_DEF defVolMP -f /etc/config/def_share.info)/.qpkg/Tailscale/
tailscale up \
--login-server=https://headscale.example.com \
--authkey=<預授權金鑰> \
--advertise-routes=192.168.1.0/24 \
--accept-dns=false--advertise-routes 讓 NAS 成為子網路路由器。--accept-dns=false 對 NAS 是硬性要求,讓 tailscaled 改寫 NAS 的 /etc/resolv.conf 有機會打斷 QTS 服務與其他容器的名稱解析。
回到控制伺服器核准路由。
docker exec headscale headscale nodes list
docker exec headscale headscale nodes list-routes
docker exec headscale headscale nodes approve-routes --identifier 1 --routes 192.168.1.0/24一個很容易誤判的錯誤。 如果預授權金鑰建立時帶了
--tags,用戶端就不可以再給--advertise-tags,否則會被拒絕註冊並回報requested tags [...] are invalid or not permitted。這個訊息會讓人以為 ACL 的tagOwners寫錯,實際上 ACL 完全正常,只是同一個標籤被要求了兩次。標籤由金鑰帶或由用戶端宣告,二選一。
加入其他裝置
Windows、macOS 與 Linux 用戶端可直接下指令。
tailscale up --login-server=https://headscale.example.com \
--accept-routes --accept-dns=true如果沒有帶 --authkey,用戶端會顯示一段註冊網址,開啟後取得 machine key,回到 NAS 執行註冊。
docker exec headscale headscale nodes register --user myuser --key <machine-key>--accept-routes 讓這台裝置能經由 NAS 存取整個區網,--accept-dns=true 則啟用 MagicDNS。要注意的是,已經在該區網內的機器不要加 --accept-routes,那會把自己所在網段的路由指向隧道造成迴圈。
另外 --accept-dns=true 會把系統 DNS 指向 MagicDNS,非 tailnet 的查詢再轉給 dns.nameservers.global 設定的伺服器。如果你區網裡跑著 Pi-hole 或 AdGuard Home,這個設定會讓它們完全不在解析路徑上,等於在家也失去過濾。想保留過濾就把 nameservers.global 指向自己的 DNS 伺服器。
iOS 與 Android 的 Tailscale 官方 App 在登入畫面的選單中提供「使用自訂協調伺服器」選項,填入 Headscale 網址即可。
設定 ACL
Headscale 的 ACL 語法與 Tailscale 相容,格式是 HuJSON(可以寫註解的 JSON)。以下範例讓一般成員可以互連,並限制標記為 tag:iot 的裝置只能被存取而不能主動連線。
{
// 宣告誰有權把 tag:iot 指派給裝置。
// 只有這一段的話不會產生任何存取限制,限制要寫在 acls 裡
"tagOwners": {
"tag:iot": ["myuser@"],
},
"acls": [
// 一般成員之間全開
{
"action": "accept",
"src": ["autogroup:member"],
"dst": ["*:*"],
},
// tag:iot 的裝置只允許回應既有連線,不給它任何主動連出的規則。
// ACL 預設拒絕,所以「不寫規則」就等於禁止它發起連線
],
}這裡有個常見誤解要點出來:tagOwners 只是宣告誰有權指派標籤,它本身不產生任何存取限制。真正的限制來自 acls 陣列,而 Tailscale 系的 ACL 是預設拒絕,所以要讓 tag:iot 不能主動連線,做法是不要在 acls 裡給它任何 src 規則,而不是在 tagOwners 裡宣告一下就好。
將檔案存為 /share/Container/headscale/config/acl.hujson,在 config.yaml 的 policy.path 填入 /etc/headscale/acl.hujson,重啟容器即生效。
改 ACL 之後一定要看容器日誌。 file 模式下語法錯誤會讓 headscale 直接拒絕啟動,改完就走的話,下次才發現整個控制平面已經掛了。
維運建議
備份 /share/Container/headscale/data 整個目錄,其中包含 SQLite 資料庫與兩把私鑰。SQLite 開了 WAL,要取得一致快照必須先停容器再打包。建議直接納入 NAS 的快照排程
一併備份 caddy/data,裡面有 ACME 帳號金鑰與憑證,重簽會消耗 Let’s Encrypt 的速率限制
升級前閱讀 GitHub Releases 的 breaking changes。v0.28 已移除 0.25 以前的資料庫遷移,v0.29 進一步移除 0.28 以前的遷移
若需要圖形介面,社群專案 headscale-ui 與 headplane 可作為補充,兩者皆非官方維護。強烈建議不要把管理介面掛在對外的網域上,那等於把整個 tailnet 的控制平面開在公網。比較好的做法是只綁區網位址,或更進一步只綁 tailnet 位址,這樣要管理就得先連上 VPN
Headscale 的 metrics 端點預設監聽 9090,可接入既有的 Prometheus。記得綁在 loopback 而不是 0.0.0.0
公網 IP 若會變動,derp.server.ipv4 與 DNS A 記錄要同步更新,否則 DERP region 會公告一個錯誤位址,客戶端 tailscale netcheck 會顯示該 region 不可達
怎麼確認真的部署成功 ?
# 憑證:subject 要是你的網域,issuer 要是 Let's Encrypt
openssl s_client -connect headscale.example.com:443 -servername headscale.example.com \
</dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
# 端點
curl -fsS https://headscale.example.com/health # {"status":"pass"}
curl -fsSI https://headscale.example.com/derp/probe # 200
curl -sI http://headscale.example.com/ # 308 轉 https控制協定本身只能靠用戶端驗證,tailscale up 能註冊成功且 tailscale status 出現 100.64.x.x,就代表 POST 加自訂 upgrade 標頭有順利穿過反向代理。
DERP 與 STUN 建議在真正的外網測,例如手機熱點:
tailscale netcheck要看到 UDP: true、IPv4: yes, <你的出口IP>:port(代表 3478/udp 通),以及 DERP 延遲清單中出現你自己的 region 並有毫秒數(代表 443 上的 DERP relay 通)。
在區網內測試失敗通常只是路由器不支援 NAT hairpin,不代表部署有問題。如果希望區網裝置也能用同一個網域連進來,在區網的 DNS 伺服器加一筆 rewrite 指向 NAS 的 LAN IP 是最乾淨的做法,SNI 仍然對得上 Caddy 的憑證。
適合與不適合的情境
Headscale 適合家庭實驗室、小型工作室,以及對資料落地有明確要求的組織,單一 tailnet 的設計對這些場景足夠。
若組織需要多租戶、依賴 OIDC 群組直接對應 ACL、需要商業支援合約,或不願意承擔控制平面的維運責任,官方 Tailscale 仍是較穩妥的選擇。Headscale v0.28 的文件註明 OIDC 群組尚不能直接用於 ACL,需以標籤對應。
還有一項要誠實面對的取捨:自架之後,那個網域的 A 記錄會公開指向你的線路,子網域名稱也會出現在公開的憑證透明度日誌裡。對外開放的 443 在上線幾天內就會開始收到自動掃描。Headscale 的攻擊面本身很窄(註冊需要有效的預授權金鑰),但這個曝露是真實存在的,值得在按下部署之前先想清楚自己是否接受。
延伸閱讀
Headscale 官方文件 https://headscale.net/
Headscale GitHub Releases v0.29.3 https://github.com/juanfont/headscale/releases/tag/v0.29.3
Headscale CHANGELOG https://github.com/juanfont/headscale/blob/main/CHANGELOG.md
Tailscale 免費方案說明 https://tailscale.com/docs/account/manage-plans/free-plans-discounts
Tailscale 定價頁 https://tailscale.com/pricing
Tailscale QNAP 整合文件 https://tailscale.com/kb/1273/qnap
QNAP Container Station 3 使用教學 https://www.qnap.com/en/how-to/tutorial/article/how-to-use-container-station-3













