- SSH rules with "action": "check" 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 managing authentication requests and node registration
- Policy tests (beta) that evaluate the tests block in policy files to assert reachability between sources and destinations
- SSH policy tests (beta) that evaluate the sshTests block in policy files with accept, deny, and check assertions
- Support for Tailscale grants alongside ACLs, including app and via fields for application-level features and traffic steering
- autogroup:danger-all autogroup that resolves to all IP addresses (0.0.0.0/0 and ::/0)
- Node attributes (nodeAttrs) block in ACL policies to assign Tailscale node capabilities to nodes matching specified targets
- randomizeClientPort as a top-level policy field to toggle the default for every node
- auto_update.enabled config option to control tailnet-wide default for client auto-update
- SSH rule parsing now trims surrounding whitespace on action, users, src, and dst fields
- Tailscale ACL compatibility improved based on systematic testing with Tailscale clients and official SaaS
- SSH rule parsing now rejects empty or wildcard entries in users, empty acceptEnv, and negative checkPeriod
- SSH rule parsing now rejects hosts: aliases as SSH destinations and non-ASCII tag names at parse time
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 requestheadscale auth register --auth-id <id> --user <user>registers a node (replaces deprecatedheadscale nodes register)
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.
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.
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.
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 |
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
GivenNamecollision 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 literalnodeinstead ofinvalid-<rand>. MagicDNS names change on upgrade for any node whose previous label was a random-suffix form; the rawHostnamecolumn 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/0and::/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-allas a source, or explicit CIDR ranges as destinations #2180 autogroup:danger-allcan only be used as a source; it cannot be used as a destination- Note: Users with non-standard IP ranges configured in
prefixes.ipv4orprefixes.ipv6(which is unsupported and produces a warning) will need to explicitly specify their CIDR ranges in ACL rules instead of using*
- This better matches Tailscale's security model where
- Validate
autogroup:selfsource restrictions matching Tailscale behavior - tags, hosts, and IPs are rejected as sources forautogroup:selfdestinations #3036- Policies using tags, hosts, or IP addresses as sources for
autogroup:selfdestinations will now fail validation
- Policies using tags, hosts, or IP addresses as sources for
- The
proto:icmpprotocol name now only includes ICMPv4 (protocol 1), matching Tailscale behavior #3036- Previously,
proto:icmpincluded both ICMPv4 and ICMPv6 - Use
proto:ipv6-icmpor protocol number58explicitly for ICMPv6
- Previously,
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_portserver-config key was removed; the toggle now lives in the policy file as a top-levelrandomizeClientPortfield, matching the Tailscale-hosted schema. #3251 Headscale refuses to start when the old key is set. Move it to the policy file referenced bypolicy.path:{ "randomizeClientPort": true, }If you do not have a policy file yet, create one with that minimal content and point
policy.pathat it. The default carries over — empty / absent policy meansrandomizeClientPort: false, matching the previous behaviour for operators who never set the key. Per-node opt-in vianodeAttrsis also supported and stacks on top of the global default.
CLI
headscale nodes registeris deprecated in favour ofheadscale 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 viaheadscale users renameto load #3160 - Evaluate the policy
testsblock on user-initiated writes across bothaclsandgrants; reject policies whose tests fail (beta) #1803
Grants
- Add support for policy grants with
ip,app, andviafields #2180 - Add
autogroup:danger-allas 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
viatags 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/drivegrants #2180
SSH Policy
- Add support for
localpart:*@<domain>in SSH ruleusersfield, mapping each matching user's email local-part as their OS username #3091 - Add SSH
checkaction support with OIDC and CLI-based approval flows #1850
CLI
- Add
headscale auth register,headscale auth approve, andheadscale auth rejectCLI commands #1850 - Deprecate
headscale nodes register --keyin favour ofheadscale auth register --auth-id#1850 headscale policy check --bypass-grpc-and-access-database-directlyvalidatesuser@tokens against the live user database #3160- Remove deprecated
--namespaceflag fromnodes list,nodes register, anddebug create-nodecommands (use--userinstead) #3093 - Remove deprecated
namespace/nscommand aliases forusersandmachine/machinesaliases fornodes#3093 - Fix
DestroyUserdeleting all pre-auth keys in the database instead of only the target user's keys #3155 headscale policy checkevaluates thetestsblock 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
authrelated routes. Theauth/registerendpoint 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
AuthSuccessandAuthWebcomponents #1850 - Unify auth pipeline with
AuthVerdicttype, supporting registration, reauthentication, and SSH checks #1850
Configuration
- Add
node.expiryconfiguration 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.expiryhas been removed; usenode.expiryinstead (applies to all registration methods including OIDC)ephemeral_node_inactivity_timeoutis deprecated in favour ofnode.ephemeral.inactivity_timeout
- Add
trusted_proxiesto gateTrue-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/configJSON output #3180 - Route
statsvizthroughtsweb.Protected#3180
Other
- Remove old migrations for the debian package #3185
- Install
config-example.yamlas example for the debian package #3186 - Fix user-owned re-registration with zero client expiry and no default storing
0001-01-01 00:00:00in the database instead ofNULL#3199 - Fix
tailscaledrestart on a node with no expiry resettingNULLto0001-01-01 00:00:00in the database, affecting both tagged and untagged nodes #3197 - Backfill
nodes.expiryrows persisted by older versions as0001-01-01 00:00:00toNULL, so nodes upgraded from <0.28 stop reporting as expired #3284 - Update reverse proxy documentation for
trusted_proxiesconfiguration option #3292
Upgrade
Please follow the steps outlined in the upgrade guide to update your existing Headscale installation.
Changelog
- 6777a82ee6c16cbeff08a5b0c8aa72e481e80b0e Rewrite reverse proxy documentation
- c5f3d5c28d628ef675210d172915751016f2b504 auth: clamp logout expiry to now
- 5228cb1a408e29b67d72720894c80ac489f11daa change: drop subnet-router full update, use policy change
- 7d845ef65ece7be591576dbe2eda45cf92f3aec7 db: advance allocator cursor under lock during IP backfill
- f61753e73768ccc1d39c7dbba0003540566b67bf db: bound IP allocation scan so exhausted prefixes error out
- ec945732584411c7566dcbe09d2333a4011971bf db: drop superseded ephemeral deletions in the GC drain loop
- 99ad555d6452586caf4981fe877c6481f095e417 db: handle degenerate prefixes in random IP allocation
- 10696fa6343a1c96de038b3d032e3663bc83aeb0 db: look up API keys by explicit primary key, not struct condition
- 4c165ae5e7707967a9df941e0b45e5617dfbc1f3 db: reap ephemeral GC watcher goroutine on cancel and reschedule
- e413919810fbe4da024970aa25b7cb0b4caec6da derp: clone regions when merging DERP maps
- 9f0c74e73a50d0bc55c6ee72c7b98dd445f01a48 dns: release lock before extra-records channel send
- 8f75ee56474f606d6926b80957019582d577d6fe docker: head tailscale latest go
- f585f8a94d5c5a3e48f937f7efc71de90882b706 flake, go.mod: move to Go 1.26.4 and refresh dependencies
- 21058d11424d5121dbc1eeb3ba0a39d2f462bfcc flake, go.mod: refresh dependencies
- 29f87e5eaa7bd5509582d2f8626aee7f602db948 flakehashes: refresh vendor hash after dropping gorilla/mux
- f61d21b4a74ff03baed0953ffef2a63e367a4a1a go.mod: drop unused gorilla/mux dependency
- b892b8f254cb1d160f62138790923dd710c19a38 hscontrol: read Apple platform via chi.URLParam, not mux.Vars
- 88044f43ffcfa246ed8692058c994f907f675c72 hscontrol: satisfy golangci-lint on changed lines
- cffdb77c8ba4ba3e61230557fefd376f131d94bc mapper, change: coalesce duplicate policy recomputes per tick
- 5e05652a78670f4374fc6988966ae0f19f4d28e6 mapper: derive incremental visibility from one shared filter
- 8237ac662a85b8aff65dce57c539d8ad1d530ca1 mapper: filter incremental UserProfiles by ACL visibility
- cd1c208980037425c4e6a440edccba2fe16ea85b mapper: filter peer-change patches by ACL visibility
- f4eeb94b1c6932a4cee0f6b4e85ce2a83216548f mapper: gate broadcast sends until a connection's initial map is delivered
- c483bebba8ceadd9ac9ca8e81d9c86c5c6d6d500 mapper: guard nil Hostinfo in addNextDNSMetadata
- efdd9463e9eb9501439196bc2971940b1af48ce6 mapper: keep one batched bundle per node in flight to preserve order
- ad2693ff1385ccd8a3c0c04b3cd9329eea797b22 mapper: record initial-map peers only after delivery
- 0e7b15461776cfda52d562548de72539c985b744 mapper: register reconnecting node atomically against cleanup
- fad8f2a729c941cab03d52bff9c7939b6512f921 mapper: test incremental visibility matches full map
- 9fc88e308f7601a81604a29e9da2f68dcc25570e noise: drop write-only nodeKey field to fix data race
- 71a4ce3c9fa12b9cb440392c0e9325c5a20950b5 noise: re-delegate SSH check when the auth session is missing (#3306)
- 0921972f96c4b4400441ab98053dce772867ab8f oidc: avoid slice panic in getCookieName for short values
- 0fdff0c79b51d1621150b8b353ca29d009d4d63b oidc: set SameSite=Lax on state/nonce CSRF cookies
- bceac495f97f364e0f5a6c2ed67dfa44f52a68c8 policy: add NodeNeedsPeerRecompute predicate
- 56cd3eb24d9cb75b7b14c25ac6fdb3b2c1008f0c policy: guard SSHCheckParams autogroup:self against nil User
- 2c9164b1c4b151878f5159b25152486dec9c0fc8 policy: precompute node routes in the peer-map build
- 40ed210521d880b46504a8d053f3112a7511fec0 policy: read pm.pol under the mutex
- 020560fc5ffe62c4e1059369a71b8d36cc263090 policy: remove unused top-level BuildPeerMap
- 7918187e7ac3eb2ae6b37bb1895c28ecdb531904 servertest: add logout/relogin storm repro with poll churn
- f497b4efd701ae45ae503d93f8b2607a894849d9 state, poll: refcount poll sessions, mark offline only on last release
- a518a5076aebcf0df55c0c2f5741dbc96d34f022 state: batch route auto-approval into one policy rebuild
- 7706552c99b7469a3851cd435052413af5ba1bad state: gate reconnect PolicyChange on NodeNeedsPeerRecompute
- 06d6816dc9511487a2b22a9ebda29b3d0ba0a837 state: keep nil expiry for nodes that stay tagged on reauth
- 2e2401833b5e55b1a9360cb2bd18aaf3c062b056 state: persist live NodeStore node in persistNodeToDB
- 4914f9f2fd054b6d55c291d80ce08c847a9437bd state: reject re-auth claiming another machine's NodeKey
- eb57a3a62bbac3148db2533f2c093b93f34c6443 state: reject registration claiming another machine's NodeKey
- 017162dac12bc0f7119721c8fa6b5c55f2a22ad7 state: signal NodeStore shutdown without closing writeQueue
- 08f186f22acaac3ade69606c8cab4a018f53f694 state: skip database persist for keepalive-only map requests
- 759381ad780b7dc255b9efcf4444eee05223d8f3 types: add ActiveSessions poll session refcount to Node
- 4f67300005e61273573b50ea041c3ac840a1a7eb types: clone Hostinfo before applying DERP change
- 5a70a72988658f9de925c68ccc0b55ede6bed606 types: lock tailcfg DNS config access for extra-records updates
- bb06b905436326dca4887e00353f173a0b54ad4c types: skip malformed derp.urls entries instead of panicking
- 84c99023e5894769954ef9319608935caf525fa0 util: check RNG error before slicing url-safe random string
- ba5434917622a7a9402b6c675870685fb43df4d4 util: handle single-address IPv4 prefix in reverse DNS generation