Skip to content

Releases

v0.29.7

Choose a tag to compare

@github-actions github-actions released this 30 Oct 09:55

Minimum supported Tailscale client version: v1.80.0

Security

This is a security release. It fixes findings from Anthropic's oss-scanner project and
the Tailworm report. None are critical, but some can cause denial of service in
Headscale. We recommend that all users upgrade to Headscale 0.29.7 and Tailscale
v1.104.4 or newer as soon as possible, especially those using Tailscale SSH.

Tailscale v1.104.4 fixes vulnerabilities in SSH forwarding, SSH session handling and
Serve. These fixes run in tailscaled on your nodes; upgrading Headscale alone does
not apply them.

See Tailscale's security bulletins and Tailscale SSH: Findings and fixes.

To disable SSH agent and port forwarding on a node, set
TS_SSH_DISABLE_FORWARDING=1 in the tailscaled service environment and restart
the service. This setting applies to all Tailscale SSH sessions on that node and
is also available in older clients. #3624

This release also fixes the issues that oss-scanner
reported in Headscale: ANT-2026-MLP8ZR5T, ANT-2026-LYVGXVSW, ANT-2026-AWGRB7O1, ANT-2026-QMCAFJ1Q, ANT-2026-RQS7JAPP, ANT-2026-5B6KE87K, ANT-2026-KFCFFM34, ANT-2026-K903ZEUA, ANT-2026-NM50ZP4N, ANT-2026-ZC18AF82, ANT-2026-NWTIBB02, ANT-2026-MX8FKG4K. #3603

Changes

  • Metrics are no longer exposed to unregistered clients through the Noise protocol. They remain available only on the configured metrics listener; an empty metrics_listen_addr disables that listener #3623
  • Update the Tailscale dependency to v1.104.4, which bounds how much of an unexpected DERP HTTP response is read #3577
  • Keep pending registrations and OIDC logins at capacity instead of evicting ones awaiting approval; new requests get a retry response, and registered nodes keep capacity for SSH checks #3586
  • Cap stored pending registration data at 64 KiB, keeping tags and expiry; oversized interactive registrations are rejected #3586
  • Check the OIDC login type before storing anything, accept each OIDC callback only once, and free an SSH check's slot once the client has read its result #3586
  • Stop serving a route that is a single address inside the node IP ranges, such as 100.64.0.11/32 or ::ffff:100.64.0.11/128, because it names a node, not a subnet; the route stays approved and listed, and wider routes are still served #3603
  • Require a valid pre-auth key to re-register an expired tagged node #3603
  • Another user's key or login on a tagged machine now creates a new device for that user instead of re-authenticating the tagged node #3603
  • Fix one node with a stored key expiry outside the years 0 to 9999 in UTC failing the map of every node that can see it; it is now left out of its peers' maps and the startup check names it with the fix command #3603
  • Refuse a key expiry outside the years 0 to 9999 in UTC on registration, on the OIDC token-expiry path and in headscale nodes expire / POST /api/v1/node/{node_id}/expire (HTTP 400); the latest accepted expiry no longer fails node listings on PostgreSQL in a zone east of UTC #3603
  • Keep the first 256 endpoints and 4096 announced routes from client reports; oversized hostnames, requested tags or other metadata return a registration error or HTTP 413 on map updates. Zoned IPv6 endpoints are refused #3603
  • At startup, trim stored hostnames, endpoints and routes to the client-report limits and remove zoned endpoints. Hostinfo exceeding its size or tag limit is dropped until the next valid report #3603
  • Pending interactive registrations and follow-up waits keep only the hostname, OS and requested tags; other device details arrive with the first map request. Reauthentication preserves routes, services and endpoints #3603
  • Fix web reauthentication dropping the discovery key, so peers could not reach the device #3603
  • A stalled request body, or a Noise tunnel with no open request, is closed after 30 seconds #3603
  • A registration follow-up from a machine other than the one that started the registration is refused before it waits #3603
  • Only the newest follow-up waits on a pending registration or SSH check; older ones are answered at once #3603
  • Fix registration locks staying in memory after the registration ends #3603
  • Fix concurrent deletes of one node freeing its addresses and announcing its removal more than once #3603
  • OIDC verifies an email against the document supplying it; an email_verified flag without an address no longer verifies another document's email #3603
  • Fix release builds reporting their version with a +dirty suffix #3584
  • Fix SSH policy acceptEnv having no effect, so allowed environment variables reach SSH sessions. Upgrade SSH destination nodes to Tailscale v1.104.4 or newer before using acceptEnv, because older clients can disclose forwarded values #3569

