Headscale 配置参数详解

Headscale v0.29.5 配置文件逐段详解:server_url、prefixes、dns、policy、database 等核心参数含义与默认值,配「改值→重启→行为对比」实测(前缀收窄 IP 分配、MagicDNS 接管、ACL 拦截、metrics 暴露)与生产配置模板。

HeadscaleHeadscale2026/10/18

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

二、前置条件

  1. 已按《Ubuntu 22.04 安装 Headscale 指南》完成 v0.29.5 安装并确认服务运行(systemctl is-active headscale 返回 active);
  2. 至少一台节点已注册(headscale nodes list 可见),便于验证参数变更对客户端的影响;
  3. 服务端已安装 sqlite3 与 curl(参数验证时会用到);
  4. 理解一条总原则: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已随安装验证(首启自动生成)
prefixestailnet IP 池prefixes.v4 / v6 / allocation实测(收窄 v4 前缀观察新节点分配)
derp中继与 STUNderp.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/TLSHTTPS 证书tls_cert_path / tls_letsencrypt_hostname参数说明
log日志log.level / log.format参数说明
policyACL/Grants 策略policy.mode / policy.path实测(文件模式 ACL 生效与拦截对比)
dnsMagicDNS 与解析下发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_updatesfalse关闭启动时的版本更新检查
log.level / log.formatinfo / 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.enabledfalse客户端日志上传 Tailscale 云端(自托管默认关闭)
taildrop.enabledtrueTaildrop 文件共享总开关
auto_update.enabledfalse是否向客户端下发自动更新默认值
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),老节点保持原地址在线。改池前规划好新池容量即可。

八、参考链接