github juanfont/headscale v0.29.0-beta.1

pre-release3 hours ago

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 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
    • Pre-existing rows with 0001-01-01 00:00:00 are not backfilled; they clear themselves the next time the node re-registers
  • 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

Upgrade

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

Changelog

  • 7f02210 .golangci: ignore tests for goconst, raise occurrence threshold
  • 157e3a3 AGENTS.md: trim to behavioural guidance, drop deprecated sub-agent
  • cfb308b Add FAQ entry to migrate back to default IP prefixes
  • e597f4c Add Headscale UI to web UI documentation
  • df339cd Add a link to Authentik's integration guide
  • f3f84a5 Add docs for policy-wide options and node attributes
  • 890a044 Add more UIs
  • 4eb5899 Add taildrive, tests, sshTests as supported features
  • 542091e Add unit test
  • 20dff82 CHANGELOG: add minimum Tailscale version for 0.29.0
  • 58a85b6 CHANGELOG: bump 0.29.0 minimum tailscale client to v1.80.0
  • f693cc0 CHANGELOG: document grants support for 0.29.0
  • 4e1d83e CHANGELOG: document hostname cleanroom rewrite
  • b52f8cb CHANGELOG: document node.expiry and oidc.expiry deprecation
  • 408f402 CHANGELOG: document nodeAttrs feature and migrations
  • f03d41e CHANGELOG: document policy tests (beta)
  • e78a24b CHANGELOG: document sshTests evaluation (beta)
  • 30d1857 CHANGELOG: document strict version upgrade path
  • ec48f34 CHANGELOG: document subnet-to-subnet ACL fixes
  • fd10741 CHANGELOG: document user-facing changes from #3180
  • bcfaf6a CHANGELOG: note nil expiry preservation fix
  • e3323b6 Describe how to set username instead of SPN for Kanidm
  • dc73376 Dockerfile.tailscale-HEAD,Dockerfile.derper: bump golang to 1.26.3
  • 78570c7 Dockerfile: bump base images
  • e40dbe3 Dockerfile: bump tailscale DERPer builder to Go 1.26.2
  • 6390fce Dockerfile: bump tailscale HEAD builder to Go 1.26.2
  • 7e6c792 Document availability of autgroup:internet
  • faf55f5 Document how to use the provider identifier in the policy
  • 1a64d95 Document supported autogroups once
  • 0f12e41 Explain one approach to update OIDC provider info
  • efd83da Explicitly mention that a headscale username should not end with @
  • d556df1 Extend upgrade guide with backup instructions
  • 4460055 Fix invisible selected menu item
  • a0d6802 Fix minor formatting issue in FAQ
  • c7f221d Fix typo and wording
  • 3672a2d Fix typo in API key creation help text
  • 414d3bb Fix typo in comment about fsnotify behavior
  • c907b0d Fix version in mkdocs
  • 32e1d77 Install config-example.yaml as example for the debian package
  • 9e50071 Link Fosdem 2026 talk
  • 84c7f0d Link to development builds
  • 8028fa5 No longer consider autogroup:self experimental
  • e07b391 Quote autogroup:self in the CHANGELOG
  • acddd73 Reformat docs with mdformat
  • 109bfc4 Refresh docs for Grants
  • c4ab267 Refresh features page
  • 8f60b81 Refresh update path
  • c29bcd2 Release planning happens in milestones
  • 9ea09ea Remove changelog section for 0.28.1
  • 14ce7e9 Remove link to Arch AUR headscale-git
  • 4844628 Remove link to sqlite
  • 892ffff Remove misleading comment
  • 61c9ae8 Remove old migrations for the debian package
  • e13f045 Remove redundant prefix
  • 4bb0241 Require to update from one version to the next
  • edb7ad0 Rewrite ACL docs as policy
  • 45b698d Shorten container introduction
  • 513544c Simplify upgrade snippet with a link to the upgrade guide
  • 8423af2 Swap favicon for updated version
  • 47307d1 Switch to mdformat to format docs
  • f3512d5 Switch to mkdocs-materialx
  • e285f3c The headscale service is enabled by default
  • 3557333 Update config-example links
  • 9baa795 Update docs for auth-id changes
  • 813eb2d Update docs for new HA tracking
  • f1494a3 Update links to Tailscale documentation
  • fda72ad Update main.md
  • 68b0014 Use distroless without quotes
  • 163363a Use docs instead of KB
  • 23a5f1b Use pymdownx.magiclink with its default configuration
  • 97778c9 all: add tests for PingRequest implementation
  • 17236fd all: annotate complex functions with gocyclo rationale
  • 3e2aa58 all: annotate gosec false positives with rationale
  • 93860a5 all: apply formatter changes
  • 4cca631 all: apply godoc [Name] link conventions across comments
  • 43afeed all: apply golangci-lint 2.9.0 fixes
  • ce580f8 all: fix golangci-lint issues (#3064)
  • eccf64e all: fix staticcheck SA4006 in types_test.go
  • 3e0a96e all: fix test flakiness and improve test infrastructure
  • b113655 all: implement PingRequest for node connectivity checking
  • f905d58 all: mechanical lint fixes
  • 742878d all: regenerate generated files for new tool versions
  • 010a556 all: rephrase prose to fit codebase voice
  • 36a73f8 all: update Go dependencies
  • 542cdb2 all: update Go to 1.26.1
  • 70f8141 all: upgrade from Go 1.26rc2 to Go 1.26.0
  • 0f6d312 all: upgrade to Go 1.26rc2 and modernize codebase
  • 4a9a329 all: use lowercase log messages
  • 0567cb6 app: add security headers middleware
  • f7d8bb8 app: remove gRPC reflection from remote server
  • 3033844 app: switch from gorilla to chi mux
  • 8a97dd1 app: wire HA health prober into scheduled tasks
  • f066d12 assets: fix logo alignment and error icon centering
  • 41d70fe auth: check machine key on tailscaled-restart fast path
  • cb3b694 auth: generalise auth flow and introduce AuthVerdict
  • 25ccb5a build: update golangci-lint and gopls in flake
  • eb23c12 capver, types: bump to tailscale v1.98, drop LegacyDERPString
  • 442fcdb capver: regenerate for tailscale v1.96
  • 31c4331 capver: regenerate from docker tags
  • 2530d86 change: document PingRequest merge first-wins foot-gun
  • 4a4032a changelog: document filter rule merging
  • f27298c changelog: document wildcard CGNAT range change Add breaking change entry for the wildcard resolution change to use CGNAT/ULA ranges instead of all IPs. Updates #3036
  • 575d8ec changelog: normalise 0.29.0 BREAKING and Changes sections
  • 9621a97 ci, pre-commit: validate vendor hash via vendorhash check
  • e171d30 ci: add build workflow for main branch
  • 99a93c1 ci: add rolling development tag to container builds
  • 795a1ef ci: fetch full history in golangci-lint job
  • d15ec28 ci: pin Docker to v28 to avoid v29 breaking changes
  • 1b6ab52 ci: regenerate integration test workflow
  • 0f97294 ci: regenerate integration test workflow
  • 5c449db ci: regenerate test-integration.yaml for TestSSHLocalpart
  • a7d405a ci: regenerate test-integration.yaml for TestTailscaleRustAxum
  • 1f9635c ci: restrict test generator to .go files
  • a76b4bd ci: switch integration tests to ARM runners
  • 4d3b567 ci: use overlay2 storage driver instead of pinning docker v28
  • e00c899 cmd, templates, integration: extract shared production constants
  • 461a0e2 cmd/dev: add local development server tool
  • 1a58b77 cmd/dev: validate --port fits the derived-port range
  • 6c08b49 cmd/headscale/cli: add confirmAction helper for force/prompt patterns
  • aae2f7d cmd/headscale/cli: add grpcRun wrapper for gRPC client lifecycle
  • 7b7b270 cmd/headscale/cli: add mustMarkRequired helper for init-time flag validation
  • d6c39e6 cmd/headscale/cli: add printListOutput to centralise table-vs-JSON branching
  • 095106f cmd/headscale/cli: convert remaining commands to RunE
  • 22fccae cmd/headscale/cli: deduplicate expiration parsing and api-key flag validation
  • 2765fd3 cmd/headscale/cli: drop dead flag-read error checks
  • af777f4 cmd/headscale/cli: extract bypassDatabase helper and simplify policy file reads
  • 92a9acc cmd/headscale/cli: mention sshTests in policy check help
  • 7460bec cmd/headscale/cli: move errMissingParameter and Error type to their users
  • 8891ec9 cmd/headscale/cli: remove deprecated output, SuccessOutput, ErrorOutput
  • d72a06c cmd/headscale/cli: remove legacy namespace and machine aliases
  • 13ebea1 cmd/headscale/cli: remove nil resp guards and unexport HasMachineOutputFlag
  • e816397 cmd/headscale/cli: remove no-op Args functions from serveCmd and dumpConfigCmd
  • e6546b2 cmd/headscale/cli: silence cobra error/usage output and centralise error formatting
  • e4fe216 cmd/headscale/cli: switch to RunE with grpcRunE and error returns
  • ca321d3 cmd/headscale/cli: use HeadscaleDateTimeFormat and util.Base10 consistently
  • 4e0c2b8 cmd/headscale/cli: validate users in policy check
  • e470774 cmd/vendorhash: track vendor SRI in flakehashes.json
  • b5090a0 cmd: use zf constants for zerolog field names
  • 3f73ed5 config, types: move randomize_client_port from server config to policy file
  • 8295883 db: enforce strict version upgrade path
  • 73613d7 db: fix database_versions table creation for PostgreSQL
  • 3037e5e db: fix slice aliasing in migration tag merge
  • 0641771 db: guard UsePreAuthKey with WHERE used=false
  • af7e7a4 db: remove unused SetApprovedRoutes and SetTags helpers
  • 7c756b8 db: scope DestroyUser to only delete the target user's pre-auth keys
  • 93e8c72 debug: explain URLIsNoise choice in ping callback
  • d5a4e6e debug: route statsviz through tsweb.Protected
  • 84adda2 doc: add CHANGELOG entries for SSH check and auth commands
  • 585d0c0 docs(config): fix typo in config-example.yaml
  • 01eb540 docs(setup): fix typo in requirements.md
  • 3acce2d errors: rewrite errors to follow go best practices
  • 5105033 feat: add prominent warning banner for non-standard IP prefixes
  • 568baf3 fix: align banner right-side border to consistent 64-char width
  • 25adfaf flake.nix, flake.lock: bump nixpkgs and pinned tools
  • 980622e flake.nix, go.mod: bump tailscale.com to v1.97.0-pre
  • 9c3a3c5 flake: upgrade golangci-lint to 2.9.0 and update nixpkgs
  • 61a14bb gen: regenerate from auth proto changes
  • 570735f gen: regenerate grpc stubs with protoc-gen-go-grpc v1.6.2
  • 64f2313 github: add needs-more-info automation workflow
  • 900f4b7 github: add support-request automation workflow
  • 174e409 github: drop nu flatten in needs-more-info timer
  • e0d8c3c github: fix needs-more-info label race condition
  • a7f981e github: fix needs-more-info label race condition
  • ce5d1ba github: split nu where in needs-more-info timer
  • c1b468f github: update issue template contact links
  • be90910 go.mod, go.sum: bump dependencies for v0.29.0
  • 1f48ebb go.mod: add github.com/realclientip/realclientip-go
  • 2f94b80 go.mod: add stress tool dependency
  • 0cf27eb go.mod: add tstest/mts tool dependency
  • 27f5641 golangci: add forbidigo rule for zerolog field constants
  • 0c6b9f5 goreleaser: remove unused ts2019 build tag
  • 48cc98b hscontrol, cli: add auth register and approve commands
  • 52d454d hscontrol/db: add migration to clear user_id on tagged nodes
  • 7148a69 hscontrol/grpcv1: use EmbedObject and zf constants
  • 53cdeff hscontrol/mapper: use sub-loggers and zf constants
  • f74ea5b hscontrol/policy/v2: add Grant policy format support
  • bc9fb6d hscontrol/policy/v2: reject ambiguous user references at load time
  • b09af38 hscontrol/poll,state: fix grace period disconnect TOCTOU race
  • 4f87241 hscontrol/poll: use sub-logger pattern for mapSession
  • 4e73133 hscontrol/routes: use sub-logger and zf constants
  • ca7362e hscontrol/servertest: add control plane lifecycle and consistency tests
  • f87b086 hscontrol/servertest: add policy, route, ephemeral, and content tests
  • 00c41b6 hscontrol/servertest: add race, stress, and poll race tests
  • ab4e205 hscontrol/servertest: expand issue tests to 24 scenarios, surface 4 issues
  • 3d53f97 hscontrol/servertest: fix test expectations for eventual consistency
  • dd16567 hscontrol/state,db: use zf constants for logging
  • 8048f10 hscontrol/state: extract findExistingNodeForPAK to reduce complexity
  • 1053fbb hscontrol/state: fix online status reset during re-registration
  • 2f907ed hscontrol/types: regenerate types_clone.go for viewer bump
  • 894e694 hscontrol/types: regenerate types_view.go
  • 1059c67 hscontrol/types: silence zerolog by default in tests
  • e0a436c hscontrol/util/zlog/zf: add tag, authkey, and route constants
  • 0288614 hscontrol: add servertest harness for in-process control plane testing
  • 580dcad hscontrol: add tests for SetTags user_id database persistence
  • 7e8930c hscontrol: add tests for default node key expiry
  • 1e4fc3f hscontrol: add tests for deleting users with tagged nodes
  • 75e56df hscontrol: enforce that tagged nodes never have user_id
  • c6c29c0 hscontrol: gate proxy header trust on trusted_proxies
  • 42b8c77 hscontrol: limit /verify request body size
  • 4ad200a hscontrol: preserve nil expiry on tailscaled restart
  • d6dfdc1 hscontrol: route hostname handling through dnsname and NodeStore
  • 91730e2 hscontrol: use EmbedObject for node logging
  • 99767cf hscontrol: validate machine key and bind src/dst in SSH check handler
  • f1e5f13 integration/acl: add tag verification step to TestACLTagPropagationPortSpecific
  • ebc57d9 integration/acl: fix TestACLPolicyPropagationOverTime infrastructure
  • 81b871c integration/acl: replace custom entrypoints with WithPackages
  • a147b0c integration/acl: use CurlFailFast for all negative curl assertions
  • eec3844 integration/dockertestutil: wait for libnetwork settle on reconnect
  • e638cbc integration/tsic: accept via peer-relay in non-direct ping check
  • ea968e2 integration/tsric: add TailscaleRustInContainer package
  • 7bb86f2 integration: HA cable-pull lifecycle test
  • a7edcf3 integration: add CI-scaled timeouts and curl helpers for flaky ACL tests
  • af26bab integration: add HA ping failover test
  • 3db0a48 integration: add SSH check mode tests
  • 2be94ce integration: add TestSSHLocalpart integration test
  • 775bc3a integration: add TestTailscaleRustAxum for tailscale-rs
  • 9b1a6b6 integration: add cap/relay grant peer relay lifecycle test
  • bca6e63 integration: add custom subnet support and fix exit node tests
  • a739862 integration: add via grant route steering tests
  • 98e9ff4 integration: authenticate Docker Hub pulls and retry transient errors
  • ba251e7 integration: cover exit nodes via autogroup:internet ACL (#3212)
  • ecaf56e integration: drop Force flag on docker network disconnect
  • 51eed41 integration: fix ACL tests for address-family-specific resolve
  • 9db5fb6 integration: fix error message assertion for invalid ACL action
  • be4fd9f integration: fix tag tests for tagged nodes with nil user_id
  • bfb6fd8 integration: fixup test
  • dfcc96d integration: harden ACL test ergonomics
  • acb8cfc integration: make docker execute and ping timeouts CI-aware
  • 27c9113 integration: regenerate workflow for HA docker disconnect test
  • 574a618 integration: reject failing sshTests at headscale policy set
  • 3a4af8c integration: remove --accept-routes from via steering routers
  • b762e4c integration: remove exit node via grant tests
  • 78fd6ef integration: replace ad-hoc test timeouts with named constants
  • 155e42f integration: retry transient docker network ops
  • a9a2001 integration: scale remaining hardcoded timeouts and replace pingAllHelper
  • d1443a4 integration: skip subpackage tests in workflow generator
  • e5ebe32 integration: standardize test infrastructure options
  • e44b402 integration: update TestSubnetRouteACL for filter merging and IPProto
  • 210f58f integration: use CI-scaled timeouts for all EventuallyWithT assertions
  • a345a22 mapper, app: ship MagicDNS Routes as empty slices, not nil
  • b3f795f mapper, policy/v2: stamp suggest-exit-node on Peer.CapMap when exit routes approved
  • 6fcff9e mapper, state: deliver nodeAttrs through MapResponse and harden nextdns DoH rewrite
  • 2d549e5 mapper/batcher: add regression tests for M1, M3, M7 fixes
  • 8e26651 mapper/batcher: add regression tests for timer leak and Close lifecycle
  • 9b24a39 mapper/batcher: add scale benchmarks
  • 21e02e5 mapper/batcher: add unit tests and benchmarks
  • feaf85b mapper/batcher: clean up test constants and output
  • 50e8b21 mapper/batcher: fix pointer retention, done-channel init, and connected-map races
  • da33795 mapper/batcher: fix race conditions in cleanup and lookups
  • 86e2798 mapper/batcher: minor production code cleanup
  • 57a38b5 mapper/batcher: reduce hot-path log verbosity
  • 3ebe4d9 mapper/batcher: reduce lock contention with two-phase send
  • afd3a6a mapper/batcher: remove disabled X-prefixed test functions
  • 87b8507 mapper/batcher: replace connected map with per-node disconnectedAt
  • 3276bda mapper/batcher: replace time.After with NewTimer to avoid timer leak
  • 5707068 mapper/batcher: restructure internals for correctness
  • 82c7efc mapper/batcher: serialize per-node work to prevent out-of-order delivery
  • 6031706 mapper/batcher: serialize per-node work to prevent out-of-order delivery
  • 051a38a mapper/batcher: track worker goroutines and stop ticker on Close
  • 3daf45e mapper: close stale map channels after send timeouts
  • 7881f65 mapper: extract node connection types to node_conn.go
  • 9371b4e mapper: fix empty Peers list not clearing client peer state
  • 3587225 mapper: fix phantom updateSentPeers on disconnected nodes
  • b81d6c7 mapper: handle RemoveNode after channel cleanup
  • 6cd919d mapper: include UserProfiles in policy-change MapResponses
  • 2058343 mapper: remove Batcher interface, rename to Batcher struct
  • 427b2f1 matcher: clarify DestsIsTheInternet single-family semantics
  • 64c398f metrics, policy/v2: drop unused scaffolding + nil-error returns
  • 65880ec nix: disable external DERP URL fetch in VM test
  • 37c6a9e nix: sync module options and descriptions with upstream nixpkgs
  • 5e33259 nix: update flake inputs
  • 2109674 nix: update flake inputs and dev shell tool versions
  • c5ef1d3 nix: upgrade dev shell to Python 3.14
  • f20bd0c node: implement disable key expiry via CLI and API
  • 4d427cf noise: limit request body size to prevent unauthenticated OOM
  • 8c6cb05 noise: pass context to sshActionFollowUp
  • e4e742c noise: pin outer RemoteAddr onto tunnel requests
  • 5a7cafd noise: reject non-HEAD on PingResponseHandler
  • d66d3a4 oidc: add confirmation page for node registration
  • 3d0f597 oidc: handle groups claim as string or array (FlexibleStringSlice)
  • 7899049 oidc: render HTML error pages for browser-facing failures
  • adb9467 oidc: validate state parameter length in callback
  • 107c2f2 policy, noise: implement SSH check action
  • c3df84e policy/matcher: include CapGrant.Dsts in match destinations
  • 8358017 policy/v2,state,mapper: implement per-viewer via route steering
  • 078b9e3 policy/v2: SaaS-derived compat tests for nodeAttrs
  • 49744cd policy/v2: accept RFC 3986 bracketed IPv6 in ACL destinations
  • 6c59d3e policy/v2: add SSH compatibility testdata from Tailscale SaaS
  • 2cb914d policy/v2: add SaaS goldens for via-grant prefix containment
  • 995ed01 policy/v2: add advertised routes to compat test topologies
  • 0fa9dca policy/v2: add data-driven grants compatibility test with Tailscale SaaS captures
  • 0acf09b policy/v2: add localpart:*@Domain SSH user compilation
  • b668c7a policy/v2: add policy unmarshal tests for bracketed IPv6
  • c0774a7 policy/v2: add policytester captures recorded from Tailscale SaaS
  • 7bc7011 policy/v2: add policytester compat test runner
  • 26eebce policy/v2: add sshtester compat runner
  • 834ac27 policy/v2: add subnet routes and exit node compatibility tests
  • 5cd5e5d policy/v2: add unit tests for ViaRoutesForPeer
  • 08d26e5 policy/v2: add unit tests for grant filter compilation helpers
  • 6a55f7d policy/v2: add via exit steering golden captures and tests
  • affaa1a policy/v2: align SSH check action with SaaS wire format
  • d600090 policy/v2: align SSH rule validation with Tailscale
  • e4e209f policy/v2: canonicalize Protocol form during unmarshal
  • abd2b15 policy/v2: clean up dead error variables, stale TODO, and test skip reasons
  • 2fb7169 policy/v2: convert ACL compat tests to data-driven format with Tailscale SaaS captures
  • 500442c policy/v2: convert routes compat tests to data-driven format with Tailscale SaaS captures
  • 013dea4 policy/v2: evaluate sshTests at write boundary
  • b29ae25 policy/v2: evaluate the tests block on user-initiated writes
  • f95b254 policy/v2: exclude exit routes from ReduceFilterRules
  • 2e1a716 policy/v2: fix empty grants/acls returning FilterAllowAll
  • 8573ff9 policy/v2: fix grant-only policies returning FilterAllowAll
  • c36cedc policy/v2: fix via grants in BuildPeerMap, MatchersForNode, and ViaRoutesForPeer
  • 28be15f policy/v2: handle autogroup:internet in via grant compilation
  • 0e3acdd policy/v2: implement CapGrant compilation with companion capabilities
  • 687cf08 policy/v2: implement autogroup:danger-all support
  • 4f040de policy/v2: implement grant validation rules matching Tailscale SaaS
  • 54db47b policy/v2: implement via route compilation for grants
  • d5b2837 policy/v2: match default proto set for tests with no proto
  • e5fcd01 policy/v2: match via-grant destinations by prefix overlap
  • a7c9721 policy/v2: overhaul compat test infrastructure
  • a4f05b0 policy/v2: parse, validate, and compile nodeAttrs
  • ebe0f40 policy/v2: preserve non-wildcard source IPs alongside wildcard ranges
  • 9f7aa55 policy/v2: refactor alias resolution to use ResolvedAddresses
  • dda3584 policy/v2: reorder ACL self grants to match Tailscale rule ordering
  • 2b7f15a policy/v2: surface autogroup:internet via grants on exit nodes
  • e05f45c policy/v2: use approved node routes in wildcard SrcIPs
  • 927ce41 policy/v2: use bare IPs in autogroup:self DstPorts
  • ccd284c policy/v2: use per-node filter compilation for via grants
  • 6a0a297 policy/v2: validate sshTests at parse
  • f172dba policy/v2: validate tests block at parse boundary
  • b051e7b policy/v2: wire PolicyManager through compiledGrant
  • f735502 policy: add ICMP protocols to default and export constants When ACL rules don't specify a protocol, Headscale now defaults to [TCP, UDP, ICMP, ICMPv6] instead of just [TCP, UDP], matching Tailscale's behavior. Also export protocol number constants (ProtocolTCP, ProtocolUDP, etc.) for use in external test packages, renaming the string protocol constants to ProtoNameTCP, ProtoNameUDP, etc. to avoid conflicts. This resolves 78 ICMP-related TODOs in the Tailscale compatibility tests, reducing the total from 165 to 87.
  • 53d17aa policy: add comprehensive Tailscale ACL compatibility tests Add extensive test coverage verifying Headscale's ACL policy behavior matches Tailscale's coordination server. Tests cover: - Source/destination resolution for users, groups, tags, hosts, IPs - autogroup:member, autogroup:tagged, autogroup:self behavior - Filter rule deduplication and merging semantics - Multi-rule interaction patterns - Error case validation Key behavioral differences documented: - Headscale creates separate filter entries per ACL rule; Tailscale merges rules with identical sources - Headscale deduplicates Dsts within a rule; Tailscale does not - Headscale does not validate autogroup:self source restrictions for ACL rules (only SSH rules); Tailscale rejects invalid sources Tests are based on real Tailscale coordination server responses captured from a test environment with 5 nodes (1 user-owned, 4 tagged).
  • 835b7eb policy: autogroup:internet does not generate packet filters
  • 14f833b policy: fix autogroup:self handling for tagged nodes Skip autogroup:self destination processing for tagged nodes since they can never match autogroup:self (which only applies to user-owned nodes). Also reorder the IsTagged() check to short-circuit before accessing User() to avoid potential nil pointer access on tagged nodes.
  • 95b1fd6 policy: fix wildcard DstPorts format and proto:icmp handling
  • 93d79d8 policy: include IPv6 in identity-based alias resolution
  • 0b1727c policy: merge filter rules with identical SrcIPs and IPProto
  • c7a0ca7 policy: surface exit nodes via autogroup:internet (#3212)
  • 29aa08d policy: update test expectations for merged filter rules
  • 8baa14e policy: use CGNAT/ULA ranges for wildcard resolution Change Asterix.Resolve() to use 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). This better matches Tailscale's security model where wildcard (*) means "any node in the tailnet" rather than literally "any IP address on the internet". Updates #3036
  • 08fe2e4 policy: use CIDR format for autogroup:self destinations
  • ebdbe03 policy: validate autogroup:self sources in ACL rules Tailscale validates that autogroup:self destinations in ACL rules can only be used when ALL sources are users, groups, autogroup:member, or wildcard (). Previously, Headscale only performed this validation for SSH rules. Add validateACLSrcDstCombination() to enforce that tags, autogroup:tagged, hosts, and raw IPs cannot be used as sources with autogroup:self destinations. Invalid policies like tag:client → autogroup:self:* are now rejected at validation time, matching Tailscale behavior. Wildcard () is allowed because autogroup:self evaluation narrows it per-node to only the node's own IPs.
  • ded51a4 policyutil: fix reduceCapGrantRule and add route reduction
  • fffc58b poll: fix poll test linter violations
  • 4aca9d6 poll: stop stale map sessions through an explicit teardown hook
  • dc0e52a proto: add AuthRegister and AuthApprove RPCs
  • 56146de proto: add CheckPolicy RPC
  • a8f7fed proto: add disable_expiry field to ExpireNodeRequest
  • 786ce2d routes: add health dimension to HA primary route election
  • 863fa2f servertest, integration: cover HA both-offline recovery
  • 0378e2d servertest: add HA health probing tests
  • 164d659 servertest: add TestViaGrantHACompat for via+HA compat tests
  • 436d3db servertest: add dynamic HA failover tests
  • 0431039 servertest: add regression tests for via grant filter rules
  • 7d104b8 servertest: add via grant map compat tests
  • b5b786f servertest: cover broader-dst via grant in filter test
  • 76ee293 servertest: cover via-grant exit-node visibility end-to-end
  • 53b8a81 servertest: support tagged pre-auth keys in test clients
  • ff29af6 servertest: use memnet networking and add WithNodeExpiry option
  • 7bab8da state, policy, noise: implement SSH check period auto-approval
  • e2f2f92 state, servertest: property-test HA election + invariant catalogue
  • 3ca4ff8 state,servertest: add grant control plane tests and fix via route ReduceRoutes filtering
  • 90e65cc state: add HA health prober
  • b1196ba state: add regression test for Node slice persistence
  • 6337a3d state: apply default node key expiry on registration
  • a2c3ac0 state: auto-bump GivenName on collision and add SetGivenName
  • 01e548e state: avoid nil deref in registration handlers when old user is missing
  • de6be71 state: batch HA probe results so dual-disconnect cannot flap primary
  • 9f7c8e9 state: clear Unhealthy when node leaves HA candidate set
  • da927eb state: compute primary routes inside NodeStore snapshot
  • fb8eeca state: defer HA failover when probe target reconnected mid-cycle
  • 66ac785 state: delete routes package, port primary route tests
  • 842f362 state: drain pending pings on Close
  • ccddece state: fix GORM not persisting user_id=NULL on tagged node conversion
  • 6ae1826 state: fix policy change race in UpdateNodeFromMapRequest
  • 82bb433 state: fix routesChanged mutating input Hostinfo
  • c7630b5 state: leave prefix unmapped when all primary candidates unhealthy
  • de60982 state: note tagged-path coverage and self-healing behaviour for #3199
  • 94ec607 state: per-goroutine deadline in HA probe cycle
  • 0e10ca4 state: preserve nil expiry on user owned registration when no default is configured
  • 3d5c0af state: preserve previous primary when all HA advertisers unhealthy
  • 0d4f229 state: replace zcache with bounded LRU for auth cache
  • 437754a state: switch consumers to NodeStore primary routes
  • 978f1e3 state: tie-break ResolveNode by GivenName then lowest NodeID
  • 380f531 state: trigger PolicyChange on every Connect and Disconnect
  • 0e5569c templates: add detailsBox collapsible component
  • c15caff templates: add error box component and error page template
  • f3eb9a7 templates: escape query value in ping page
  • 4a7e147 templates: generalise auth templates for web and OIDC
  • 814226f templates: improve accessibility, dark mode, and typography
  • c9dbea5 templates: improve ping page spacing and design system usage
  • 3918020 templates: use CSS variables in all shared components
  • de5b1ea templates: use table layout for registration confirm details
  • f34dec2 testcapture: add typed capture format package
  • f49c42e testdata: add SaaS captures for compat tests
  • 30dce30 testdata: convert .json to .hujson with header comments
  • 9482cdf testdata: drop unused uppercase SSH-*.hujson fixtures
  • 835db97 testdata: strip unused fields from all test data files (23MB -> 4MB)
  • d243ada types,mapper,integration: enable Taildrive and add cap/drive grant lifecycle test
  • 2a2d5c8 types/change: fix slice aliasing in Change.Merge
  • cef5338 types/change: panic on Merge with conflicting TargetNode values
  • 64d13f7 types/config, types/node: model default-auto-update from auto_update.enabled
  • 5ebc53c types/node, mapper, policy/v2: assemble self CapMap inside TailNode
  • 5d502bf types/node, mapper: strip own IPv4 from emission when node has disable-ipv4 cap
  • 8ea4cd3 types/node, policy/v2: drop taildrive caps from baseline emission
  • cf3d30b types: add MarshalZerologObject to domain types
  • 1fe682b types: add Unhealthy and SessionEpoch fields to Node
  • 4d0b273 types: add node.expiry config, deprecate oidc.expiry
  • 610c1da types: avoid NodeView clone in CanAccess
  • b01e67e types: consider subnet routes as source identity in ACL matching
  • 3529fe0 types: fix OIDC identifier path traversal dropping subject
  • 4064f13 types: fix nil panics in Owner() and TailscaleUserID() for orphaned nodes
  • 15c1cfd types: include ExitRoutes in HasNetworkChanges
  • 942313a types: move DebugRoutes from routes to types
  • a3c4ad2 types: omit secret fields from JSON marshalling
  • 7a20db9 types: persist Node JSON slices via named IsZero types
  • 5802069 zlog: add utility package for safe and consistent logging

Don't miss a new headscale release

NewReleases is sending notifications on new releases.