Upgrade

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

Changelog

  • c0a51b2 .gitattributes: mark golden data as generated
  • c9cf210 CHANGELOG: align the 0.29.7 entries with earlier releases
  • 3d9031c CHANGELOG: document SSH acceptEnv fix
  • 5083559 CHANGELOG: document the 0.29.7 security and patch release
  • 8651eb9 CHANGELOG: list the oss-scanner findings for 0.29.7
  • abe807a CHANGELOG: list the pending authentication bounds for 0.29.7
  • 3345493 auth, state: keep only hostname, OS and tags in pending registrations
  • 1b7435c auth: check the follow-up's machine key before it waits
  • 99d10b9 db, state: let only the delete that removed a node free its addresses
  • 151c852 go.mod: pin golang-lru/v2 to main for expirable.Close
  • 718cbc3 go.mod: tidy for Go 1.27
  • 044d8f5 go.mod: update tailscale.com to v1.104.4
  • 37b4237 hscontrol: bound pending authentication without evicting sessions
  • be114c0 hscontrol: refuse a key expiry that cannot be encoded
  • c32bf08 hscontrol: remove unused OIDC user change result
  • 3bb6d3a integration/tsic: drop unused watchIPN
  • fd6547f integration: guard SSH acceptEnv forwarding
  • 15af45c mapper: skip peers whose key expiry cannot be encoded
  • 2d881c5 noise: restrict metrics to the metrics listener
  • bc3ac27 noise: time out stalled bodies and idle tunnels
  • e1c6cb9 oidc: keep email_verified with the address it came with
  • 6e0d520 policy/v2, servertest, hscontrol: add tailnet overlap route captures
  • 02b1528 policy: pin SSH forwarding behavior and recommend upgrades
  • f193207 prek: exempt step captures from the large file check
  • 93340a6 release: reject dirty sources and build artifacts
  • 9318ce4 release: retry existing tags without the Docker downgrade
  • 99cb5c1 servertest: add login, profile, restart and register log helpers
  • c848c5a servertest: add relogin and auth key helpers
  • 7a51c8f servertest: compare router self routes with captures
  • 9ce0b28 servertest: compare user profiles in via captures
  • 4647866 servertest: list passing via captures
  • e56661c state: bound client reports of nodes loaded at startup
  • 4d04361 state: drop registration locks once unused
  • c8fb25d state: give another user's key on a tagged machine a new device
  • e6219cd state: need a valid pre-auth key to re-register an expired tagged node
  • 2852b65 state: never serve a single address inside the node ranges
  • 514979b state: stop the auth cache TTL sweeper on Close
    ...
Read more

v0.29.6

Choose a tag to compare

@github-actions github-actions released this 24 Oct 10:29

Minimum supported Tailscale client version: v1.80.0

Security

  • Build with Go 1.27.3 to reject certificate chains containing the compromised eMudhra intermediate keys (CVE-2026-111943) Go #82345
  • Update and pin the normal and debug container bases to images whose CA bundles no longer include emSign ECC Root CA - G3 #3579
  • Update golang.org/x/net to v0.60.0, golang.org/x/text to v0.42.0 and gRPC to v1.83.2 to address HTTP/2 denial-of-service and text-processing vulnerabilities (x/net, x/text, gRPC)

Custom builds must be rebuilt with Go 1.27.3 or newer to receive the certificate-chain protection.

