Headscale v0.29.0-beta.4

v0.29.0-beta.4Pre-release
Added 9
  • SSH rules with check action are now supported, prompting users to authenticate via OIDC or CLI approval before access is granted
  • New headscale auth CLI command group with approve, reject, and register subcommands for authentication workflows
  • Policy tests in the tests block now evaluated on policy writes, reloads, and checks, with failures preventing application
  • SSH policy tests in the sshTests block now evaluated on policy writes, reloads, and checks
  • Support for Tailscale grants alongside ACLs to control application-level features like Taildrive and peer relay
  • New autogroup:danger-all that resolves to all IP addresses including those outside the tailnet
Changed 3
  • Minimum supported Tailscale client version is now v1.80.0
  • Tailscale ACL compatibility improved through systematic test case generation against official SaaS
  • SSH rule parsing now trims whitespace on action, users, src, and dst fields and validates entries more strictly

Minimum supported Tailscale client version: v1.80.0

Tailscale ACL compatibility improvements

Extensive test cases were systematically generated using Tailscale clients and the official SaaS to understand how the packet filter should be generated. We discovered a few differences, but overall our implementation was very close. #3036

SSH check action

SSH rules with "action": "check" are now supported. When a client initiates a SSH connection to a node with a check action policy, the user is prompted to authenticate via OIDC or CLI approval before access is granted. OIDC approval requires the authenticated user to own the source node; tagged source nodes cannot use SSH check-mode.

A new headscale auth CLI command group supports the approval flow:

  • headscale auth approve --auth-id <id> approves a pending authentication request (SSH check or web auth)
  • headscale auth reject --auth-id <id> rejects a pending authentication request
  • headscale auth register --auth-id <id> --user <user> registers a node (replaces deprecated headscale nodes register)

#1850 #3180

Policy tests (beta)

Headscale now evaluates the tests block in a policy file. Tests assert reachability between named sources and destinations and cover the whole policy — both acls and grants rules contribute. They run on user-initiated writes via headscale policy set, on SIGHUP reload (systemctl reload headscale / kill -HUP $(pidof headscale)), and on headscale policy check. A failing test rejects the write before it is applied, with the same error message Tailscale SaaS would return for the same policy.

At boot a stored policy whose tests no longer pass — for example because a referenced user was deleted while the server was offline — logs a warning and the server keeps running. Fix the policy and reload.

This feature is beta while behavioural coverage against Tailscale SaaS broadens.

#3229

SSH policy tests (beta)

Headscale now evaluates the sshTests block in a policy file. Each entry names a source, one or more destination hosts, and three optional user lists: accept asserts the listed login users reach every destination via an accept- or check-action SSH rule, deny asserts none of them reach any destination, and check requires reachability specifically through a check-action rule. Tests run on headscale policy set, on SIGHUP reload (systemctl reload headscale / kill -HUP $(pidof headscale)), and on headscale policy check. A failing test rejects the write before it is applied, with the same error message Tailscale SaaS would return for the same policy.

At boot a stored policy whose sshTests no longer pass — for example because a referenced user was deleted while the server was offline — logs a warning and the server keeps running. Fix the policy and reload.

This feature is beta while behavioural coverage against Tailscale SaaS broadens.

#3263

SSH rule validation

SSH rule parsing now trims surrounding whitespace on action, users, src, and dst, rejects empty or wildcard entries in users, rejects empty acceptEnv, and rejects negative checkPeriod. hosts: aliases are rejected as SSH destinations, non-ASCII tag names are rejected at parse time, and the wording for group-nesting cycles matches Tailscale SaaS. #3263

Grants

We now support Tailscale grants alongside ACLs. Grants extend what you can express in a policy beyond packet filtering: the app field controls application-level features like Taildrive file sharing and peer relay, and the via field steers traffic through specific tagged subnet routers or exit nodes. The ip field works like an ACL rule. Grants can be mixed with ACLs in the same policy file. #2180

As part of this, we added autogroup:danger-all. It resolves to 0.0.0.0/0 and ::/0, all IP addresses, including those outside the tailnet. This replaces the old behaviour where * matched all IPs (see BREAKING below). The name is intentional: accepting traffic from the entire internet is a security-sensitive choice. autogroup:danger-all can only be used as a source.

Node attributes (nodeAttrs)

