Headscale 配置参数详解
Headscale v0.29.5 配置文件逐段详解:server_url、prefixes、dns、policy、database 等核心参数含义与默认值,配「改值→重启→行为对比」实测(前缀收窄 IP 分配、MagicDNS 接管、ACL 拦截、metrics 暴露)与生产配置模板。
Headscale 配置参数详解
> 目标:逐段讲清 Headscale v0.29.5 主配置文件 /etc/headscale/config.yaml 的结构、关键参数含义与默认值,并用「改值 → 重启 → 行为对比」的实测方式验证 prefixes 地址分配、DNS 托管、ACL 策略、metrics 暴露等核心参数的真实行为。全部实测在 Ubuntu 22.04.5 + Headscale v0.29.5 环境完成,回显逐条照抄。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- Headscale:v0.29.5(安装方式:官方二进制 + systemd,安装过程见《Ubuntu 22.04 安装 Headscale 指南》)
- 配置文件:/etc/headscale/config.yaml(来源为 v0.29.5 tag 的官方 config-example.yaml,494 行)
- 数据目录:/var/lib/headscale(SQLite 库 db.sqlite、Noise 私钥 noise_private.key)
- 节点:两台 Ubuntu 22.04.5(node1 = 192.168.32.3,tailnet IP 100.64.0.1;node2 = 192.168.32.4,tailnet IP 100.64.0.2),Tailscale 客户端 1.104.1
- 控制面地址:http://192.168.32.2:8080
二、前置条件
- 已按《Ubuntu 22.04 安装 Headscale 指南》完成 v0.29.5 安装并确认服务运行(
systemctl is-active headscale返回 active); - 至少一台节点已注册(
headscale nodes list可见),便于验证参数变更对客户端的影响; - 服务端已安装 sqlite3 与 curl(参数验证时会用到);
- 理解一条总原则:Headscale 只在启动时读取配置,任何修改都要
systemctl restart headscale才生效;DNS/策略类变更还会异步下发到节点,必要时在节点上重启 tailscaled 强制刷新。
三、参数查看与修改方式
参数的查看、校验、修改、生效四步闭环如下。
查看当前生效配置(全量读取配置文件):
grep -nE '^[a-z_]+:' /etc/headscale/config.yaml
实测输出(顶层段落清单):
13:server_url: http://192.168.32.2:8080
19:listen_addr: 0.0.0.0:8080
24:metrics_listen_addr: 0.0.0.0:9090
34:grpc_listen_addr: 0.0.0.0:50443
40:grpc_allow_insecure: false
47:trusted_proxies: []
51:noise:
75:prefixes:
91:derp:
152:disable_check_updates: false
155:node:
194:database:
256:acme_url: https://acme-v02.api.letsencrypt.org/directory
259:acme_email: ""
262:tls_letsencrypt_hostname: ""
267:tls_letsencrypt_cache_dir: /var/lib/headscale/cache
272:tls_letsencrypt_challenge_type: HTTP-01
276:tls_letsencrypt_listen: ":http"
279:tls_cert_path: ""
280:tls_key_path: ""
282:log:
289:policy:
321:dns:
378:unix_socket: /var/run/headscale/headscale.sock
379:unix_socket_permission: "0770"
455:logtail:
461:taildrop:
467: enabled: false
477:auto_update:
修改配置前先做语法预检(不重启就能发现错误):
headscale configtest
实测输出(通过时仅打印数据库打开日志):
2026-10-17T22:08:10Z INF Opening database database=sqlite3 path=/var/lib/headscale/db.sqlite
配置错误的代价是服务循环重启(systemd 状态显示 activating),因此建议养成「先 configtest 再 restart」的习惯。修改与生效的标准流程:
# 1. 编辑参数(本文用 sed 演示,手工编辑等价)
sed -i 's|旧值|新值|' /etc/headscale/config.yaml
# 2. 语法预检
headscale configtest
# 3. 重启生效
systemctl restart headscale
# 4. 确认状态
systemctl is-active headscale
实测输出(restart 后):
active
四、参数总览
config.yaml 按段落组织,各段职责与本文实测覆盖情况:
| 段落 | 职责 | 关键参数 | 本文实测 |
|---|---|---|---|
| 顶层服务 | 对外地址与监听 | server_url / listen_addr / metrics_listen_addr / grpc_listen_addr | 实测(server_url 是接入正确性的根;metrics 改公网监听跨机抓取) |
| noise | 控制面与客户端通信加密 | noise.private_key_path | 已随安装验证(首启自动生成) |
| prefixes | tailnet IP 池 | prefixes.v4 / v6 / allocation | 实测(收窄 v4 前缀观察新节点分配) |
| derp | 中继与 STUN | derp.urls / derp.server.enabled | 结构实测(默认引用 Tailscale 公网 DERP 列表) |
| node | 节点生命周期 | node.expiry / node.ephemeral.inactivity_timeout / routes.ha | 参数说明(行为与运维见《Headscale 运维手册》) |
| database | 元数据存储 | database.type / sqlite.path / write_ahead_log | 已随备份恢复验证 |
| ACME/TLS | HTTPS 证书 | tls_cert_path / tls_letsencrypt_hostname | 参数说明 |
| log | 日志 | log.level / log.format | 参数说明 |
| policy | ACL/Grants 策略 | policy.mode / policy.path | 实测(文件模式 ACL 生效与拦截对比) |
| dns | MagicDNS 与解析下发 | dns.magic_dns / base_domain / override_local_dns / nameservers | 实测(开关对比 + 全置空对比 + 校验报错) |
| unix_socket | 本地 CLI 通道 | unix_socket / unix_socket_permission | 已随安装验证 |
| logtail/taildrop/auto_update | 云端日志/文件共享/客户端自动更新 | 各自 enabled | 参数说明 |
五、核心参数分类详解
5.1 server_url 与监听地址组
服务端身份与监听的四个参数决定了「客户端从哪进来、服务绑在哪」:
server_url: http://192.168.32.2:8080 # 客户端连接控制面的对外地址
listen_addr: 0.0.0.0:8080 # HTTP 服务监听地址
metrics_listen_addr: 127.0.0.1:9090 # /metrics 与 /debug 监听地址(空值关闭)
grpc_listen_addr: 0.0.0.0:50443 # 远程 gRPC 管理监听地址
grpc_allow_insecure: false # 是否允许无 TLS 的明文 gRPC
实测行为对比(改 metrics_listen_addr 从 127.0.0.1:9090 为 0.0.0.0:9090 后重启):
sed -i 's|metrics_listen_addr: 127.0.0.1:9090|metrics_listen_addr: 0.0.0.0:9090|' /etc/headscale/config.yaml
systemctl restart headscale && systemctl is-active headscale
curl -s -m 3 http://127.0.0.1:9090/metrics | head -3
实测输出(本机抓取成功):
# HELP go_gc_duration_seconds A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 6.8741e-05
从另一台节点(192.168.32.3)跨机抓取同样成功:
# HELP go_gc_duration_seconds A summary of the wall-time pause (stop-the-world) duration in garbage collection cycles.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 6.8741e-05
关键结论:metrics 改 0.0.0.0 后即可被 Prometheus 跨机采集(Prometheus 侧抓取与告警配置见《Headscale 运维手册》);生产中若无独立监控网段,建议保持 127.0.0.1 或用防火墙限制 9090。server_url 的实测要求在安装篇已验证:必须与客户端实际访问地址一致。gRPC 监听有个容易误判的现象:grpc_listen_addr 写了 0.0.0.0:50443,但未配置 TLS 证书(tls_cert_path 为空)且 grpc_allow_insecure=false 时,该端口并不会监听——ss -tlnp 只能看到 8080 与 9090:
LISTEN 0 4096 *:9090 *:* users:(("headscale",pid=3928,fd=14))
LISTEN 0 4096 *:8080 *:* users:(("headscale",pid=3928,fd=13))
本地 CLI 通过 unix_socket(/var/run/headscale/headscale.sock,权限 0770)连接,不走 gRPC,因此单机管理无需关注 50443。
5.2 prefixes:tailnet IP 池与分配策略
prefixes:
v4: 100.64.0.0/10 # IPv4 池,必须是 100.64.0.0/10(CGNAT)的子集
v6: fd7a:115c:a1e0::/48 # IPv6 池,必须是 Tailscale ULA 段的子集
allocation: sequential # sequential(顺序)或 random(随机)
💡 技术拓展:IP 分配策略不止 sequential 一种,两者对比——
策略 原理 取舍 sequential(默认) 从池内顺次分配下一个空闲 IP,可能出现空洞 地址可预测,便于人工排查 random 用 crypto/rand 从池内随机挑空闲 IP 地址不可预测,节点命名与 IP 无规律 本文与官方默认一致采用 sequential,便于在运维时按 IP 识别节点上线顺序。
官方注释明确:前缀必须是 CGNAT/ULA 标准段的子集,超出会「break in subtle, hard-to-debug ways」。实测验证「收窄前缀只影响新节点」——把 v4 从 100.64.0.0/10 收窄为 100.64.4.0/24 后重启,再注册一台新节点 node3(用预授权密钥):
sed -i 's|^ v4: 100.64.0.0/10| v4: 100.64.4.0/24|' /etc/headscale/config.yaml
systemctl restart headscale && systemctl is-active headscale
# node3 注册后查看节点列表
headscale nodes list
实测输出:
ID | Hostname | Name | MachineKey | NodeKey | User | Tags | IP addresses | Ephemeral | Last seen | Expiration | Connected | Expired
1 | node1 | node1 | [3yxWo] | [103CQ] | demo | | 100.64.0.1 | false | 2026-10-17 21:59:07 | N/A | online | no
| | | | | | | fd7a:115c:a1e0::1 | | | | |
2 | node2 | node2 | [I2g49] | [OoFbg] | demo | | 100.64.0.2 | false | 2026-10-17 21:59:07 | N/A | online | no
| | | | | | | fd7a:115c:a1e0::2 | | | | |
3 | node3 | node3 | [B3drU] | [6oAVY] | demo | | 100.64.4.1 | false | 2026-10-17 21:59:43 | N/A | online | no
| | | | | | | fd7a:115c:a1e0::3 | | | | |
两条结论:新节点 node3 拿到新前缀内首个地址 100.64.4.1;老节点 node1/node2 保持 100.64.0.1/0.2 不变(已分配地址不回收不重排),全网互通不受影响。规划地址池时即可利用该特性做「分批扩容、互不影响」。
5.3 derp:中继与 STUN
derp:
server:
enabled: false # 内嵌 DERP 开关(要求 server_url 为 https)
region_id: 999
region_code: "headscale"
region_name: "Headscale Embedded DERP"
verify_clients: true # 仅允许本控制面节点使用内嵌 DERP
stun_listen_addr: "0.0.0.0:3478" # STUN 打洞服务(UDP)
private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true
urls: # 外部 DERP 列表(JSON)
- https://controlplane.tailscale.com/derpmap/default
paths: [] # 本地 DERP 映射文件(YAML)
auto_update_enabled: true # 定期刷新 DERP 列表
update_frequency: 3h
要点:默认 urls 引用 Tailscale 官方 DERP 列表,节点打洞失败时即可走全球公网中继,零成本获得可用性;derp.server.enabled: true 才启用内嵌 DERP,此时官方文档要求 server_url 为 https(DERP 需要 TLS),并开放 3478/udp 的 STUN。默认值下实测服务端日志无内嵌 DERP 相关输出,节点 tailscale debug netmap 中可见下发的是 Tailscale 公网 DERP 区域(derp1h.tailscale.com 等)。完全不想要公网中继(纯内网互通)时,把 urls 清空并保持 server.enabled 为 false 即可。
5.4 dns:MagicDNS 与解析下发(重点实测段)
v0.29.5 的 dns 段完整结构(去注释):
dns:
magic_dns: true # MagicDNS 开关
base_domain: tailnet.example.com # MagicDNS 域名后缀,必须与 server_url 域名不同
override_local_dns: true # 是否强制接管节点本地解析
nameservers:
global: # 接管时下发给节点的全局 DNS
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
split: {} # 分域解析:域名 -> DNS 列表
search_domains: [] # 附加搜索域
extra_records: [] # 额外 A/AAAA 记录(Headscale 特有)
实验一:仅关闭 magic_dns 的实际效果。 把 magic_dns 改为 false 并重启服务端与客户端 tailscaled 后,检查客户端实际收到的 DNS 配置:
sed -i 's/magic_dns: true/magic_dns: false/' /etc/headscale/config.yaml
systemctl restart headscale
# 节点侧强制刷新后查看
tailscale debug netmap | grep -A 20 '"DNS"'
实测输出(node1 收到的 netmap DNS 段):
"DNS": {
"Domains": [
"tailnet.example.com"
],
"Resolvers": [
{
"Addr": "1.1.1.1"
},
{
"Addr": "1.0.0.1"
},
{
"Addr": "2606:4700:4700::1111"
},
{
"Addr": "2606:4700:4700::1001"
}
]
},
节点 /etc/resolv.conf 与域名解析实测(magic_dns 为 false 时):
nameserver 100.100.100.100
nameserver fd7a:115c:a1e0::53
search tailnet.example.com
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
实测结论:仅关 magic_dns 时,控制面仍向节点下发搜索域(Domains)与全局解析器(Resolvers,来自 override_local_dns: true + global 列表),节点 /etc/resolv.conf 依旧被 tailscale 接管(指向 100.100.100.100),tailnet 内域名解析仍正常。magic_dns: false 只关「以 MagicDNS 形式注册主机名」的能力,不解除 DNS 接管。
实验二:完全解除 DNS 接管。 按官方注释「想让 Headscale 不再管理 DNS,dns 段所有字段置空」,将 magic_dns: false、base_domain: ""、override_local_dns: false、nameservers.global: [] 同时设置,重启后节点侧 /etc/resolv.conf 恢复系统默认:
# Based on host file: '/etc/resolv.conf' (internal resolver)
# ExtServers: [host(127.0.0.53)]
# Overrides: []
# Option ndots from: internal
实验三:配置校验报错。 保留 override_local_dns: true 但清空 global 列表时重启,服务进入循环重启:
Error: initializing: loading configuration: Fatal config error: dns.nameservers.global must be set when dns.override_local_dns is true
Oct 17 21:56:49 hs-controller systemd[1]: headscale.service: Scheduled restart job, restart counter is at 2.
恢复 global 列表后重启,服务回到 active,节点重新被接管:
nameserver 100.100.100.100
nameserver fd7a:115c:a1e0::53
search tailnet.example.com
DNS 段的三条实用结论:只关 MagicDNS 用 magic_dns: false;要彻底不接管必须全段置空;override_local_dns: true 时 global 必填否则拒绝启动。base_domain 实测中节点间以 node2.tailnet.example.com 互访正常(安装篇 7.5 节)。
5.5 policy:ACL 策略(重点实测段)
policy:
mode: file # file(HuJSON 文件)或 database
path: /etc/headscale/acl.hujson # file 模式下的策略文件路径
编写最小 ACL(/etc/headscale/acl.hujson,HuJSON 兼容 JSON 且允许注释):
{
// baseline: allow all traffic from anywhere
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
]
}
启用文件模式并重启:
sed -i 's| path: ""| path: /etc/headscale/acl.hujson|' /etc/headscale/config.yaml
systemctl restart headscale && systemctl is-active headscale
实测输出:
active
查看当前生效策略(服务端回读):
headscale policy get
实测输出:
{
// baseline: allow all traffic from anywhere
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": ["*:*"]
}
拦截对比实验: 把 dst 从 *:* 收紧为 *:22,80,443 并重启,在 node2 监听 9999 端口,node1 实测连接:
"dst": ["*:22,80,443"]
-- ACL *:* -> nc 9999:(收紧版)
nc: connect to 100.64.0.2 port 9999 (tcp) timed out: Operation now in progress
-- ACL *:* -> nc 9999:(恢复放行版)
Connection to 100.64.0.2 9999 port [tcp/*] succeeded!
node1 侧收到的包过滤规则也同步变化(tailscale debug netmap 的 PacketFilter 只剩 22/80/443 三个端口段),证明 ACL 文件在重启后重新编译并下发。语法注意(实测踩坑): v0.29.5 的策略解析对用户引用格式有要求,"src": ["demo"] 会因被当作主机别名而拒绝启动:
Error: initializing: creating new headscale: init state: initializing policy manager: parsing policy: host not defined in policy: "demo"
写成 "user:demo" 同样被拒:
Error: initializing: creating new headscale: init state: initializing policy manager: parsing policy: parsing policy from bytes: json: cannot unmarshal JSON string into Go v2.AliasEnc within "/acls/0/src/0": invalid alias format: "user:demo"
两种写法都会让服务循环重启;策略文件改动同样必须 systemctl restart headscale 后才重新编译下发。策略语法细节以 headscale.net/stable/ref/policy/ 为准,跨用户/分组规则建议先在测试环境用 policy get 验证。
5.6 database:元数据存储
database:
type: sqlite # sqlite(官方推荐)或 postgres(仅为兼容保留)
gorm:
prepare_stmt: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true # WAL 模式,官方建议生产开启
wal_autocheckpoint: 1000
官方注释明确「所有新开发、测试与优化都以 SQLite 为准,不鼓励 Postgres」。开启 WAL 后数据目录会出现 db.sqlite-shm 与 db.sqlite-wal 伴随文件(实测确认存在),备份时用 sqlite3 在线备份命令可保证一致性(步骤见《Headscale 运维手册》)。
5.7 node 与其他段
node:
expiry: 0 # 未打标签节点的默认密钥过期时间,0 表示永不过期
ephemeral:
inactivity_timeout: 30m # 临时节点不活跃后自动删除的时限
routes:
ha:
probe_interval: 10s # HA 子网路由健康探测间隔(0 关闭)
probe_timeout: 5s
node.expiry 对应「节点需要周期性重新认证」的策略,设为 180d 可对齐 Tailscale SaaS 行为;实际运维中可对单节点手工过期(headscale nodes expire,实测见《Headscale 运维手册》)。其余段落速查:
| 参数 | 默认值 | 说明 |
|---|---|---|
| disable_check_updates | false | 关闭启动时的版本更新检查 |
| log.level / log.format | info / text | 日志级别(panic~trace)与格式(text/json) |
| tls_cert_path / tls_key_path | "" | 配置后 HTTP 服务以 HTTPS 监听(自签或商业证书) |
| tls_letsencrypt_hostname | "" | 配置后自动向 Let's Encrypt 申请证书(需 80 端口可达) |
| trusted_proxies | [] | 反向代理网段,配置后信任 X-Forwarded-For 等头(官方不建议前置代理) |
| logtail.enabled | false | 客户端日志上传 Tailscale 云端(自托管默认关闭) |
| taildrop.enabled | true | Taildrop 文件共享总开关 |
| auto_update.enabled | false | 是否向客户端下发自动更新默认值 |
| unix_socket / unix_socket_permission | /var/run/headscale/headscale.sock / 0770 | 本地 CLI 通道 |
六、生产配置模板
以下为「50~200 节点、8GB 内存、NVMe SSD、内网自托管 + 域名 HTTPS」场景的推荐配置(相对默认值的差异点在注释标注):
server_url: https://hs.example.com # 用域名 + HTTPS,客户端无需感知端口
listen_addr: 0.0.0.0:8080 # 8080 仅内网可达,或前置防火墙放行
metrics_listen_addr: 127.0.0.1:9090 # 指标仅本机,再由本机采集器转发
grpc_listen_addr: 127.0.0.1:50443 # 不做远程 CLI,收回到环回
prefixes:
v4: 100.64.0.0/16 # 预留 6.5 万地址,避免后期改池
v6: fd7a:115c:a1e0::/48
allocation: sequential
derp:
server:
enabled: true # 全内网可启用内嵌 DERP(需 https + 3478/udp)
region_id: 999
region_code: "self"
region_name: "Self-hosted DERP"
verify_clients: true
stun_listen_addr: "0.0.0.0:3478"
urls: [] # 关闭公网 DERP,强制走内嵌
auto_update_enabled: false
node:
expiry: 180d # 与 Tailscale SaaS 一致的周期重认证
ephemeral:
inactivity_timeout: 30m
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
tls_cert_path: /etc/headscale/tls/cert.pem # 组织内 CA 签发
tls_key_path: /etc/headscale/tls/key.pem
log:
level: info
format: text
policy:
mode: file
path: /etc/headscale/acl.hujson
dns:
magic_dns: true
base_domain: tailnet.example.com # 与 server_url 域名不同
override_local_dns: true
nameservers:
global:
- 10.0.0.53 # 内网 DNS(global 必填,否则拒绝启动)
split: {}
search_domains: []
extra_records: []
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
模板说明:SQLite 单文件存储在 200 节点规模下足够(官方亦按 SQLite 优化),备份与迁移成本最低;节点规模上千或需要外部备份体系时再评估 Postgres。内存占用参考实测:单服务进程常驻约 20MB(systemctl status headscale 显示 Memory: 20.9M),小规格服务器即可承载。
七、常见问题(FAQ)
Q1:改了配置为什么没生效?
Headscale 只在启动时读配置。标准流程是 headscale configtest 预检 → systemctl restart headscale → systemctl is-active headscale 确认。DNS/ACL 类变更还会异步下发节点,节点侧可 systemctl restart tailscaled 强制刷新(实测 MagicDNS 与 ACL 实验均如此)。
Q2:服务一直 activating 循环重启?
看 journalctl -u headscale -n 20 找 Fatal config error。实测两大触发点:dns.nameservers.global 为空但 override_local_dns 为 true;policy 文件存在语法或引用错误(如把用户名当主机别名)。修复对应段落后重启即恢复。
Q3:启用内嵌 DERP 后客户端连不上 DERP?
DERP 需要 TLS:server_url 必须是 https,且 3478/udp(STUN)需在防火墙放行。先用 curl -v https://<server_url> 确认证书链,再从节点 tailscale ping <对端> 观察是否回落中继。
Q4:ACL 规则里怎么引用用户?为什么 "demo" 报 host not defined?
v0.29.5 的策略解析器对裸用户名按主机别名处理,实测 ["demo"] 与 ["user:demo"] 均被拒绝。跨用户的访问控制建议使用组(groups)或标签(tags)语法,写法以官方策略参考页为准,并用 headscale policy get 回读确认。
Q5:想缩小 IP 池,会影响已注册节点吗? 不会。实测将 v4 收窄到 100.64.4.0/24 后,新节点从新池分配(100.64.4.1),老节点保持原地址在线。改池前规划好新池容量即可。
八、参考链接
- v0.29.5 官方配置样例(494 行):https://github.com/juanfont/headscale/raw/v0.29.5/config-example.yaml
- 配置参考(stable):https://headscale.net/stable/ref/config/
- DNS 参考:https://headscale.net/stable/ref/dns/
- 策略(ACL/Grants)参考:https://headscale.net/stable/ref/policy/
- DERP 参考:https://headscale.net/stable/ref/derp/
- 本专栏配套:《Ubuntu 22.04 安装 Headscale 指南》《Headscale 运维手册》