Changes

  • Speed up peer-map construction on large tailnets by finding each node's matching rules once, instead of checking every pair of nodes against every rule #3556
  • Build the peer map once per policy write instead of twice, and skip the rebuild when reloading an unchanged policy #3557
  • Reduce work when broadcasting map updates by avoiding repeated recipient filtering #3568
  • Avoid INTERNAL_ERROR stream errors when a node logs out or is deleted, or when Headscale shuts down; writes already in progress have a short grace period to finish, while clients that stop reading are still disconnected #3575
  • Return database-open errors instead of risking a panic or returning a partially initialized database handle #3496
  • Update the Tailscale dependency to v1.102.0 for Go 1.27 JSON compatibility in SSH check responses

Upgrade

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

Changelog

  • ee74ce4 CHANGELOG: document the 0.29.6 security and patch release
  • a2b1d78 CHANGELOG: set the 0.29.6 release date
  • f2b5f45 all: prepare 0.29.6 security updates
  • c32e8e4 db: return database-open errors for 0.29.6
  • 981f2eb lint: retain the exhaustruct exclusion with the updated toolchain
  • f1499ce mapper: avoid refiltering broadcast recipients
  • c618198 policy: backport peer-map optimizations for 0.29.6
  • 941d7ff poll: end stopped map sessions without resetting active streams

v0.29.5

Choose a tag to compare

@github-actions github-actions released this 13 Oct 13:05
  • Fix via exit-node steering being undone by an unrelated ACL or grant rule that matches the viewer #3495
  • Fix via grants not offering exit nodes and subnet routes to a group that names an unknown user #3516
  • Fix SSH check periods and app grants dropping a group that names an unknown user #3516
  • Fix policy tests and sshTests failing for a group that names an unknown user #3516
  • Fix an unknown user in nodeAttrs rejecting the policy #3516
  • Fix every grant being fully resolved as if it had via when the policy has no via grants, slowing map generation on large tailnets #3538
  • Fix a data race in via route resolution when the policy has both via grants and ACLs #3515
  • Fix packet filters under autogroup:self not updating after a user is added or renamed #3501
  • Fix a rejected policy leaving its packet filter active #3501
  • Fix an exit node or subnet router not seeing its own approved routes until it reconnected, so tailscale status did not show it offering an exit node #3518
  • Fix deleted nodes, and peers hidden by a policy change, staying listed in the Tailscale Android app; removed peers are now sent as their own incremental map update #3492 #3527
  • Policy changes no longer resend DNS configuration to every node, sparing clients a full netmap rebuild; a node gets its DNS configuration when its own NextDNS nodeAttrs, tags or hostname change, which also fixes NextDNS device metadata going stale after a hostname change #3492
  • Fix SSH access surviving the removal of its policy rules; clients kept their last SSH rules, and a pending SSH check could still be approved #3517
  • Fix SSH check periods coming from the first check rule for a node pair instead of the rule for the login user #3517
  • Policy changes resend a node's SSH policy only when it changed, sparing clients a full netmap rebuild #3517
  • Fix SSH check accepting a repeated follow-up for an already-decided session, even after a rejection #3526
  • Fix an ephemeral node being deleted while online, when it reconnected while its previous session was being marked offline #3539
  • Fix peers dropping traffic from a node that re-authenticates after its key expired #3541
  • Fix clients uploading logs to Tailscale Inc. while logtail.enabled is false; clients also granted the data-plane-audit-logs node attribute now go down until tailscale up #3522
  • Fix a node ping being lost when a full map update is queued at the same time #3523

Upgrade

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