ACL policies now accept a nodeAttrs block. Each entry hands a list of Tailscale node capabilities to every node matching target. The accepted target forms are the same as acls.src and grants.src: users, groups, tags, hosts, prefixes, autogroup:member, autogroup:tagged, and *.

{
  "randomizeClientPort": true,
  "nodeAttrs": [
    { "target": ["autogroup:tagged"], "attr": ["disable-captive-portal-detection"] },
    { "target": ["alice@example.com"], "attr": ["nextdns:abc123"] },
  ],
}

Frequently requested capabilities this unlocks include magicdns-aaaa, disable-relay-server, disable-captive-portal-detection, nextdns:<profile> / nextdns:no-device-info, randomize-client-port, and the Taildrive drive:share / drive:access pair. The set is not limited to these, any string-only cap an operator places in policy reaches clients unchanged.

randomizeClientPort also lands as a top-level policy field that toggles the default for every node, replacing the old server-config knob.

A new auto_update.enabled config option controls the tailnet-wide default for client auto-update. When true, every node's CapMap carries default-auto-update: [true] so fresh clients pick up the default unless they make a local opt-in / opt-out choice.

Policies that use the funnel cap, ipPool blocks, or autogroup:admin / autogroup:owner targets are rejected at load — those features depend on machinery headscale does not yet ship.

#3251

Taildrive

Taildrive (file-sync between nodes) is now configurable through policy. Grant drive:share to the node that hosts files and drive:access to nodes that read or write them; pair with a tailscale.com/cap/drive grant to set the per-share access mode:

{
  "nodeAttrs": [
    { "target": ["tag:fileserver"], "attr": ["drive:share"] },
    { "target": ["autogroup:member"], "attr": ["drive:access"] },
  ],
  "grants": [
    {
      "src": ["autogroup:member"],
      "dst": ["tag:fileserver"],
      "app": {
        "tailscale.com/cap/drive": [{ "shares": ["*"], "access": "rw" }],
      },
    },
  ],
}

A wildcard nodeAttrs ("target": ["*"]) hands the caps to every node when fine-grained control is not needed.

Hostname sanitisation

Hostnames are now santised using Tailscales magicdns sanitisation rules, matching Tailscale SaaS behavior. This means that hostnames with non-ASCII characters, special characters, or reserved DNS label characters are now transformed into valid DNS labels for MagicDNS. This improves our previously too strict sanitisation that rejected hostnames based on our guesswork and not based on the Tailscale upstream behaviour.

Examples that previously regressed and now work:

InputRaw (Hostname)DNS label (GivenName)
Joe's Mac miniJoe's Mac minijoes-mac-mini
Yuri's MacBook ProYuri's MacBook Proyuris-macbook-pro
Test@HostTest@Hosttest-host
mail.servermail.servermail-server
My-PC!My-PC!my-pc
我的电脑我的电脑node

#3202

HA subnet router health probing

Headscale now actively probes HA subnet routers to detect nodes that are connected but not forwarding traffic. The control plane periodically pings HA subnet routers via the Noise control channel and fails over to a healthy standby if the primary stops responding. This is enabled by default (node.routes.ha.probe_interval: 10s, probe_timeout: 5s) and only active when HA routes exist (2+ nodes advertising the same prefix). Set probe_interval to 0 to disable. This complements the existing disconnect-based failover, catching "zombie connected" routers that maintain their control session but cannot route packets. #3194

BREAKING
Hostname handling
  • The GivenName collision policy changed from an 8-char random hash suffix (laptop-abc12xyz) to a monotonic numeric suffix (laptop, laptop-1, laptop-2, …), matching Tailscale SaaS. Empty / all-non-ASCII hostnames now fall back to the literal node instead of invalid-<rand>. MagicDNS names change on upgrade for any node whose previous label was a random-suffix form; the raw Hostname column is unchanged. #3202
