在 CentOS Stream 10 上自建 Headscale,并接入 Tailscale 客户端
一、背景说明
很多人说“自建 Tailscale 服务端”,严格来说,通常指的是:
- 使用 Headscale 作为自建控制平面
- 使用 Tailscale 官方客户端接入 Headscale
也就是说:
- Tailscale 官方 SaaS 控制面:不是直接自建这一套
- Headscale:开源的自建兼容控制平面
- Tailscale 客户端:Windows / macOS / Linux / iOS / Android 都可以接入
这套方案适合:
- 家庭设备组网
- 公司内网穿透
- 多设备互联
- 远程桌面、SSH、NAS 管理
- 不想依赖官方控制面
二、环境说明
本文示例环境:
- 服务端系统:CentOS Stream 10
- 容器运行时:Podman
- 反向代理:Nginx
- 控制面:Headscale
- 客户端:Tailscale
三、部署目标
最终实现以下效果:
- Headscale 运行在容器中
- Nginx 提供 HTTPS 入口
- 客户端通过
--login-server=https://你的域名接入 - 节点支持长期在线
- 支持查看节点、生成预授权 key、后续多设备接入
四、目录规划
建议统一使用如下目录:
/opt/headscale/
├── config/
│ ├── config.yaml
│ └── acl.hujson
└── lib/
创建目录:
sudo mkdir -p /opt/headscale/config
sudo mkdir -p /opt/headscale/lib
五、安装 Podman 和 Nginx
sudo dnf -y install podman nginx
sudo systemctl enable --now nginx
如果系统启用了防火墙,放行端口:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --permanent --add-port=3478/udp
sudo firewall-cmd --reload
说明:
80/tcp:HTTP443/tcp:HTTPS3478/udp:STUN / DERP 相关端口
六、Headscale 配置文件
创建 /opt/headscale/config/config.yaml
server_url: https://你的域名
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
tls_cert_path: ""
tls_key_path: ""
noise:
private_key_path: /var/lib/headscale/noise_private.key
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
derp:
server:
enabled: true
region_id: 999
region_code: "custom"
region_name: "Headscale DERP"
verify_clients: true
stun_listen_addr: "0.0.0.0:3478"
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
ipv4: 你的公网IPv4
urls:
- https://controlplane.tailscale.com/derpmap/default
paths: []
database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000
log:
level: info
format: json
node:
expiry: 0
policy:
mode: file
path: /etc/headscale/acl.hujson
dns:
magic_dns: true
base_domain: tail.example.com
override_local_dns: true
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
split: {}
search_domains: []
extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false
randomize_client_port: false
taildrop:
enabled: true
关键说明
1. server_url
写成你最终对外提供服务的 HTTPS 地址,例如:
server_url: https://hs.example.com
2. listen_addr
在容器场景下,建议:
listen_addr: 0.0.0.0:8080
不要写成容器内部的 127.0.0.1:8080,否则宿主机端口映射后可能无法访问。
3. node.expiry: 0
这项很关键,表示禁用节点过期。
这样设备首次接入后,后续重启不会因为节点过期而要求重新登录。
4. base_domain
必须是一个合法 FQDN,并且不要和 server_url 使用同一个域名。例如:
server_url:https://hs.example.combase_domain:tail.example.com
5. policy.path
这里指定 ACL 文件路径,必须和容器挂载路径保持一致。
七、ACL 配置文件
创建 /opt/headscale/config/acl.hujson
{
"tagOwners": {
"tag:server": ["root@"],
"tag:router": ["root@"],
"tag:exit": ["root@"]
},
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
],
"autoApprovers": {
"routes": {
"192.168.1.0/24": ["tag:router"]
},
"exitNode": ["tag:exit"]
}
}
说明
- 这是一个全放通的起步 ACL,适合先验证网络互通
tagOwners中的root@要替换成你实际使用的 Headscale 用户- 后续可以再细化权限
八、启动 Headscale 容器
sudo podman run -d \
--name headscale \
--restart=always \
-p 127.0.0.1:8080:8080 \
-p 3478:3478/udp \
-v /opt/headscale/config:/etc/headscale:Z \
-v /opt/headscale/lib:/var/lib/headscale:Z \
docker.io/headscale/headscale:latest \
serve
说明:
127.0.0.1:8080:8080:仅暴露给本机,供 Nginx 反代3478:3478/udp:开放 STUN / DERP
查看运行状态:
podman ps -a
podman logs --tail=50 headscale
本机健康检查:
curl http://127.0.0.1:8080/health
如果正常,返回:
{"status":"pass"}
九、Nginx 反向代理配置
创建 /etc/nginx/conf.d/headscale.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name 你的域名;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name 你的域名;
ssl_certificate /etc/letsencrypt/live/你的域名/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/你的域名/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
}
}
为什么一定要加 Upgrade / Connection
Headscale 在反代后需要正确处理 WebSocket / TS2021 相关请求。
如果 Nginx 没有透传 Upgrade 头,客户端可能出现:
- 登录失败
/machine/register返回 500- 日志提示
No Upgrade header in TS2021 request
检查配置并重载:
sudo nginx -t
sudo systemctl reload nginx
十、HTTPS 证书
可以使用 Let’s Encrypt + Certbot。
申请成功后,测试:
curl https://你的域名/health
如果正常,返回:
{"status":"pass"}
这说明:
- 域名解析正常
- HTTPS 正常
- Nginx 反代正常
- Headscale 已经可公网访问
十一、创建用户
查看现有用户:
podman exec -it headscale headscale users list
创建用户:
podman exec -it headscale headscale users create root
再次查看:
podman exec -it headscale headscale users list
注意:
- 某些命令用的是数字 ID
- 不是所有地方都接受用户名字符串
十二、生成预授权 Key
生成可复用、24 小时有效的 key:
podman exec -it headscale headscale preauthkeys create --user 1 --reusable --expiration 24h
说明:
--user 1中的1是用户数字 ID,不是用户名--expiration 24h表示 key 只用于首次注册设备时的有效期- 已经接入成功的设备,后续在线主要依赖 node key,不会因为这把 preauth key 过期就失联
十三、客户端接入
1. 首次接入
tailscale up --login-server=https://你的域名 --authkey=你的预授权key
2. 指定设备名
设备名必须符合 DNS label 规则:
- 不能有空格
- 不能有中文
- 不能有特殊字符
- 推荐全小写
例如:
tailscale up --login-server=https://你的域名 --authkey=你的预授权key --hostname=macbookpro
或者已接入后修改:
tailscale set --hostname=macbookpro
3. 查看客户端状态
tailscale status
tailscale ip
tailscale status --json
十四、服务端查看节点
查看节点列表:
podman exec -it headscale headscale nodes list
示例输出:
ID | Hostname | Name | User | IP addresses | Connected | Expired
1 | macbookpro | macbookpro | root | 100.64.0.1, fd7a:...::1 | online | no
常用观察点:
Connected: 是否在线Expired: 是否过期IP addresses: 节点分配到的地址User: 节点归属用户
十五、常用命令汇总
Headscale 侧
查看容器
podman ps -a
查看日志
podman logs --tail=50 headscale
podman logs -f headscale
查看健康状态
curl http://127.0.0.1:8080/health
curl https://你的域名/health
配置校验
podman exec -it headscale headscale configtest
用户管理
podman exec -it headscale headscale users list
podman exec -it headscale headscale users create root
预授权 key
podman exec -it headscale headscale preauthkeys create --user 1 --reusable --expiration 24h
节点查看
podman exec -it headscale headscale nodes list
重启容器
podman restart headscale
Nginx 侧
检查配置
nginx -t
重载配置
systemctl reload nginx
查看状态
systemctl status nginx --no-pager
客户端侧
首次接入
tailscale up --login-server=https://你的域名 --authkey=你的预授权key
指定设备名
tailscale set --hostname=macbookpro
查看状态
tailscale status
tailscale ip
tailscale status --json
查看后台日志
tailscale debug daemon-logs
注销
tailscale logout
十六、几个容易踩的坑
1. ACL 文件没有挂载到容器
如果日志报:
open /etc/headscale/acl.hujson: no such file or directory
说明容器里看不到 ACL 文件,要检查目录挂载是否正确。
2. 容器里 listen_addr 配成 127.0.0.1
如果 Headscale 运行在容器里,listen_addr 不应写成容器内部的 loopback,否则主机映射后访问可能失败。
推荐:
listen_addr: 0.0.0.0:8080
3. Nginx 未透传 Upgrade 头
如果日志报:
No Upgrade header in TS2021 request
通常说明 Nginx 没有正确代理 WebSocket/Upgrade 请求。
4. --user 传了用户名而不是数字 ID
有些 Headscale CLI 参数要求的是用户数字 ID,不是用户名。
5. 设备名带空格
下面这种会报错:
tailscale up --hostname="MacBook Pro"
因为空格不是合法 DNS label。
正确示例:
tailscale up --hostname=macbookpro
十七、关于 key 过期
很多人担心:
如果 preauth key 过期了,设备下次重启还能不能连?
答案是:
- 新设备注册:受 preauth key 有效期限制
- 已经注册成功的老设备:后续主要依赖 node key
- 配了
node.expiry: 0后,老设备一般不会因为节点过期而掉线
也就是说,preauth key 不是每次开机都要重新用的。
- 感谢你赐予我前进的力量