Changelog

  • e279de9 Dockerfile: bump golang builders to 1.27.2 / 1.27.1
  • ceac1c1 Dockerfile: bump rust 1.95-trixie to 1.99-trixie
  • 755e6a1 all: build with Go 1.26.8
  • 8d2ac4a ci: regenerate the release integration test matrix
  • 52e0180 ci: reuse compiled integration tests and prebuilt images
  • 733d544 docker: let the tailscale builders fetch a newer toolchain
  • d0cb5a5 go.mod: update dependencies
  • 0a20796 hscontrol: preserve ephemeral nodes with live sessions
  • 98ce49d mapper: deliver peer removals as separate deltas
  • a4f854f mapper: preserve node pings when a full update is queued
  • 929e333 mapper: refresh self when approved routes change
  • 00f6702 mapper: retain reconnect removals until delivery
  • d03d3a0 mapper: retain the logtail instruction across client restarts
  • fb61927 mkdocs: bump version
  • 4d42bc6 noise: reject reused SSH check verdicts
  • bd8b8ce policy/v2: invalidate per-node filters when users change
  • 711d00d policy/v2: preserve via exit steering across unrelated rules
  • f079cf7 policy/v2: resolve unregistered users to an empty set
  • 5ec1a5a policy/v2: restore the live policy after a rejected update
  • 5fb2d4e policy/v2: revoke stale SSH access and recheck authentication
  • a4e56d7 policy/v2: skip route resolution without via grants
  • 6ed6dd8 policy/v2: stop ViaRoutesForPeer appending into pm.pol.Grants
  • 940260c servertest: bound fixture batching and await observable state
  • b16b705 servertest: wait for stable via-route netmaps
  • 5c25a66 state: send relogins as whole nodes when peers need them

v0.29.4

Choose a tag to compare

@github-actions github-actions released this 23 Sep 19:36

Minimum supported Tailscale client version: v1.80.0

Changes

  • Fix a node being listed among its own peers in an incremental map update, which crashes the Tailscale Android app on the device list #3459
  • Fix deleting a node leaving its long poll open, so the client stayed connected instead of asking for a new login #3449
  • Fix interactive OIDC login when the confirmation page is reloaded by an ad blocker, back navigation, or pull-to-refresh; the confirmation page now has its own URL, keeping single-use authorization codes out of reloads #3448
  • Harden the OIDC callback: state and nonce cookies take their Secure flag from server_url so they survive a TLS-terminating proxy, a callback state is single-use, and an invalid oidc.issuer or a missing oidc.client_id/oidc.client_secret now fails at startup #3334
  • Fix HTTP metrics only counting OPTIONS requests, so http_requests_total and http_request_duration_seconds now cover regular traffic #3414
  • Fix extra-records filewatcher hanging on shutdown after the watched file is deleted, and leaking the watcher when setup fails #3437
  • Lowercase DNS extra record names so mixed-case records resolve #3366
  • Fix headscale users rename sending the raw --identifier flag value instead of the matched user's identifier, so renaming by name works again #3442
  • Fix tailsql not shutting down with headscale, leaving the process hanging on graceful shutdown #3400
  • Fix tvOS setup instructions: install the VPN configuration before setting the coordination server URL #3431
  • Map requests that only bump LastSeen, endpoints or DERP region no longer resend the whole node to every peer, and health probes that change nothing no longer write. Adds headscale_mapper_changes_dropped_total and headscale_ha_health_updates_total #3417 #3450
  • The peer map is keyed by node ID and reused for writes that cannot change peer visibility, so a routine map request no longer rebuilds it. Adds headscale_nodestore_snapshot_builds_total #3417 #3450
  • Fix an expired node staying online forever, because expiring it updated the key deadline without ending its map session #3472
  • Fix ACME renewal stopping permanently after a badNonce reply, because the error logging middleware drained the response body the acme client needs to detect it #3461
  • Fix #-prefixed metadata fields being rejected outside acls, so policy editors can store metadata in grants, SSH rules and nodeAttrs #3481
  • Fix exit nodes not offered by recent macOS and iOS clients, which read the suggest-exit-node peer attribute rather than the advertised 0.0.0.0/0 routes #3487
  • Fix exit node not offered to viewers whose only matching rule is a via grant; peer visibility now comes from the peer map alone #3409

Upgrade

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