ACL Policy
  • Wildcard (*) in ACL sources and destinations now resolves to Tailscale's CGNAT range (100.64.0.0/10) and ULA range (fd7a:115c:a1e0::/48) instead of all IPs (0.0.0.0/0 and ::/0) #3036
    • This better matches Tailscale's security model where * means "any node in the tailnet" rather than "any IP address"
    • Policies that need to match all IP addresses including non-Tailscale IPs should use autogroup:danger-all as a source, or explicit CIDR ranges as destinations #2180
    • autogroup:danger-all can only be used as a source; it cannot be used as a destination
    • Note: Users with non-standard IP ranges configured in prefixes.ipv4 or prefixes.ipv6 (which is unsupported and produces a warning) will need to explicitly specify their CIDR ranges in ACL rules instead of using *
  • Validate autogroup:self source restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources for autogroup:self destinations #3036
    • Policies using tags, hosts, or IP addresses as sources for autogroup:self destinations will now fail validation
  • The proto:icmp protocol name now only includes ICMPv4 (protocol 1), matching Tailscale behavior #3036
    • Previously, proto:icmp included both ICMPv4 and ICMPv6
    • Use proto:ipv6-icmp or protocol number 58 explicitly for ICMPv6
Upgrade Path
  • Headscale now enforces a strict version upgrade path #3083
    • Skipping minor versions (e.g. 0.27 → 0.29) is blocked; upgrade one minor version at a time
    • Downgrading to a previous minor version is blocked
    • Patch version changes within the same minor are always allowed
Configuration
  • The randomize_client_port server-config key was removed; the toggle now lives in the policy file as a top-level randomizeClientPort field, matching the Tailscale-hosted schema. #3251 Headscale refuses to start when the old key is set. Move it to the policy file referenced by policy.path:

    {
      "randomizeClientPort": true,
    }
    

    If you do not have a policy file yet, create one with that minimal content and point policy.path at it. The default carries over — empty / absent policy means randomizeClientPort: false, matching the previous behaviour for operators who never set the key. Per-node opt-in via nodeAttrs is also supported and stacks on top of the global default.

CLI
  • headscale nodes register is deprecated in favour of headscale auth register --auth-id <id> --user <user> #1850
    • The old command continues to work but will be removed in a future release
Changes
ACL Policy
  • Fix subnet-to-subnet peer visibility — subnet routers now correctly become peers when ACL rules reference only subnet CIDRs as sources, without requiring node IP rules #3175
  • Fix filter rule reduction to use only approved subnet routes instead of all advertised routes, matching Tailscale SaaS behavior #3175
  • Add ICMP and IPv6-ICMP protocols to default filter rules when no protocol is specified #3036
  • Fix autogroup:self handling for tagged nodes - tagged nodes no longer incorrectly receive autogroup:self filter rules #3036
  • Use CIDR format for autogroup:self destination IPs matching Tailscale behavior #3036
  • Merge filter rules with identical SrcIPs and IPProto matching Tailscale behavior - multiple ACL rules with the same source now produce a single FilterRule with combined DstPorts #3036
  • Fix exit nodes incorrectly receiving filter rules for destinations that only overlap via exit routes #3169 #3175
  • Fix address-based aliases (hosts, raw IPs) incorrectly expanding to include the matching node's other address family #2180
  • Fix identity-based aliases (tags, users, groups) resolving to IPv4 only; they now include both IPv4 and IPv6 matching Tailscale behavior #2180
  • Fix wildcard (*) source in ACLs now using actually-approved subnet routes instead of autoApprover policy prefixes #2180
  • Fix non-wildcard source IPs being dropped when combined with wildcard * in the same ACL rule #2180
  • Fix exit node approval not triggering filter rule recalculation for peers #2180
  • Policy validation error messages now include field context (e.g., src=, dst=) and are more descriptive #2180
  • Reject policies whose user@ tokens match multiple DB users; rename the duplicate via headscale users rename to load #3160
  • Evaluate the policy tests block on user-initiated writes across both acls and grants; reject policies whose tests fail (beta) #1803
Grants
  • Add support for policy grants with ip, app, and via fields #2180
  • Add autogroup:danger-all as a source-only autogroup resolving to all IP addresses #2180
  • Add capability grants for Taildrive (cap/drive) and peer relay (cap/relay) with automatic companion capabilities #2180
  • Add per-viewer via route steering — grants with via tags control which subnet router or exit node handles traffic for each group of viewers #2180
  • Enable Taildrive node attributes on all nodes; actual access is controlled by cap/drive grants #2180
SSH Policy
  • Add support for localpart:*@<domain> in SSH rule users field, mapping each matching user's email local-part as their OS username #3091
  • Add SSH check action support with OIDC and CLI-based approval flows #1850