Changelog

  • 3a2b133 .github/workflows: regenerate the integration test matrix
  • bdfa340 AGENTS.md: drop stale line numbers
  • 8106636 CHANGELOG: add 0.29.4
  • b9b2beb Update AppleTV configuration steps
  • 2a76860 build: bump test image Go to 1.27.1
  • 974becf change, mapper: distinguish deleted nodes
  • b3b55cc cli: fix users rename when resolved by name
  • 46a80ea dns: cancel extra-records retry on shutdown and close watcher on setup error
  • 33db8c6 fix(metrics): collect metrics for non-OPTIONS requests
  • 032470a hscontrol: cancel tailsql on graceful shutdown
  • ebe18ce hscontrol: keep the ACME error body readable in acmeLogger
  • c7d9d03 hscontrol: replace tailscale line refs with doc links
  • 1e528f7 integration: cover deletion across client versions
  • b9c7b4c integration: cover node deletion ending the long poll
  • 7783e7f integration: cover node expiry and recovery for every client
  • 625fa86 integration: pin docker client to daemon API version
  • cc41593 integration: pin which Hostinfo changes reach peers
  • fc894aa linting issue fix
  • 2b8dbea mapper: assert no map response lists the recipient as its own peer
  • b8da1c2 mapper: drop empty changes before fan-out
  • 397149e mapper: stop a deleted node's map session
  • 9fbf7b9 mapper: take peer visibility from the peer map only
  • d388244 noise: make deleted-node expiry clock independent
  • 2b28f57 oidc: harden callback CSRF cookies, state reuse, and issuer config
  • cbb9357 oidc: harden reloadable confirmation flow
  • 2da47a7 oidc: serve the registration confirmation page from a reloadable URL
  • 938c2bd policy,state: key the peer map by node ID
  • bc5b9d5 policy,types: skip recompile when the user list is unchanged
  • 8490631 policy/v2: add via exit node capture
  • 694bafe policy/v2: compare peer CapMap against SaaS route captures
  • 5aded15 policy/v2: suggest approved exit nodes by default
  • aa4c952 policy: ignore '#' metadata fields across the whole policy
  • fb197db poll, noise: tell a deleted node to re-authenticate
  • 83eff40 poll: interrupt blocked map writes
  • a7f7e6a servertest: compare user-owned nodes in via compat tests
  • 8973347 state: classify map requests before broadcasting
  • d4b073e state: exclude self from peers on the named peer-ID path
  • a91f708 state: mark expired nodes offline without ending the session
  • 0fca2bb state: preserve committed node deletion changes
  • fdb782d state: rename persist helpers to say what they do
  • 6a0f67d state: resolve changed peers through adjacency
  • 237dc74 state: reuse peer adjacency for payload-only writes
  • 8f7ee37 state: skip node health writes that change nothing
  • b972da3 state: stop broadcasting a whole peer on disconnect
  • e3dba1f templates: apply the tvOS setup reorder to the served /apple page
  • 2b6cb3a types/change: drop unused VisibilityChange
  • 4411264 types: detect policy change on user identity and exit routes
  • b4f991b types: lowercase DNS extra record names
  • e6c0a8a types: regenerate node view

v0.29.3

Choose a tag to compare

@github-actions github-actions released this 29 Jul 12:51

Minimum supported Tailscale client version: v1.80.0

Changes

  • Fix tagged node stuck expired after tailscale logout, unable to re-authenticate #3394
  • Re-registering a tagged node with a different pre-auth key now applies the new key's tags instead of silently keeping the old ones #3394
  • Fix re-authenticating an already-tagged node with --advertise-tags being rejected when the authenticating user owns the tags #3394
  • Fix ephemeral nodes lingering as disconnected after reconnect churn #3383
  • Fix node registration falsely returning 401 registration timed out when auth completes as the request context expires #3392
  • Check the machine key on the followup registration poll so a leaked auth ID cannot return the registering user's identity #3393
  • Reject /key requests below the supported capability version floor, matching /ts2021 #3391