CLI
  • Add headscale auth register, headscale auth approve, and headscale auth reject CLI commands #1850
  • Deprecate headscale nodes register --key in favour of headscale auth register --auth-id #1850
  • headscale policy check --bypass-grpc-and-access-database-directly validates user@ tokens against the live user database #3160
  • Remove deprecated --namespace flag from nodes list, nodes register, and debug create-node commands (use --user instead) #3093
  • Remove deprecated namespace/ns command aliases for users and machine/machines aliases for nodes #3093
  • Fix DestroyUser deleting all pre-auth keys in the database instead of only the target user's keys #3155
  • headscale policy check evaluates the tests block when invoked with --bypass-grpc-and-access-database-directly; without the flag it warns instead of running the tests against empty data #1803
API
  • Add auth related routes. The auth/register endpoint now expects data as JSON #1850
  • Remove gRPC reflection from the remote (TCP) server #3180
OIDC
  • Add a confirmation page before completing node registration, showing the device hostname and machine key fingerprint #3180
  • Generalise auth templates into reusable AuthSuccess and AuthWeb components #1850
  • Unify auth pipeline with AuthVerdict type, supporting registration, reauthentication, and SSH checks #1850
Configuration
  • Add node.expiry configuration option to set a default node key expiry for nodes registered via auth key #3122
    • Tagged nodes (registered with tagged pre-auth keys) are exempt from default expiry
    • oidc.expiry has been removed; use node.expiry instead (applies to all registration methods including OIDC)
    • ephemeral_node_inactivity_timeout is deprecated in favour of node.ephemeral.inactivity_timeout
  • Add trusted_proxies to gate True-Client-IP / X-Real-IP / X-Forwarded-For (previously honoured from any client) #3268
Debug
  • Add node connectivity ping page for verifying control-plane reachability #3183
  • Omit secret fields (Pass, ClientSecret, APIKey) from /debug/config JSON output #3180
  • Route statsviz through tsweb.Protected #3180
Other
  • Remove old migrations for the debian package #3185
  • Install config-example.yaml as example for the debian package #3186
  • Fix user-owned re-registration with zero client expiry and no default storing 0001-01-01 00:00:00 in the database instead of NULL #3199
  • Fix tailscaled restart on a node with no expiry resetting NULL to 0001-01-01 00:00:00 in the database, affecting both tagged and untagged nodes #3197
  • Backfill nodes.expiry rows persisted by older versions as 0001-01-01 00:00:00 to NULL, so nodes upgraded from <0.28 stop reporting as expired #3284
  • Update reverse proxy documentation for trusted_proxies configuration option #3292
Upgrade

Please follow the steps outlined in the upgrade guide to update your existing Headscale installation.

Changelog
  • e759d9fc9090ea97652348c084e39f4632dd4068 auth: re-validate key when an expired node re-registers
  • 68a6d3cf17f62002be1bca436ade474cb3551440 db: drop ambiguous machine-key getter, match precisely in test helper
  • 9b8949727d6da845a9f6fb80fce887ea2da36321 db: treat unknown pre-auth key as not found
  • a1d3e982554f8cc21bca0a80982cba005034b6c3 state: allow key expiry to be set on tagged nodes
  • fd08b8fa8c179fa4c7d2310ddee847d9e39e2e2c state: make any-user machine-key lookup deterministic
  • a5ef3aff15ab559e06f1553dcbe653a7b1e2d4f4 state: patch relogins and gate endpoint broadcasts
  • 0961e79e16746d0e60dc447a9929070cad82c986 state: re-register converted tagged nodes with reused key
  • a73d38bb3f3b0f9d9a1a090574c3e1774d88fc36 state: reject re-registration claiming another node's key
  • 168947848549280ededd48cd2529bfeb1d6c9f5a state: return all nodes for a machine key, reject ambiguous ownership
  • 96d2e6ed60f2d357c8b646eb5a490dd8de1b4b2b state: roll back node store when re-registration write fails
  • b83bf3f9936e61c55846adfa8091763d48f5d50b state: serialise registration per machine key
  • bff216a184eebaeb82baaabf50705e683185d899 state: update node in place on pre-auth-key re-registration
  • 4da06925d01727c11bafa633ade6aaf0d35d01d1 types/change: add NodeKeyRotated for relogin peer patch
View original

Upgraded? How did it go?

Discussion