Upgrade

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

Changelog

  • 235a57e CHANGELOG: add 0.29.3
  • 089d6c4 Explicitly select lunr as search provider
  • fba84ca auth: check machine key on the followup registration path
  • 1292841 build: bump Go toolchain to 1.26.5
  • 9609a0b hscontrol: gate /key on supported capability version
  • bdc3e99 hscontrol: prefer completed auth over expired ctx in followup wait
  • 5aff68b mkdocs: bump version
  • 4a1e773 policy,state: authorize reauth tags against the authenticating user
  • 5fb514e poll: do not cancel ephemeral GC until Connect succeeds
  • 1fccdb1 state: apply a new pre-auth key's tags on re-registration
  • d202883 state: do not expire tagged nodes on logout

v0.29.2

Choose a tag to compare

@github-actions github-actions released this 01 Jul 17:58

Changes

  • Fix map generation serializing on the policy lock, so a mass reconnect on autogroup:self, via or relay policies no longer stalls clients into unexpected EOF retry loops #3358
  • Fix /ts2021 rejecting the WebSocket GET upgrade with 405, which prevented Tailscale JS/WASM control clients from connecting #3359
  • Gracefully handle nodes with an invalid FQDN (empty or too long) instead of failing map delivery; offending names are logged at startup with the fix command #3349

Upgrade

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

Changelog

  • 8eea894 CHANGELOG: drop unreleased 0.30.0 stub
  • f708c5b CHANGELOG: note /ts2021 WebSocket GET fix
  • 735742e CHANGELOG: note 0.29.2 invalid-name map fix
  • 3ac33cf CHANGELOG: shorten 0.29.2 invalid-name entry, set date
  • f885d87 Fix invalid ip syntax
  • 1ec7b7f hscontrol: register /ts2021 for WebSocket GET
  • 8f4e69d integration: add TS2021 WebSocket tests to CI matrix
  • e7851ef integration: test /ts2021 WebSocket GET with a real WASM client
  • 9d13274 mapper,policy: add reconnect-storm and lock-concurrency regression tests
  • ec67197 mapper: skip peers with invalid names instead of failing the map
  • d4f2acf policy: take RLock for reads so map generation runs concurrently
  • 5fb76eb poll: return an HTTP error on long-poll setup failure
  • fd154fd state: log nodes with map-breaking data at startup
  • 9c9206a state: reject renames whose FQDN exceeds the hostname limit

v0.29.1

Choose a tag to compare

@github-actions github-actions released this 18 Jun 14:20

Minimum supported Tailscale client version: v1.80.0

Changes

  • Fix nodes with tags='null' losing their assigned user on upgrade #3325

Upgrade

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

Changelog

  • 636f660 db: preserve user_id on untagged nodes with tags='null'

v0.29.0

Choose a tag to compare

@github-actions github-actions released this 17 Jun 09:21

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:

Input Raw (Hostname) DNS label (GivenName)
Joe's Mac mini Joe's Mac mini joes-mac-mini
Yuri's MacBook Pro Yuri's MacBook Pro yuris-macbook-pro
Test@Host Test@Host test-host
mail.server mail.server mail-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...
Read more

v0.29.0-beta.4

v0.29.0-beta.4 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 15 Jun 18:57

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:

Input Raw (Hostname) DNS label (GivenName)
Joe's Mac mini Joe's Mac mini joes-mac-mini
Yuri's MacBook Pro Yuri's MacBook Pro yuris-macbook-pro
Test@Host Test@Host test-host
mail.server mail.server mail-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...
Read more

v0.29.0-beta.3

v0.29.0-beta.3 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 11 Jun 14:45

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:

Input Raw (Hostname) DNS label (GivenName)
Joe's Mac mini Joe's Mac mini joes-mac-mini
Yuri's MacBook Pro Yuri's MacBook Pro yuris-macbook-pro
Test@Host Test@Host test-host
mail.server mail.server mail-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...
Read more