From f424f64a6ebaa72fe318c2cfa288422d3f20501e Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 13 Jul 2025 10:57:04 +0000 Subject: [PATCH] Deployed a8f2eebf to development with MkDocs 1.6.1 and mike 2.1.3 --- development/ref/tls/index.html | 2 +- development/search/search_index.json | 2 +- development/sitemap.xml | 56 +++++++++++++-------------- development/sitemap.xml.gz | Bin 430 -> 430 bytes 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/development/ref/tls/index.html b/development/ref/tls/index.html index 01bf19a0..82b159d0 100644 --- a/development/ref/tls/index.html +++ b/development/ref/tls/index.html @@ -1,4 +1,4 @@ - TLS - Headscale
Skip to content

Running the service via TLS (optional)

Bring your own certificate

Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the tls_cert_path and tls_cert_path configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_cert_path: ""
+ TLS - Headscale      

Running the service via TLS (optional)

Bring your own certificate

Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the tls_cert_path and tls_key_path configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_cert_path: ""
 tls_key_path: ""
 

The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.

Let's Encrypt / ACME

To get a certificate automatically via Let's Encrypt, set tls_letsencrypt_hostname to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the server_url configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in tls_letsencrypt_cache_dir. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_letsencrypt_hostname: ""
 tls_letsencrypt_listen: ":http"
diff --git a/development/search/search_index.json b/development/search/search_index.json
index 63957405..8e69df53 100644
--- a/development/search/search_index.json
+++ b/development/search/search_index.json
@@ -1 +1 @@
-{"config":{"lang":["en"],"separator":"[\\s\\-,:!=\\[\\]()\"`/]+|\\.(?!\\d)|&[lg]t;|(?!\\b)(?=[A-Z][a-z])","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to headscale","text":"

Headscale is an open source, self-hosted implementation of the Tailscale control server.

This page contains the documentation for the latest version of headscale. Please also check our FAQ.

Join our Discord server for a chat and community support.

"},{"location":"#design-goal","title":"Design goal","text":"

Headscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscale's goal is to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. It implements a narrow scope, a single Tailscale network (tailnet), suitable for a personal use, or a small open-source organisation.

"},{"location":"#supporting-headscale","title":"Supporting headscale","text":"

Please see Sponsor for more information.

"},{"location":"#contributing","title":"Contributing","text":"

Headscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be discussed with the Maintainers before being submitted.

Please see Contributing for more information.

"},{"location":"#about","title":"About","text":"

Headscale is maintained by Kristoffer Dalby and Juan Font.

"},{"location":"about/clients/","title":"Client and operating system support","text":"

We aim to support the last 10 releases of the Tailscale client on all provided operating systems and platforms. Some platforms might require additional configuration to connect with headscale.

OS Supports headscale Linux Yes OpenBSD Yes FreeBSD Yes Windows Yes (see docs and /windows on your headscale for more information) Android Yes (see docs for more information) macOS Yes (see docs and /apple on your headscale for more information) iOS Yes (see docs and /apple on your headscale for more information) tvOS Yes (see docs and /apple on your headscale for more information)"},{"location":"about/contributing/","title":"Contributing","text":"

Headscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be discussed with the maintainers before being added to the project. This model has been chosen to reduce the risk of burnout by limiting the maintenance overhead of reviewing and validating third-party code.

"},{"location":"about/contributing/#why-do-we-have-this-model","title":"Why do we have this model?","text":"

Headscale has a small maintainer team that tries to balance working on the project, fixing bugs and reviewing contributions.

When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops.

Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly.

The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added.

This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature.

"},{"location":"about/contributing/#what-do-we-require","title":"What do we require?","text":"

A general description is provided here and an explicit list is provided in our pull request template.

All new features have to start out with a design document, which should be discussed on the issue tracker (not discord). It should include a use case for the feature, how it can be implemented, who will implement it and a plan for maintaining it.

All features have to be end-to-end tested (integration tests) and have good unit test coverage to ensure that they work as expected. This will also ensure that the feature continues to work as expected over time. If a change cannot be tested, a strong case for why this is not possible needs to be presented.

The contributor should help to maintain the feature over time. In case the feature is not maintained probably, the maintainers reserve themselves the right to remove features they redeem as unmaintainable. This should help to improve the quality of the software and keep it in a maintainable state.

"},{"location":"about/contributing/#bug-fixes","title":"Bug fixes","text":"

Headscale is open to code contributions for bug fixes without discussion.

"},{"location":"about/contributing/#documentation","title":"Documentation","text":"

If you find mistakes in the documentation, please submit a fix to the documentation.

"},{"location":"about/faq/","title":"Frequently Asked Questions","text":""},{"location":"about/faq/#what-is-the-design-goal-of-headscale","title":"What is the design goal of headscale?","text":"

Headscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscale's goal is to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. It implements a narrow scope, a single Tailscale network (tailnet), suitable for a personal use, or a small open-source organisation.

"},{"location":"about/faq/#how-can-i-contribute","title":"How can I contribute?","text":"

Headscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be discussed with the Maintainers before being submitted.

Please see Contributing for more information.

"},{"location":"about/faq/#why-is-acknowledged-contribution-the-chosen-model","title":"Why is 'acknowledged contribution' the chosen model?","text":"

Both maintainers have full-time jobs and families, and we want to avoid burnout. We also want to avoid frustration from contributors when their PRs are not accepted.

We are more than happy to exchange emails, or to have dedicated calls before a PR is submitted.

"},{"location":"about/faq/#whenwhy-is-feature-x-going-to-be-implemented","title":"When/Why is Feature X going to be implemented?","text":"

We don't know. We might be working on it. If you're interested in contributing, please post a feature request about it.

Please be aware that there are a number of reasons why we might not accept specific contributions:

  • It is not possible to implement the feature in a way that makes sense in a self-hosted environment.
  • Given that we are reverse-engineering Tailscale to satisfy our own curiosity, we might be interested in implementing the feature ourselves.
  • You are not sending unit and integration tests with it.
"},{"location":"about/faq/#do-you-support-y-method-of-deploying-headscale","title":"Do you support Y method of deploying headscale?","text":"

We currently support deploying headscale using our binaries and the DEB packages. Visit our installation guide using official releases for more information.

In addition to that, you may use packages provided by the community or from distributions. Learn more in the installation guide using community packages.

For convenience, we also build container images with headscale. But please be aware that we don't officially support deploying headscale using Docker. On our Discord server we have a \"docker-issues\" channel where you can ask for Docker-specific help to the community.

"},{"location":"about/faq/#scaling-how-many-clients-does-headscale-support","title":"Scaling / How many clients does Headscale support?","text":"

It depends. As often stated, Headscale is not enterprise software and our focus is homelabbers and self-hosters. Of course, we do not prevent people from using it in a commercial/professional setting and often get questions about scaling.

Please note that when Headscale is developed, performance is not part of the consideration as the main audience is considered to be users with a moddest amount of devices. We focus on correctness and feature parity with Tailscale SaaS over time.

To understand if you might be able to use Headscale for your usecase, I will describe two scenarios in an effort to explain what is the central bottleneck of Headscale:

  1. An environment with 1000 servers

  2. they rarely \"move\" (change their endpoints)

  3. new nodes are added rarely

  4. An environment with 80 laptops/phones (end user devices)

  5. nodes move often, e.g. switching from home to office

Headscale calculates a map of all nodes that need to talk to each other, creating this \"world map\" requires a lot of CPU time. When an event that requires changes to this map happens, the whole \"world\" is recalculated, and a new \"world map\" is created for every node in the network.

This means that under certain conditions, Headscale can likely handle 100s of devices (maybe more), if there is little to no change happening in the network. For example, in Scenario 1, the process of computing the world map is extremly demanding due to the size of the network, but when the map has been created and the nodes are not changing, the Headscale instance will likely return to a very low resource usage until the next time there is an event requiring the new map.

In the case of Scenario 2, the process of computing the world map is less demanding due to the smaller size of the network, however, the type of nodes will likely change frequently, which would lead to a constant resource usage.

Headscale will start to struggle when the two scenarios overlap, e.g. many nodes with frequent changes will cause the resource usage to remain constantly high. In the worst case scenario, the queue of nodes waiting for their map will grow to a point where Headscale never will be able to catch up, and nodes will never learn about the current state of the world.

We expect that the performance will improve over time as we improve the code base, but it is not a focus. In general, we will never make the tradeoff to make things faster on the cost of less maintainable or readable code. We are a small team and have to optimise for maintainabillity.

"},{"location":"about/faq/#which-database-should-i-use","title":"Which database should I use?","text":"

We recommend the use of SQLite as database for headscale:

  • SQLite is simple to setup and easy to use
  • It scales well for all of headscale's usecases
  • Development and testing happens primarily on SQLite
  • PostgreSQL is still supported, but is considered to be in \"maintenance mode\"

The headscale project itself does not provide a tool to migrate from PostgreSQL to SQLite. Please have a look at the related tools documentation for migration tooling provided by the community.

The choice of database has little to no impact on the performance of the server, see Scaling / How many clients does Headscale support? for understanding how Headscale spends its resources.

"},{"location":"about/faq/#why-is-my-reverse-proxy-not-working-with-headscale","title":"Why is my reverse proxy not working with headscale?","text":"

We don't know. We don't use reverse proxies with headscale ourselves, so we don't have any experience with them. We have community documentation on how to configure various reverse proxies, and a dedicated \"reverse-proxy-issues\" channel on our Discord server where you can ask for help to the community.

"},{"location":"about/faq/#can-i-use-headscale-and-tailscale-on-the-same-machine","title":"Can I use headscale and tailscale on the same machine?","text":"

Running headscale on a machine that is also in the tailnet can cause problems with subnet routers, traffic relay nodes, and MagicDNS. It might work, but it is not supported.

"},{"location":"about/faq/#why-do-two-nodes-see-each-other-in-their-status-even-if-an-acl-allows-traffic-only-in-one-direction","title":"Why do two nodes see each other in their status, even if an ACL allows traffic only in one direction?","text":"

A frequent use case is to allow traffic only from one node to another, but not the other way around. For example, the workstation of an administrator should be able to connect to all nodes but the nodes themselves shouldn't be able to connect back to the administrator's node. Why do all nodes see the administrator's workstation in the output of tailscale status?

This is essentially how Tailscale works. If traffic is allowed to flow in one direction, then both nodes see each other in their output of tailscale status. Traffic is still filtered according to the ACL, with the exception of tailscale ping which is always allowed in either direction.

See also https://tailscale.com/kb/1087/device-visibility.

"},{"location":"about/features/","title":"Features","text":"

Headscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscale's goal is to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. This page provides on overview of Headscale's feature and compatibility with the Tailscale control server:

  • Full \"base\" support of Tailscale's features
  • Node registration
    • Interactive
    • Pre authenticated key
  • DNS
    • MagicDNS
    • Global and restricted nameservers (split DNS)
    • search domains
    • Extra DNS records (Headscale only)
  • Taildrop (File Sharing)
  • Routes
    • Subnet routers
    • Exit nodes
  • Dual stack (IPv4 and IPv6)
  • Ephemeral nodes
  • Embedded DERP server
  • Access control lists (GitHub label \"policy\")
    • ACL management via API
    • Some Autogroups, currently: autogroup:internet, autogroup:nonroot, autogroup:member, autogroup:tagged
    • Auto approvers for subnet routers and exit nodes
    • Tailscale SSH
  • Node registration using Single-Sign-On (OpenID Connect) (GitHub label \"OIDC\")
    • Basic registration
    • Update user profile from identity provider
    • OIDC groups cannot be used in ACLs
  • Funnel (#1040)
  • Serve (#1234)
  • Network flow logs (#1687)
"},{"location":"about/help/","title":"Getting help","text":"

Join our Discord server for announcements and community support.

Please report bugs via GitHub issues

"},{"location":"about/releases/","title":"Releases","text":"

All headscale releases are available on the GitHub release page. Those releases are available as binaries for various platforms and architectures, packages for Debian based systems and source code archives. Container images are available on Docker Hub and GitHub Container Registry.

An Atom/RSS feed of headscale releases is available here.

See the \"announcements\" channel on our Discord server for news about headscale.

"},{"location":"about/sponsor/","title":"Sponsor","text":"

If you like to support the development of headscale, please consider a donation via ko-fi.com/headscale. Thank you!

"},{"location":"ref/acls/","title":"ACLs","text":"

Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.

For instance, instead of referring to users when defining groups you must use users (which are the equivalent to user/logins in Tailscale.com).

Please check https://tailscale.com/kb/1018/acls/ for further information.

When using ACL's the User borders are no longer applied. All machines whichever the User have the ability to communicate with other hosts as long as the ACL's permits this exchange.

"},{"location":"ref/acls/#acls-use-case-example","title":"ACLs use case example","text":"

Let's build an example use case for a small business (It may be the place where ACL's are the most useful).

We have a small company with a boss, an admin, two developers and an intern.

The boss should have access to all servers but not to the user's hosts. Admin should also have access to all hosts except that their permissions should be limited to maintaining the hosts (for example purposes). The developers can do anything they want on dev hosts but only watch on productions hosts. Intern can only interact with the development servers.

There's an additional server that acts as a router, connecting the VPN users to an internal network 10.20.0.0/16. Developers must have access to those internal resources.

Each user have at least a device connected to the network and we have some servers.

  • database.prod
  • database.dev
  • app-server1.prod
  • app-server1.dev
  • billing.internal
  • router.internal

"},{"location":"ref/acls/#acl-setup","title":"ACL setup","text":"

ACLs have to be written in huJSON.

When registering the servers we will need to add the flag --advertise-tags=tag:<tag1>,tag:<tag2>, and the user that is registering the server should be allowed to do it. Since anyone can add tags to a server they can register, the check of the tags is done on headscale server and only valid tags are applied. A tag is valid if the user that is registering it is allowed to do it.

To use ACLs in headscale, you must edit your config.yaml file. In there you will find a policy.path parameter. This will need to point to your ACL file. More info on how these policies are written can be found here.

Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service (sudo systemctl reload headscale) or by sending a SIGHUP signal (sudo kill -HUP $(pidof headscale)) to the main process. Headscale logs the result of ACL policy processing after each reload.

Here are the ACL's to implement the same permissions as above:

acl.json
{\n  // groups are collections of users having a common scope. A user can be in multiple groups\n  // groups cannot be composed of groups\n  \"groups\": {\n    \"group:boss\": [\"boss@\"],\n    \"group:dev\": [\"dev1@\", \"dev2@\"],\n    \"group:admin\": [\"admin1@\"],\n    \"group:intern\": [\"intern1@\"]\n  },\n  // tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.\n  // This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)\n  // and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)\n  \"tagOwners\": {\n    // the administrators can add servers in production\n    \"tag:prod-databases\": [\"group:admin\"],\n    \"tag:prod-app-servers\": [\"group:admin\"],\n\n    // the boss can tag any server as internal\n    \"tag:internal\": [\"group:boss\"],\n\n    // dev can add servers for dev purposes as well as admins\n    \"tag:dev-databases\": [\"group:admin\", \"group:dev\"],\n    \"tag:dev-app-servers\": [\"group:admin\", \"group:dev\"]\n\n    // interns cannot add servers\n  },\n  // hosts should be defined using its IP addresses and a subnet mask.\n  // to define a single host, use a /32 mask. You cannot use DNS entries here,\n  // as they're prone to be hijacked by replacing their IP addresses.\n  // see https://github.com/tailscale/tailscale/issues/3800 for more information.\n  \"hosts\": {\n    \"postgresql.internal\": \"10.20.0.2/32\",\n    \"webservers.internal\": \"10.20.10.1/29\"\n  },\n  \"acls\": [\n    // boss have access to all servers\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:boss\"],\n      \"dst\": [\n        \"tag:prod-databases:*\",\n        \"tag:prod-app-servers:*\",\n        \"tag:internal:*\",\n        \"tag:dev-databases:*\",\n        \"tag:dev-app-servers:*\"\n      ]\n    },\n\n    // admin have only access to administrative ports of the servers, in tcp/22\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:admin\"],\n      \"proto\": \"tcp\",\n      \"dst\": [\n        \"tag:prod-databases:22\",\n        \"tag:prod-app-servers:22\",\n        \"tag:internal:22\",\n        \"tag:dev-databases:22\",\n        \"tag:dev-app-servers:22\"\n      ]\n    },\n\n    // we also allow admin to ping the servers\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:admin\"],\n      \"proto\": \"icmp\",\n      \"dst\": [\n        \"tag:prod-databases:*\",\n        \"tag:prod-app-servers:*\",\n        \"tag:internal:*\",\n        \"tag:dev-databases:*\",\n        \"tag:dev-app-servers:*\"\n      ]\n    },\n\n    // developers have access to databases servers and application servers on all ports\n    // they can only view the applications servers in prod and have no access to databases servers in production\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:dev\"],\n      \"dst\": [\n        \"tag:dev-databases:*\",\n        \"tag:dev-app-servers:*\",\n        \"tag:prod-app-servers:80,443\"\n      ]\n    },\n    // developers have access to the internal network through the router.\n    // the internal network is composed of HTTPS endpoints and Postgresql\n    // database servers.\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:dev\"],\n      \"dst\": [\"10.20.0.0/16:443,5432\"]\n    },\n\n    // servers should be able to talk to database in tcp/5432. Database should not be able to initiate connections to\n    // applications servers\n    {\n      \"action\": \"accept\",\n      \"src\": [\"tag:dev-app-servers\"],\n      \"proto\": \"tcp\",\n      \"dst\": [\"tag:dev-databases:5432\"]\n    },\n    {\n      \"action\": \"accept\",\n      \"src\": [\"tag:prod-app-servers\"],\n      \"dst\": [\"tag:prod-databases:5432\"]\n    },\n\n    // interns have access to dev-app-servers only in reading mode\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:intern\"],\n      \"dst\": [\"tag:dev-app-servers:80,443\"]\n    },\n\n    // We still have to allow internal users communications since nothing guarantees that each user have\n    // their own users.\n    { \"action\": \"accept\", \"src\": [\"boss@\"], \"dst\": [\"boss@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"dev1@\"], \"dst\": [\"dev1@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"dev2@\"], \"dst\": [\"dev2@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"admin1@\"], \"dst\": [\"admin1@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"intern1@\"], \"dst\": [\"intern1@:*\"] }\n  ]\n}\n
"},{"location":"ref/configuration/","title":"Configuration","text":"
  • Headscale loads its configuration from a YAML file
  • It searches for config.yaml in the following paths:
    • /etc/headscale
    • $HOME/.headscale
    • the current working directory
  • To load the configuration from a different path, use:
    • the command line flag -c, --config
    • the environment variable HEADSCALE_CONFIG
  • Validate the configuration file with: headscale configtest

Get the example configuration from the GitHub repository

Always select the same GitHub tag as the released version you use to ensure you have the correct example configuration. The main branch might contain unreleased changes.

View on GitHubDownload with wgetDownload with curl
  • Development version: https://github.com/juanfont/headscale/blob/main/config-example.yaml
  • Version 0.26.0: https://github.com/juanfont/headscale/blob/v0.26.0/config-example.yaml
# Development version\nwget -O config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml\n\n# Version 0.26.0\nwget -O config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.26.0/config-example.yaml\n
# Development version\ncurl -o config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml\n\n# Version 0.26.0\ncurl -o config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.26.0/config-example.yaml\n
"},{"location":"ref/dns/","title":"DNS","text":"

Headscale supports most DNS features from Tailscale. DNS related settings can be configured within dns section of the configuration file.

"},{"location":"ref/dns/#setting-extra-dns-records","title":"Setting extra DNS records","text":"

Headscale allows to set extra DNS records which are made available via MagicDNS. Extra DNS records can be configured either via static entries in the configuration file or from a JSON file that Headscale continuously watches for changes:

  • Use the dns.extra_records option in the configuration file for entries that are static and don't change while Headscale is running. Those entries are processed when Headscale is starting up and changes to the configuration require a restart of Headscale.
  • For dynamic DNS records that may be added, updated or removed while Headscale is running or DNS records that are generated by scripts the option dns.extra_records_path in the configuration file is useful. Set it to the absolute path of the JSON file containing DNS records and Headscale processes this file as it detects changes.

An example use case is to serve multiple apps on the same host via a reverse proxy like NGINX, in this case a Prometheus monitoring stack. This allows to nicely access the service with \"http://grafana.myvpn.example.com\" instead of the hostname and port combination \"http://hostname-in-magic-dns.myvpn.example.com:3000\".

Limitations

Currently, only A and AAAA records are processed by Tailscale.

  1. Configure extra DNS records using one of the available configuration options:

    Static entries, via dns.extra_recordsDynamic entries, via dns.extra_records_path config.yaml
    dns:\n  ...\n  extra_records:\n    - name: \"grafana.myvpn.example.com\"\n      type: \"A\"\n      value: \"100.64.0.3\"\n\n    - name: \"prometheus.myvpn.example.com\"\n      type: \"A\"\n      value: \"100.64.0.3\"\n  ...\n

    Restart your headscale instance.

    extra-records.json
    [\n  {\n    \"name\": \"grafana.myvpn.example.com\",\n    \"type\": \"A\",\n    \"value\": \"100.64.0.3\"\n  },\n  {\n    \"name\": \"prometheus.myvpn.example.com\",\n    \"type\": \"A\",\n    \"value\": \"100.64.0.3\"\n  }\n]\n

    Headscale picks up changes to the above JSON file automatically.

    Good to know

    • The dns.extra_records_path option in the configuration file needs to reference the JSON file containing extra DNS records.
    • Be sure to \"sort keys\" and produce a stable output in case you generate the JSON file with a script. Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.
  2. Verify that DNS records are properly set using the DNS querying tool of your choice:

    Query with digQuery with drill
    dig +short grafana.myvpn.example.com\n100.64.0.3\n
    drill -Q grafana.myvpn.example.com\n100.64.0.3\n
  3. Optional: Setup the reverse proxy

    The motivating example here was to be able to access internal monitoring services on the same host without specifying a port, depicted as NGINX configuration snippet:

    nginx.conf
    server {\n    listen 80;\n    listen [::]:80;\n\n    server_name grafana.myvpn.example.com;\n\n    location / {\n        proxy_pass http://localhost:3000;\n        proxy_set_header Host $http_host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n}\n
"},{"location":"ref/oidc/","title":"OpenID Connect","text":"

Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features:

  • Autoconfiguration via OpenID Connect Discovery Protocol
  • Proof Key for Code Exchange (PKCE) code verification
  • Authorization based on a user's domain, email address or group membership
  • Synchronization of standard OIDC claims

Please see limitations for known issues and limitations.

"},{"location":"ref/oidc/#configuration","title":"Configuration","text":"

OpenID requires configuration in Headscale and your identity provider:

  • Headscale: The oidc section of the Headscale configuration contains all available configuration options along with a description and their default values.
  • Identity provider: Please refer to the official documentation of your identity provider for specific instructions. Additionally, there might be some useful hints in the Identity provider specific configuration section below.
"},{"location":"ref/oidc/#basic-configuration","title":"Basic configuration","text":"

A basic configuration connects Headscale to an identity provider and typically requires:

  • OpenID Connect Issuer URL from the identity provider. Headscale uses the OpenID Connect Discovery Protocol 1.0 to automatically obtain OpenID configuration parameters (example: https://sso.example.com).
  • Client ID from the identity provider (example: headscale).
  • Client secret generated by the identity provider (example: generated-secret).
  • Redirect URI for your identity provider (example: https://headscale.example.com/oidc/callback).
HeadscaleIdentity provider
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n
  • Create a new confidential client (Client ID, Client secret)
  • Add Headscale's OIDC callback URL as valid redirect URL: https://headscale.example.com/oidc/callback
  • Configure additional parameters to improve user experience such as: name, description, logo, \u2026
"},{"location":"ref/oidc/#enable-pkce-recommended","title":"Enable PKCE (recommended)","text":"

Proof Key for Code Exchange (PKCE) adds an additional layer of security to the OAuth 2.0 authorization code flow by preventing authorization code interception attacks, see: https://datatracker.ietf.org/doc/html/rfc7636. PKCE is recommended and needs to be configured for Headscale and the identity provider alike:

HeadscaleIdentity provider
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  pkce:\n    enabled: true\n
  • Enable PKCE for the headscale client
  • Set the PKCE challenge method to \"S256\"
"},{"location":"ref/oidc/#authorize-users-with-filters","title":"Authorize users with filters","text":"

Headscale allows to filter for allowed users based on their domain, email address or group membership. These filters can be helpful to apply additional restrictions and control which users are allowed to join. Filters are disabled by default, users are allowed to join once the authentication with the identity provider succeeds. In case multiple filters are configured, a user needs to pass all of them.

Allowed domainsAllowed users/emailsAllowed groups
  • Check the email domain of each authenticating user against the list of allowed domains and only authorize users whose email domain matches example.com.
  • Access allowed: alice@example.com
  • Access denied: bob@example.net
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  allowed_domains:\n    - \"example.com\"\n
  • Check the email address of each authenticating user against the list of allowed email addresses and only authorize users whose email is part of the allowed_users list.
  • Access allowed: alice@example.com, bob@example.net
  • Access denied: mallory@example.net
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  allowed_users:\n    - \"alice@example.com\"\n    - \"bob@example.net\"\n
  • Use the OIDC groups claim of each authenticating user to get their group membership and only authorize users which are members in at least one of the referenced groups.
  • Access allowed: users in the headscale_users group
  • Access denied: users without groups, users with other groups
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  scope: [\"openid\", \"profile\", \"email\", \"groups\"]\n  allowed_groups:\n    - \"headscale_users\"\n
"},{"location":"ref/oidc/#customize-node-expiration","title":"Customize node expiration","text":"

The node expiration is the amount of time a node is authenticated with OpenID Connect until it expires and needs to reauthenticate. The default node expiration is 180 days. This can either be customized or set to the expiration from the Access Token.

Customize node expirationUse expiration from Access Token
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  expiry: 30d   # Use 0 to disable node expiration\n

Please keep in mind that the Access Token is typically a short-lived token that expires within a few minutes. You will have to configure token expiration in your identity provider to avoid frequent reauthentication.

oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  use_expiry_from_token: true\n

Expire a node and force re-authentication

A node can be expired immediately via:

headscale node expire -i <NODE_ID>\n

"},{"location":"ref/oidc/#reference-a-user-in-the-policy","title":"Reference a user in the policy","text":"

You may refer to users in the Headscale policy via:

  • Email address
  • Username
  • Provider identifier (only available in the database or from your identity provider)

A user identifier in the policy must contain a single @

The Headscale policy requires a single @ to reference a user. If the username or provider identifier doesn't already contain a single @, it needs to be appended at the end. For example: the username ssmith has to be written as ssmith@ to be correctly identified as user within the policy.

Email address or username might be updated by users

Many identity providers allow users to update their own profile. Depending on the identity provider and its configuration, the values for username or email address might change over time. This might have unexpected consequences for Headscale where a policy might no longer work or a user might obtain more access by hijacking an existing username or email address.

"},{"location":"ref/oidc/#supported-oidc-claims","title":"Supported OIDC claims","text":"

Headscale uses the standard OIDC claims to populate and update its local user profile on each login. OIDC claims are read from the ID Token or from the UserInfo endpoint.

Headscale profile OIDC claim Notes / examples email address email Only used when email_verified: true display name name eg: Sam Smith username preferred_username Depends on identity provider, eg: ssmith, ssmith@idp.example.com, \\\\example.com\\ssmith profile picture picture URL to a profile picture or avatar provider identifier iss, sub A stable and unique identifier for a user, typically a combination of iss and sub OIDC claims groups Only used to filter for allowed groups"},{"location":"ref/oidc/#limitations","title":"Limitations","text":"
  • Support for OpenID Connect aims to be generic and vendor independent. It offers only limited support for quirks of specific identity providers.
  • OIDC groups cannot be used in ACLs.
  • The username provided by the identity provider needs to adhere to this pattern:
    • The username must be at least two characters long.
    • It must only contain letters, digits, hyphens, dots, underscores, and up to a single @.
    • The username must start with a letter.
  • A user's email address is only synchronized to the local user profile when the identity provider marks the email address as verified (email_verified: true).

Please see the GitHub label \"OIDC\" for OIDC related issues.

"},{"location":"ref/oidc/#identity-provider-specific-configuration","title":"Identity provider specific configuration","text":"

Third-party software and services

This section of the documentation is specific for third-party software and services. We recommend users read the third-party documentation on how to configure and integrate an OIDC client. Please see the Configuration section for a description of Headscale's OIDC related configuration settings.

Any identity provider with OpenID Connect support should \"just work\" with Headscale. The following identity providers are known to work:

  • Authelia
  • Authentik
  • Kanidm
  • Keycloak
"},{"location":"ref/oidc/#authelia","title":"Authelia","text":"

Authelia is fully supported by Headscale.

"},{"location":"ref/oidc/#additional-configuration-to-authorize-users-based-on-filters","title":"Additional configuration to authorize users based on filters","text":"

Authelia (4.39.0 or newer) no longer provides standard OIDC claims such as email or groups via the ID Token. The OIDC email and groups claims are used to authorize users with filters. This extra configuration step is only needed if you need to authorize access based on one of the following user properties:

  • domain
  • email address
  • group membership

Please follow the instructions from Authelia's documentation on how to Restore Functionality Prior to Claims Parameter.

"},{"location":"ref/oidc/#authentik","title":"Authentik","text":"
  • Authentik is fully supported by Headscale.
  • Headscale does not JSON Web Encryption. Leave the field Encryption Key in the providers section unset.
"},{"location":"ref/oidc/#google-oauth","title":"Google OAuth","text":"

No username due to missing preferred_username

Google OAuth does not send the preferred_username claim when the scope profile is requested. The username in Headscale will be blank/not set.

In order to integrate Headscale with Google, you'll need to have a Google Cloud Console account.

Google OAuth has a verification process if you need to have users authenticate who are outside of your domain. If you only need to authenticate users from your domain name (ie @example.com), you don't need to go through the verification process.

However if you don't have a domain, or need to add users outside of your domain, you can manually add emails via Google Console.

"},{"location":"ref/oidc/#steps","title":"Steps","text":"
  1. Go to Google Console and login or create an account if you don't have one.
  2. Create a project (if you don't already have one).
  3. On the left hand menu, go to APIs and services -> Credentials
  4. Click Create Credentials -> OAuth client ID
  5. Under Application Type, choose Web Application
  6. For Name, enter whatever you like
  7. Under Authorised redirect URIs, add Headscale's OIDC callback URL: https://headscale.example.com/oidc/callback
  8. Click Save at the bottom of the form
  9. Take note of the Client ID and Client secret, you can also download it for reference if you need it.
  10. Configure Headscale following the \"Basic configuration\" steps. The issuer URL for Google OAuth is: https://accounts.google.com.
"},{"location":"ref/oidc/#kanidm","title":"Kanidm","text":"
  • Kanidm is fully supported by Headscale.
  • Groups for the allowed groups filter need to be specified with their full SPN, for example: headscale_users@sso.example.com.
"},{"location":"ref/oidc/#keycloak","title":"Keycloak","text":"

Keycloak is fully supported by Headscale.

"},{"location":"ref/oidc/#additional-configuration-to-use-the-allowed-groups-filter","title":"Additional configuration to use the allowed groups filter","text":"

Keycloak has no built-in client scope for the OIDC groups claim. This extra configuration step is only needed if you need to authorize access based on group membership.

  • Create a new client scope groups for OpenID Connect:
    • Configure a Group Membership mapper with name groups and the token claim name groups.
    • Enable the mapper for the ID Token, Access Token and UserInfo endpoint.
  • Configure the new client scope for your Headscale client:
    • Edit the Headscale client.
    • Search for the client scope group.
    • Add it with assigned type Default.
  • Configure the allowed groups in Headscale. Keep in mind that groups in Keycloak start with a leading /.
"},{"location":"ref/oidc/#microsoft-entra-id","title":"Microsoft Entra ID","text":"

In order to integrate Headscale with Microsoft Entra ID, you'll need to provision an App Registration with the correct scopes and redirect URI.

Configure Headscale following the \"Basic configuration\" steps. The issuer URL for Microsoft Entra ID is: https://login.microsoftonline.com/<tenant-UUID>/v2.0. The following extra_params might be useful:

  • domain_hint: example.com to use your own domain
  • prompt: select_account to force an account picker during login
"},{"location":"ref/remote-cli/","title":"Controlling headscale with remote CLI","text":"

This documentation has the goal of showing a user how-to control a headscale instance from a remote machine with the headscale command line binary.

"},{"location":"ref/remote-cli/#prerequisite","title":"Prerequisite","text":"
  • A workstation to run headscale (any supported platform, e.g. Linux).
  • A headscale server with gRPC enabled.
  • Connections to the gRPC port (default: 50443) are allowed.
  • Remote access requires an encrypted connection via TLS.
  • An API key to authenticate with the headscale server.
"},{"location":"ref/remote-cli/#create-an-api-key","title":"Create an API key","text":"

We need to create an API key to authenticate with the remote headscale server when using it from our workstation.

To create an API key, log into your headscale server and generate a key:

headscale apikeys create --expiration 90d\n

Copy the output of the command and save it for later. Please note that you can not retrieve a key again, if the key is lost, expire the old one, and create a new key.

To list the keys currently associated with the server:

headscale apikeys list\n

and to expire a key:

headscale apikeys expire --prefix \"<PREFIX>\"\n
"},{"location":"ref/remote-cli/#download-and-configure-headscale","title":"Download and configure headscale","text":"
  1. Download the headscale binary from GitHub's release page. Make sure to use the same version as on the server.

  2. Put the binary somewhere in your PATH, e.g. /usr/local/bin/headscale

  3. Make headscale executable:

    chmod +x /usr/local/bin/headscale\n
  4. Provide the connection parameters for the remote headscale server either via a minimal YAML configuration file or via environment variables:

    Minimal YAML configuration fileEnvironment variables config.yaml
    cli:\n    address: <HEADSCALE_ADDRESS>:<PORT>\n    api_key: <API_KEY_FROM_PREVIOUS_STEP>\n
    export HEADSCALE_CLI_ADDRESS=\"<HEADSCALE_ADDRESS>:<PORT>\"\nexport HEADSCALE_CLI_API_KEY=\"<API_KEY_FROM_PREVIOUS_STEP>\"\n

    Bug

    Headscale currently requires at least an empty configuration file when environment variables are used to specify connection details. See issue 2193 for more information.

    This instructs the headscale binary to connect to a remote instance at <HEADSCALE_ADDRESS>:<PORT>, instead of connecting to the local instance.

  5. Test the connection

    Let us run the headscale command to verify that we can connect by listing our nodes:

    headscale nodes list\n

    You should now be able to see a list of your nodes from your workstation, and you can now control the headscale server from your workstation.

"},{"location":"ref/remote-cli/#behind-a-proxy","title":"Behind a proxy","text":"

It is possible to run the gRPC remote endpoint behind a reverse proxy, like Nginx, and have it run on the same port as headscale.

While this is not a supported feature, an example on how this can be set up on NixOS is shown here.

"},{"location":"ref/remote-cli/#troubleshooting","title":"Troubleshooting","text":"
  • Make sure you have the same headscale version on your server and workstation.
  • Ensure that connections to the gRPC port are allowed.
  • Verify that your TLS certificate is valid and trusted.
  • If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
    • Add your self-signed certificate to the trust store of your OS or
    • Disable certificate verification by either setting cli.insecure: true in the configuration file or by setting HEADSCALE_CLI_INSECURE=1 via an environment variable. We do not recommend to disable certificate validation.
"},{"location":"ref/routes/","title":"Routes","text":"

Headscale supports route advertising and can be used to manage subnet routers and exit nodes for a tailnet.

  • Subnet routers may be used to connect an existing network such as a virtual private cloud or an on-premise network with your tailnet. Use a subnet router to access devices where Tailscale can't be installed or to gradually rollout Tailscale.
  • Exit nodes can be used to route all Internet traffic for another Tailscale node. Use it to securely access the Internet on an untrusted Wi-Fi or to access online services that expect traffic from a specific IP address.
"},{"location":"ref/routes/#subnet-router","title":"Subnet router","text":"

The setup of a subnet router requires double opt-in, once from a subnet router and once on the control server to allow its use within the tailnet. Optionally, use autoApprovers to automatically approve routes from a subnet router.

"},{"location":"ref/routes/#setup-a-subnet-router","title":"Setup a subnet router","text":""},{"location":"ref/routes/#configure-a-node-as-subnet-router","title":"Configure a node as subnet router","text":"

Register a node and advertise the routes it should handle as comma separated list:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-routes=10.0.0.0/8,192.168.0.0/24\n

If the node is already registered, it can advertise new routes or update previously announced routes with:

$ sudo tailscale set --advertise-routes=10.0.0.0/8,192.168.0.0/24\n

Finally, enable IP forwarding to route traffic.

"},{"location":"ref/routes/#enable-the-subnet-router-on-the-control-server","title":"Enable the subnet router on the control server","text":"

The routes of a tailnet can be displayed with the headscale nodes list-routes command. A subnet router with the hostname myrouter announced the IPv4 networks 10.0.0.0/8 and 192.168.0.0/24. Those need to be approved before they can be used.

$ headscale nodes list-routes\nID | Hostname | Approved | Available                  | Serving (Primary)\n1  | myrouter |          | 10.0.0.0/8, 192.168.0.0/24 |\n

Approve all desired routes of a subnet router by specifying them as comma separated list:

$ headscale nodes approve-routes --identifier 1 --routes 10.0.0.0/8,192.168.0.0/24\nNode updated\n

The node myrouter can now route the IPv4 networks 10.0.0.0/8 and 192.168.0.0/24 for the tailnet.

$ headscale nodes list-routes\nID | Hostname | Approved                   | Available                  | Serving (Primary)\n1  | myrouter | 10.0.0.0/8, 192.168.0.0/24 | 10.0.0.0/8, 192.168.0.0/24 | 10.0.0.0/8, 192.168.0.0/24\n
"},{"location":"ref/routes/#use-the-subnet-router","title":"Use the subnet router","text":"

To accept routes advertised by a subnet router on a node:

$ sudo tailscale set --accept-routes\n

Please refer to the official Tailscale documentation for how to use a subnet router on different operating systems.

"},{"location":"ref/routes/#restrict-the-use-of-a-subnet-router-with-acl","title":"Restrict the use of a subnet router with ACL","text":"

The routes announced by subnet routers are available to the nodes in a tailnet. By default, without an ACL enabled, all nodes can accept and use such routes. Configure an ACL to explicitly manage who can use routes.

The ACL snippet below defines three hosts, a subnet router router, a regular node node and service.example.net as internal service that can be reached via a route on the subnet router router. It allows the node node to access service.example.net on port 80 and 443 which is reachable via the subnet router. Access to the subnet router itself is denied.

Access the routes of a subnet router without the subnet router itself
{\n  \"hosts\": {\n    // the router is not referenced but announces 192.168.0.0/24\"\n    \"router\": \"100.64.0.1/32\",\n    \"node\": \"100.64.0.2/32\",\n    \"service.example.net\": \"192.168.0.1/32\"\n  },\n  \"acls\": [\n    {\n      \"action\": \"accept\",\n      \"src\": [\"node\"],\n      \"dst\": [\"service.example.net:80,443\"]\n    }\n  ]\n}\n
"},{"location":"ref/routes/#automatically-approve-routes-of-a-subnet-router","title":"Automatically approve routes of a subnet router","text":"

The initial setup of a subnet router usually requires manual approval of their announced routes on the control server before they can be used by a node in a tailnet. Headscale supports the autoApprovers section of an ACL to automate the approval of routes served with a subnet router.

The ACL snippet below defines the tag tag:router owned by the user alice. This tag is used for routes in the autoApprovers section. The IPv4 route 192.168.0.0/24 is automatically approved once announced by a subnet router owned by the user alice and that also advertises the tag tag:router.

Subnet routers owned by alice and tagged with tag:router are automatically approved
{\n  \"tagOwners\": {\n    \"tag:router\": [\"alice@\"]\n  },\n  \"autoApprovers\": {\n    \"routes\": {\n      \"192.168.0.0/24\": [\"tag:router\"]\n    }\n  },\n  \"acls\": [\n    // more rules\n  ]\n}\n

Advertise the route 192.168.0.0/24 from a subnet router that also advertises the tag tag:router when joining the tailnet:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-tags tag:router --advertise-routes 192.168.0.0/24\n

Please see the official Tailscale documentation for more information on auto approvers.

"},{"location":"ref/routes/#exit-node","title":"Exit node","text":"

The setup of an exit node requires double opt-in, once from an exit node and once on the control server to allow its use within the tailnet. Optionally, use autoApprovers to automatically approve an exit node.

"},{"location":"ref/routes/#setup-an-exit-node","title":"Setup an exit node","text":""},{"location":"ref/routes/#configure-a-node-as-exit-node","title":"Configure a node as exit node","text":"

Register a node and make it advertise itself as an exit node:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-exit-node\n

If the node is already registered, it can advertise exit capabilities like this:

$ sudo tailscale set --advertise-exit-node\n

Finally, enable IP forwarding to route traffic.

"},{"location":"ref/routes/#enable-the-exit-node-on-the-control-server","title":"Enable the exit node on the control server","text":"

The routes of a tailnet can be displayed with the headscale nodes list-routes command. An exit node can be recognized by its announced routes: 0.0.0.0/0 for IPv4 and ::/0 for IPv6. The exit node with the hostname myexit is already available, but needs to be approved:

$ headscale nodes list-routes\nID | Hostname | Approved | Available       | Serving (Primary)\n1  | myexit   |          | 0.0.0.0/0, ::/0 |\n

For exit nodes, it is sufficient to approve either the IPv4 or IPv6 route. The other will be approved automatically.

$ headscale nodes approve-routes --identifier 1 --routes 0.0.0.0/0\nNode updated\n

The node myexit is now approved as exit node for the tailnet:

$ headscale nodes list-routes\nID | Hostname | Approved        | Available       | Serving (Primary)\n1  | myexit   | 0.0.0.0/0, ::/0 | 0.0.0.0/0, ::/0 | 0.0.0.0/0, ::/0\n
"},{"location":"ref/routes/#use-the-exit-node","title":"Use the exit node","text":"

The exit node can now be used on a node with:

$ sudo tailscale set --exit-node myexit\n

Please refer to the official Tailscale documentation for how to use an exit node on different operating systems.

"},{"location":"ref/routes/#restrict-the-use-of-an-exit-node-with-acl","title":"Restrict the use of an exit node with ACL","text":"

An exit node is offered to all nodes in a tailnet. By default, without an ACL enabled, all nodes in a tailnet can select and use an exit node. Configure autogroup:internet in an ACL rule to restrict who can use any of the available exit nodes.

Example use of autogroup:internet
{\n  \"acls\": [\n    {\n      \"action\": \"accept\",\n      \"src\": [\"...\"],\n      \"dst\": [\"autogroup:internet:*\"]\n    }\n  ]\n}\n
"},{"location":"ref/routes/#automatically-approve-an-exit-node-with-auto-approvers","title":"Automatically approve an exit node with auto approvers","text":"

The initial setup of an exit node usually requires manual approval on the control server before it can be used by a node in a tailnet. Headscale supports the autoApprovers section of an ACL to automate the approval of a new exit node as soon as it joins the tailnet.

The ACL snippet below defines the tag tag:exit owned by the user alice. This tag is used for exitNode in the autoApprovers section. A new exit node which is owned by the user alice and that also advertises the tag tag:exit is automatically approved:

Exit nodes owned by alice and tagged with tag:exit are automatically approved
{\n  \"tagOwners\": {\n    \"tag:exit\": [\"alice@\"]\n  },\n  \"autoApprovers\": {\n    \"exitNode\": [\"tag:exit\"]\n  },\n  \"acls\": [\n    // more rules\n  ]\n}\n

Advertise a node as exit node and also advertise the tag tag:exit when joining the tailnet:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-tags tag:exit --advertise-exit-node\n

Please see the official Tailscale documentation for more information on auto approvers.

"},{"location":"ref/routes/#high-availability","title":"High availability","text":"

Headscale has limited support for high availability routing. Multiple subnet routers with overlapping routes or multiple exit nodes can be used to provide high availability for users. If one router node goes offline, another one can serve the same routes to clients. Please see the official Tailscale documentation on high availability for details.

Bug

In certain situations it might take up to 16 minutes for Headscale to detect a node as offline. A failover node might not be selected fast enough, if such a node is used as subnet router or exit node causing service interruptions for clients. See issue 2129 for more information.

"},{"location":"ref/routes/#troubleshooting","title":"Troubleshooting","text":""},{"location":"ref/routes/#enable-ip-forwarding","title":"Enable IP forwarding","text":"

A subnet router or exit node is routing traffic on behalf of other nodes and thus requires IP forwarding. Check the official Tailscale documentation for how to enable IP forwarding.

"},{"location":"ref/tls/","title":"Running the service via TLS (optional)","text":""},{"location":"ref/tls/#bring-your-own-certificate","title":"Bring your own certificate","text":"

Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the tls_cert_path and tls_cert_path configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_cert_path: \"\"\ntls_key_path: \"\"\n

The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.

"},{"location":"ref/tls/#lets-encrypt-acme","title":"Let's Encrypt / ACME","text":"

To get a certificate automatically via Let's Encrypt, set tls_letsencrypt_hostname to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the server_url configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in tls_letsencrypt_cache_dir. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_letsencrypt_hostname: \"\"\ntls_letsencrypt_listen: \":http\"\ntls_letsencrypt_cache_dir: \".cache\"\ntls_letsencrypt_challenge_type: HTTP-01\n
"},{"location":"ref/tls/#challenge-types","title":"Challenge types","text":"

Headscale only supports two values for tls_letsencrypt_challenge_type: HTTP-01 (default) and TLS-ALPN-01.

"},{"location":"ref/tls/#http-01","title":"HTTP-01","text":"

For HTTP-01, headscale must be reachable on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in listen_addr. By default, headscale listens on port 80 on all local IPs for Let's Encrypt automated validation.

If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set tls_letsencrypt_listen to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run setcap). Keep in mind, however, that Let's Encrypt will only connect to port 80 for the validation callback, so if you change tls_letsencrypt_listen you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in tls_letsencrypt_listen.

"},{"location":"ref/tls/#tls-alpn-01","title":"TLS-ALPN-01","text":"

For TLS-ALPN-01, headscale listens on the ip:port combination defined in listen_addr. Let's Encrypt will only connect to port 443 for the validation callback, so if listen_addr is not set to port 443, something else (e.g. a firewall rule) will be required to forward the traffic from port 443 to the ip:port combination specified in listen_addr.

"},{"location":"ref/tls/#technical-description","title":"Technical description","text":"

Headscale uses autocert, a Golang library providing ACME protocol verification, to facilitate certificate renewals via Let's Encrypt. Certificates will be renewed automatically, and the following can be expected:

  • Certificates provided from Let's Encrypt have a validity of 3 months from date issued.
  • Renewals are only attempted by headscale when 30 days or less remains until certificate expiry.
  • Renewal attempts by autocert are triggered at a random interval of 30-60 minutes.
  • No log output is generated when renewals are skipped, or successful.
"},{"location":"ref/tls/#checking-certificate-expiry","title":"Checking certificate expiry","text":"

If you want to validate that certificate renewal completed successfully, this can be done either manually, or through external monitoring software. Two examples of doing this manually:

  1. Open the URL for your headscale server in your browser of choice, and manually inspecting the expiry date of the certificate you receive.
  2. Or, check remotely from CLI using openssl:
$ openssl s_client -servername [hostname] -connect [hostname]:443 | openssl x509 -noout -dates\n(...)\nnotBefore=Feb  8 09:48:26 2024 GMT\nnotAfter=May  8 09:48:25 2024 GMT\n
"},{"location":"ref/tls/#log-output-from-the-autocert-library","title":"Log output from the autocert library","text":"

As these log lines are from the autocert library, they are not strictly generated by headscale itself.

acme/autocert: missing server name\n

Likely caused by an incoming connection that does not specify a hostname, for example a curl request directly against the IP of the server, or an unexpected hostname.

acme/autocert: host \"[foo]\" not configured in HostWhitelist\n

Similarly to the above, this likely indicates an invalid incoming request for an incorrect hostname, commonly just the IP itself.

The source code for autocert can be found here

"},{"location":"ref/integration/reverse-proxy/","title":"Running headscale behind a reverse proxy","text":"

Community documentation

This page is not actively maintained by the headscale authors and is written by community members. It is not verified by headscale developers.

It might be outdated and it might miss necessary steps.

Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.

"},{"location":"ref/integration/reverse-proxy/#websockets","title":"WebSockets","text":"

The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.

WebSockets support is also required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our config-example.yaml.

"},{"location":"ref/integration/reverse-proxy/#cloudflare","title":"Cloudflare","text":"

Running headscale behind a cloudflare proxy or cloudflare tunnel is not supported and will not work as Cloudflare does not support WebSocket POSTs as required by the Tailscale protocol. See this issue

"},{"location":"ref/integration/reverse-proxy/#tls","title":"TLS","text":"

Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.

config.yaml
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served\nlisten_addr: 0.0.0.0:8080\nmetrics_listen_addr: 0.0.0.0:9090\ntls_cert_path: \"\"\ntls_key_path: \"\"\n
"},{"location":"ref/integration/reverse-proxy/#nginx","title":"nginx","text":"

The following example configuration can be used in your nginx setup, substituting values as necessary. <IP:PORT> should be the IP address and port where headscale is running. In most cases, this will be http://localhost:8080.

nginx.conf
map $http_upgrade $connection_upgrade {\n    default      upgrade;\n    ''           close;\n}\n\nserver {\n    listen 80;\n    listen [::]:80;\n\n    listen 443      ssl http2;\n    listen [::]:443 ssl http2;\n\n    server_name <YOUR_SERVER_NAME>;\n\n    ssl_certificate <PATH_TO_CERT>;\n    ssl_certificate_key <PATH_CERT_KEY>;\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    location / {\n        proxy_pass http://<IP:PORT>;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_set_header Host $server_name;\n        proxy_redirect http:// https://;\n        proxy_buffering off;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        add_header Strict-Transport-Security \"max-age=15552000; includeSubDomains\" always;\n    }\n}\n
"},{"location":"ref/integration/reverse-proxy/#istioenvoy","title":"istio/envoy","text":"

If you using Istio ingressgateway or Envoy as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:

Sending local reply with details upgrade_failed\n
"},{"location":"ref/integration/reverse-proxy/#envoy","title":"Envoy","text":"

You need to add a new upgrade_type named tailscale-control-protocol. see details

"},{"location":"ref/integration/reverse-proxy/#istio","title":"Istio","text":"

Same as envoy, we can use EnvoyFilter to add upgrade_type.

apiVersion: networking.istio.io/v1alpha3\nkind: EnvoyFilter\nmetadata:\n  name: headscale-behind-istio-ingress\n  namespace: istio-system\nspec:\n  configPatches:\n    - applyTo: NETWORK_FILTER\n      match:\n        listener:\n          filterChain:\n            filter:\n              name: envoy.filters.network.http_connection_manager\n      patch:\n        operation: MERGE\n        value:\n          typed_config:\n            \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n            upgrade_configs:\n              - upgrade_type: tailscale-control-protocol\n
"},{"location":"ref/integration/reverse-proxy/#caddy","title":"Caddy","text":"

The following Caddyfile is all that is necessary to use Caddy as a reverse proxy for headscale, in combination with the config.yaml specifications above to disable headscale's built in TLS. Replace values as necessary - <YOUR_SERVER_NAME> should be the FQDN at which headscale will be served, and <IP:PORT> should be the IP address and port where headscale is running. In most cases, this will be localhost:8080.

Caddyfile
<YOUR_SERVER_NAME> {\n    reverse_proxy <IP:PORT>\n}\n

Caddy v2 will automatically provision a certificate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary.

For a slightly more complex configuration which utilizes Docker containers to manage Caddy, headscale, and Headscale-UI, Guru Computing's guide is an excellent reference.

"},{"location":"ref/integration/reverse-proxy/#apache","title":"Apache","text":"

The following minimal Apache config will proxy traffic to the headscale instance on <IP:PORT>. Note that upgrade=any is required as a parameter for ProxyPass so that WebSockets traffic whose Upgrade header value is not equal to WebSocket (i. e. Tailscale Control Protocol) is forwarded correctly. See the Apache docs for more information on this.

apache.conf
<VirtualHost *:443>\n    ServerName <YOUR_SERVER_NAME>\n\n    ProxyPreserveHost On\n    ProxyPass / http://<IP:PORT>/ upgrade=any\n\n    SSLEngine On\n    SSLCertificateFile <PATH_TO_CERT>\n    SSLCertificateKeyFile <PATH_CERT_KEY>\n</VirtualHost>\n
"},{"location":"ref/integration/tools/","title":"Tools related to headscale","text":"

Community contributions

This page contains community contributions. The projects listed here are not maintained by the headscale authors and are written by community members.

This page collects third-party tools, client libraries, and scripts related to headscale.

Name Repository Link Description tailscale-manager Github Dynamically manage Tailscale route advertisements headscalebacktosqlite Github Migrate headscale from PostgreSQL back to SQLite headscale-pf Github Populates user groups based on user groups in Jumpcloud or Authentik headscale-client-go Github A Go client implementation for the Headscale HTTP API."},{"location":"ref/integration/web-ui/","title":"Web interfaces for headscale","text":"

Community contributions

This page contains community contributions. The projects listed here are not maintained by the headscale authors and are written by community members.

Headscale doesn't provide a built-in web interface but users may pick one from the available options.

Name Repository Link Description headscale-ui Github A web frontend for the headscale Tailscale-compatible coordination server HeadscaleUi GitHub A static headscale admin ui, no backend environment required Headplane GitHub An advanced Tailscale inspired frontend for headscale headscale-admin Github Headscale-Admin is meant to be a simple, modern web interface for headscale ouroboros Github Ouroboros is designed for users to manage their own devices, rather than for admins unraid-headscale-admin Github A simple headscale admin UI for Unraid, it offers Local (docker exec) and API Mode headscale-console Github WebAssembly-based client supporting SSH, VNC and RDP with optional self-service capabilities

You can ask for support on our Discord server in the \"web-interfaces\" channel.

"},{"location":"setup/requirements/","title":"Requirements","text":"

Headscale should just work as long as the following requirements are met:

  • A server with a public IP address for headscale. A dual-stack setup with a public IPv4 and a public IPv6 address is recommended.
  • Headscale is served via HTTPS on port 4431.
  • A reasonably modern Linux or BSD based operating system.
  • A dedicated local user account to run headscale.
  • A little bit of command line knowledge to configure and operate headscale.
"},{"location":"setup/requirements/#assumptions","title":"Assumptions","text":"

The headscale documentation and the provided examples are written with a few assumptions in mind:

  • Headscale is running as system service via a dedicated local user headscale.
  • The configuration is loaded from /etc/headscale/config.yaml.
  • SQLite is used as database.
  • The data directory for headscale (used for private keys, ACLs, SQLite database, \u2026) is located in /var/lib/headscale.
  • URLs and values that need to be replaced by the user are either denoted as <VALUE_TO_CHANGE> or use placeholder values such as headscale.example.com.

Please adjust to your local environment accordingly.

  1. The Tailscale client assumes HTTPS on port 443 in certain situations. Serving headscale either via HTTP or via HTTPS on a port other than 443 is possible but sticking with HTTPS on port 443 is strongly recommended for production setups. See issue 2164 for more information.\u00a0\u21a9

"},{"location":"setup/upgrade/","title":"Upgrade an existing installation","text":"

Update an existing headscale installation to a new version:

  • Read the announcement on the GitHub releases page for the new version. It lists the changes of the release along with possible breaking changes.
  • Create a backup of your database.
  • Update headscale to the new version, preferably by following the same installation method.
  • Compare and update the configuration file.
  • Restart headscale.
"},{"location":"setup/install/community/","title":"Community packages","text":"

Several Linux distributions and community members provide packages for headscale. Those packages may be used instead of the official releases provided by the headscale maintainers. Such packages offer improved integration for their targeted operating system and usually:

  • setup a dedicated local user account to run headscale
  • provide a default configuration
  • install headscale as system service

Community packages might be outdated

The packages mentioned on this page might be outdated or unmaintained. Use the official releases to get the current stable version or to test pre-releases.

"},{"location":"setup/install/community/#arch-linux","title":"Arch Linux","text":"

Arch Linux offers a package for headscale, install via:

pacman -S headscale\n

The AUR package headscale-git can be used to build the current development version.

"},{"location":"setup/install/community/#fedora-rhel-centos","title":"Fedora, RHEL, CentOS","text":"

A third-party repository for various RPM based distributions is available at: https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/. The site provides detailed setup and installation instructions.

"},{"location":"setup/install/community/#nix-nixos","title":"Nix, NixOS","text":"

A Nix package is available as: headscale. See the NixOS package site for installation details.

"},{"location":"setup/install/community/#gentoo","title":"Gentoo","text":"
emerge --ask net-vpn/headscale\n

Gentoo specific documentation is available here.

"},{"location":"setup/install/community/#openbsd","title":"OpenBSD","text":"

Headscale is available in ports. The port installs headscale as system service with rc.d and provides usage instructions upon installation.

pkg_add headscale\n
"},{"location":"setup/install/container/","title":"Running headscale in a container","text":"

Community documentation

This page is not actively maintained by the headscale authors and is written by community members. It is not verified by headscale developers.

It might be outdated and it might miss necessary steps.

This documentation has the goal of showing a user how-to set up and run headscale in a container. A container runtime such as Docker or Podman is required. The container image can be found on Docker Hub and GitHub Container Registry. The container image URLs are:

  • Docker Hub: docker.io/headscale/headscale:<VERSION>
  • GitHub Container Registry: ghcr.io/juanfont/headscale:<VERSION>
"},{"location":"setup/install/container/#configure-and-run-headscale","title":"Configure and run headscale","text":"
  1. Create a directory on the Docker host to store headscale's configuration and the SQLite database:

    mkdir -p ./headscale/{config,lib,run}\ncd ./headscale\n
  2. Download the example configuration for your chosen version and save it as: $(pwd)/config/config.yaml. Adjust the configuration to suit your local environment. See Configuration for details.

  3. Start headscale from within the previously created ./headscale directory:

    docker run \\\n  --name headscale \\\n  --detach \\\n  --volume \"$(pwd)/config:/etc/headscale\" \\\n  --volume \"$(pwd)/lib:/var/lib/headscale\" \\\n  --volume \"$(pwd)/run:/var/run/headscale\" \\\n  --publish 127.0.0.1:8080:8080 \\\n  --publish 127.0.0.1:9090:9090 \\\n  docker.io/headscale/headscale:<VERSION> \\\n  serve\n

    Note: use 0.0.0.0:8080:8080 instead of 127.0.0.1:8080:8080 if you want to expose the container externally.

    This command mounts the local directories inside the container, forwards port 8080 and 9090 out of the container so the headscale instance becomes available and then detaches so headscale runs in the background.

    A similar configuration for docker-compose:

    docker-compose.yaml
    services:\n  headscale:\n    image: docker.io/headscale/headscale:<VERSION>\n    restart: unless-stopped\n    container_name: headscale\n    ports:\n      - \"127.0.0.1:8080:8080\"\n      - \"127.0.0.1:9090:9090\"\n    volumes:\n      # Please set <HEADSCALE_PATH> to the absolute path\n      # of the previously created headscale directory.\n      - <HEADSCALE_PATH>/config:/etc/headscale\n      - <HEADSCALE_PATH>/lib:/var/lib/headscale\n      - <HEADSCALE_PATH>/run:/var/run/headscale\n    command: serve\n
  4. Verify headscale is running:

    Follow the container logs:

    docker logs --follow headscale\n

    Verify running containers:

    docker ps\n

    Verify headscale is available:

    curl http://127.0.0.1:9090/metrics\n
  5. Create a headscale user:

    docker exec -it headscale \\\n  headscale users create myfirstuser\n
"},{"location":"setup/install/container/#register-a-machine-normal-login","title":"Register a machine (normal login)","text":"

On a client machine, execute the tailscale up command to login:

tailscale up --login-server YOUR_HEADSCALE_URL\n

To register a machine when running headscale in a container, take the headscale command and pass it to the container:

docker exec -it headscale \\\n  headscale nodes register --user myfirstuser --key <YOUR_MACHINE_KEY>\n
"},{"location":"setup/install/container/#register-a-machine-using-a-pre-authenticated-key","title":"Register a machine using a pre authenticated key","text":"

Generate a key using the command line:

docker exec -it headscale \\\n  headscale preauthkeys create --user myfirstuser --reusable --expiration 24h\n

This will return a pre-authenticated key that can be used to connect a node to headscale with the tailscale up command:

tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>\n
"},{"location":"setup/install/container/#debugging-headscale-running-in-docker","title":"Debugging headscale running in Docker","text":"

The Headscale container image is based on a \"distroless\" image that does not contain a shell or any other debug tools. If you need to debug headscale running in the Docker container, you can use the -debug variant, for example docker.io/headscale/headscale:x.x.x-debug.

"},{"location":"setup/install/container/#running-the-debug-docker-container","title":"Running the debug Docker container","text":"

To run the debug Docker container, use the exact same commands as above, but replace docker.io/headscale/headscale:x.x.x with docker.io/headscale/headscale:x.x.x-debug (x.x.x is the version of headscale). The two containers are compatible with each other, so you can alternate between them.

"},{"location":"setup/install/container/#executing-commands-in-the-debug-container","title":"Executing commands in the debug container","text":"

The default command in the debug container is to run headscale, which is located at /ko-app/headscale inside the container.

Additionally, the debug container includes a minimalist Busybox shell.

To launch a shell in the container, use:

docker run -it docker.io/headscale/headscale:x.x.x-debug sh\n

You can also execute commands directly, such as ls /ko-app in this example:

docker run docker.io/headscale/headscale:x.x.x-debug ls /ko-app\n

Using docker exec -it allows you to run commands in an existing container.

"},{"location":"setup/install/official/","title":"Official releases","text":"

Official releases for headscale are available as binaries for various platforms and DEB packages for Debian and Ubuntu. Both are available on the GitHub releases page.

"},{"location":"setup/install/official/#using-packages-for-debianubuntu-recommended","title":"Using packages for Debian/Ubuntu (recommended)","text":"

It is recommended to use our DEB packages to install headscale on a Debian based system as those packages configure a local user to run headscale, provide a default configuration and ship with a systemd service file. Supported distributions are Ubuntu 22.04 or newer, Debian 11 or newer.

  1. Download the latest headscale package for your platform (.deb for Ubuntu and Debian).

    HEADSCALE_VERSION=\"\" # See above URL for latest version, e.g. \"X.Y.Z\" (NOTE: do not add the \"v\" prefix!)\nHEADSCALE_ARCH=\"\" # Your system architecture, e.g. \"amd64\"\nwget --output-document=headscale.deb \\\n \"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb\"\n
  2. Install headscale:

    sudo apt install ./headscale.deb\n
  3. Configure headscale by editing the configuration file:

    sudo nano /etc/headscale/config.yaml\n
  4. Enable and start the headscale service:

    sudo systemctl enable --now headscale\n
  5. Verify that headscale is running as intended:

    sudo systemctl status headscale\n
"},{"location":"setup/install/official/#using-standalone-binaries-advanced","title":"Using standalone binaries (advanced)","text":"

Advanced

This installation method is considered advanced as one needs to take care of the local user and the systemd service themselves. If possible, use the DEB packages or a community package instead.

This section describes the installation of headscale according to the Requirements and assumptions. Headscale is run by a dedicated local user and the service itself is managed by systemd.

  1. Download the latest headscale binary from GitHub's release page:

    sudo wget --output-document=/usr/local/bin/headscale \\\nhttps://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>\n
  2. Make headscale executable:

    sudo chmod +x /usr/local/bin/headscale\n
  3. Add a dedicated local user to run headscale:

    sudo useradd \\\n --create-home \\\n --home-dir /var/lib/headscale/ \\\n --system \\\n --user-group \\\n --shell /usr/sbin/nologin \\\n headscale\n
  4. Download the example configuration for your chosen version and save it as: /etc/headscale/config.yaml. Adjust the configuration to suit your local environment. See Configuration for details.

    sudo mkdir -p /etc/headscale\nsudo nano /etc/headscale/config.yaml\n
  5. Copy headscale's systemd service file to /etc/systemd/system/headscale.service and adjust it to suit your local setup. The following parameters likely need to be modified: ExecStart, WorkingDirectory, ReadWritePaths.

  6. In /etc/headscale/config.yaml, override the default headscale unix socket with a path that is writable by the headscale user or group:

    config.yaml
    unix_socket: /var/run/headscale/headscale.sock\n
  7. Reload systemd to load the new configuration file:

    systemctl daemon-reload\n
  8. Enable and start the new headscale service:

    systemctl enable --now headscale\n
  9. Verify that headscale is running as intended:

    systemctl status headscale\n
"},{"location":"setup/install/source/","title":"Build from source","text":"

Community documentation

This page is not actively maintained by the headscale authors and is written by community members. It is not verified by headscale developers.

It might be outdated and it might miss necessary steps.

Headscale can be built from source using the latest version of Go and Buf (Protobuf generator). See the Contributing section in the GitHub README for more information.

"},{"location":"setup/install/source/#openbsd","title":"OpenBSD","text":""},{"location":"setup/install/source/#install-from-source","title":"Install from source","text":"
# Install prerequisites\npkg_add go git\n\ngit clone https://github.com/juanfont/headscale.git\n\ncd headscale\n\n# optionally checkout a release\n# option a. you can find official release at https://github.com/juanfont/headscale/releases/latest\n# option b. get latest tag, this may be a beta release\nlatestTag=$(git describe --tags `git rev-list --tags --max-count=1`)\n\ngit checkout $latestTag\n\ngo build -ldflags=\"-s -w -X github.com/juanfont/headscale/hscontrol/types.Version=$latestTag\" -X github.com/juanfont/headscale/hscontrol/types.GitCommitHash=HASH\" github.com/juanfont/headscale\n\n# make it executable\nchmod a+x headscale\n\n# copy it to /usr/local/sbin\ncp headscale /usr/local/sbin\n
"},{"location":"setup/install/source/#install-from-source-via-cross-compile","title":"Install from source via cross compile","text":"
# Install prerequisites\n# 1. go v1.20+: headscale newer than 0.21 needs go 1.20+ to compile\n# 2. gmake: Makefile in the headscale repo is written in GNU make syntax\n\ngit clone https://github.com/juanfont/headscale.git\n\ncd headscale\n\n# optionally checkout a release\n# option a. you can find official release at https://github.com/juanfont/headscale/releases/latest\n# option b. get latest tag, this may be a beta release\nlatestTag=$(git describe --tags `git rev-list --tags --max-count=1`)\n\ngit checkout $latestTag\n\nmake build GOOS=openbsd\n\n# copy headscale to openbsd machine and put it in /usr/local/sbin\n
"},{"location":"usage/getting-started/","title":"Getting started","text":"

This page helps you get started with headscale and provides a few usage examples for the headscale command line tool headscale.

Prerequisites

  • Headscale is installed and running as system service. Read the setup section for installation instructions.
  • The configuration file exists and is adjusted to suit your environment, see Configuration for details.
  • Headscale is reachable from the Internet. Verify this by opening client specific setup instructions in your browser, e.g. https://headscale.example.com/windows
  • The Tailscale client is installed, see Client and operating system support for more information.
"},{"location":"usage/getting-started/#getting-help","title":"Getting help","text":"

The headscale command line tool provides built-in help. To show available commands along with their arguments and options, run:

NativeContainer
# Show help\nheadscale help\n\n# Show help for a specific command\nheadscale <COMMAND> --help\n
# Show help\ndocker exec -it headscale \\\n  headscale help\n\n# Show help for a specific command\ndocker exec -it headscale \\\n  headscale <COMMAND> --help\n
"},{"location":"usage/getting-started/#manage-headscale-users","title":"Manage headscale users","text":"

In headscale, a node (also known as machine or device) is always assigned to a headscale user. Such a headscale user may have many nodes assigned to them and can be managed with the headscale users command. Invoke the built-in help for more information: headscale users --help.

"},{"location":"usage/getting-started/#create-a-headscale-user","title":"Create a headscale user","text":"NativeContainer
headscale users create <USER>\n
docker exec -it headscale \\\n  headscale users create <USER>\n
"},{"location":"usage/getting-started/#list-existing-headscale-users","title":"List existing headscale users","text":"NativeContainer
headscale users list\n
docker exec -it headscale \\\n  headscale users list\n
"},{"location":"usage/getting-started/#register-a-node","title":"Register a node","text":"

One has to register a node first to use headscale as coordination with Tailscale. The following examples work for the Tailscale client on Linux/BSD operating systems. Alternatively, follow the instructions to connect Android, Apple or Windows devices.

"},{"location":"usage/getting-started/#normal-interactive-login","title":"Normal, interactive login","text":"

On a client machine, run the tailscale up command and provide the FQDN of your headscale instance as argument:

tailscale up --login-server <YOUR_HEADSCALE_URL>\n

Usually, a browser window with further instructions is opened and contains the value for <YOUR_MACHINE_KEY>. Approve and register the node on your headscale server:

NativeContainer
headscale nodes register --user <USER> --key <YOUR_MACHINE_KEY>\n
docker exec -it headscale \\\n  headscale nodes register --user <USER> --key <YOUR_MACHINE_KEY>\n
"},{"location":"usage/getting-started/#using-a-preauthkey","title":"Using a preauthkey","text":"

It is also possible to generate a preauthkey and register a node non-interactively. First, generate a preauthkey on the headscale instance. By default, the key is valid for one hour and can only be used once (see headscale preauthkeys --help for other options):

NativeContainer
headscale preauthkeys create --user <USER>\n
docker exec -it headscale \\\n  headscale preauthkeys create --user <USER>\n

The command returns the preauthkey on success which is used to connect a node to the headscale instance via the tailscale up command:

tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>\n
"},{"location":"usage/connect/android/","title":"Connecting an Android client","text":"

This documentation has the goal of showing how a user can use the official Android Tailscale client with headscale.

"},{"location":"usage/connect/android/#installation","title":"Installation","text":"

Install the official Tailscale Android client from the Google Play Store or F-Droid.

"},{"location":"usage/connect/android/#configuring-the-headscale-url","title":"Configuring the headscale URL","text":"
  • Open the app and select the settings menu in the upper-right corner
  • Tap on Accounts
  • In the kebab menu icon (three dots) in the upper-right corner select Use an alternate server
  • Enter your server URL (e.g https://headscale.example.com) and follow the instructions
"},{"location":"usage/connect/apple/","title":"Connecting an Apple client","text":"

This documentation has the goal of showing how a user can use the official iOS and macOS Tailscale clients with headscale.

Instructions on your headscale instance

An endpoint with information on how to connect your Apple device is also available at /apple on your running instance.

"},{"location":"usage/connect/apple/#ios","title":"iOS","text":""},{"location":"usage/connect/apple/#installation","title":"Installation","text":"

Install the official Tailscale iOS client from the App Store.

"},{"location":"usage/connect/apple/#configuring-the-headscale-url","title":"Configuring the headscale URL","text":"
  • Open the Tailscale app
  • Click the account icon in the top-right corner and select Log in\u2026.
  • Tap the top-right options menu button and select Use custom coordination server.
  • Enter your instance url (e.g https://headscale.example.com)
  • Enter your credentials and log in. Headscale should now be working on your iOS device.
"},{"location":"usage/connect/apple/#macos","title":"macOS","text":""},{"location":"usage/connect/apple/#installation_1","title":"Installation","text":"

Choose one of the available Tailscale clients for macOS and install it.

"},{"location":"usage/connect/apple/#configuring-the-headscale-url_1","title":"Configuring the headscale URL","text":""},{"location":"usage/connect/apple/#command-line","title":"Command line","text":"

Use Tailscale's login command to connect with your headscale instance (e.g https://headscale.example.com):

tailscale login --login-server <YOUR_HEADSCALE_URL>\n
"},{"location":"usage/connect/apple/#gui","title":"GUI","text":"
  • Option + Click the Tailscale icon in the menu and hover over the Debug menu
  • Under Custom Login Server, select Add Account...
  • Enter the URL of your headscale instance (e.g https://headscale.example.com) and press Add Account
  • Follow the login procedure in the browser
"},{"location":"usage/connect/apple/#tvos","title":"tvOS","text":""},{"location":"usage/connect/apple/#installation_2","title":"Installation","text":"

Install the official Tailscale tvOS client from the App Store.

Danger

Don't open the Tailscale App after installation!

"},{"location":"usage/connect/apple/#configuring-the-headscale-url_2","title":"Configuring the headscale URL","text":"
  • Open Settings (the Apple tvOS settings) > Apps > Tailscale
  • Under ALTERNATE COORDINATION SERVER URL, select URL
  • Enter the URL of your headscale instance (e.g https://headscale.example.com) and press OK
  • Return to the tvOS Home screen
  • Open Tailscale
  • Click the button Install VPN configuration and confirm the appearing popup by clicking the Allow button
  • Scan the QR code and follow the login procedure
"},{"location":"usage/connect/windows/","title":"Connecting a Windows client","text":"

This documentation has the goal of showing how a user can use the official Windows Tailscale client with headscale.

Instructions on your headscale instance

An endpoint with information on how to connect your Windows device is also available at /windows on your running instance.

"},{"location":"usage/connect/windows/#installation","title":"Installation","text":"

Download the Official Windows Client and install it.

"},{"location":"usage/connect/windows/#configuring-the-headscale-url","title":"Configuring the headscale URL","text":"

Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g https://headscale.example.com):

tailscale login --login-server <YOUR_HEADSCALE_URL>\n

Follow the instructions in the opened browser window to finish the configuration.

"},{"location":"usage/connect/windows/#troubleshooting","title":"Troubleshooting","text":""},{"location":"usage/connect/windows/#unattended-mode","title":"Unattended mode","text":"

By default, Tailscale's Windows client is only running when the user is logged in. If you want to keep Tailscale running all the time, please enable \"Unattended mode\":

  • Click on the Tailscale tray icon and select Preferences
  • Enable Run unattended
  • Confirm the \"Unattended mode\" message

See also Keep Tailscale running when I'm not logged in to my computer

"},{"location":"usage/connect/windows/#failing-node-registration","title":"Failing node registration","text":"

If you are seeing repeated messages like:

[GIN] 2022/02/10 - 16:39:34 | 200 |    1.105306ms |       127.0.0.1 | POST     \"/machine/redacted\"\n

in your headscale output, turn on DEBUG logging and look for:

2022-02-11T00:59:29Z DBG Machine registration has expired. Sending a authurl to register machine=redacted\n

This typically means that the registry keys above was not set appropriately.

To reset and try again, it is important to do the following:

  1. Shut down the Tailscale service (or the client running in the tray)
  2. Delete Tailscale Application data folder, located at C:\\Users\\<USERNAME>\\AppData\\Local\\Tailscale and try to connect again.
  3. Ensure the Windows node is deleted from headscale (to ensure fresh setup)
  4. Start Tailscale on the Windows machine and retry the login.
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-,:!=\\[\\]()\"`/]+|\\.(?!\\d)|&[lg]t;|(?!\\b)(?=[A-Z][a-z])","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Welcome to headscale","text":"

Headscale is an open source, self-hosted implementation of the Tailscale control server.

This page contains the documentation for the latest version of headscale. Please also check our FAQ.

Join our Discord server for a chat and community support.

"},{"location":"#design-goal","title":"Design goal","text":"

Headscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscale's goal is to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. It implements a narrow scope, a single Tailscale network (tailnet), suitable for a personal use, or a small open-source organisation.

"},{"location":"#supporting-headscale","title":"Supporting headscale","text":"

Please see Sponsor for more information.

"},{"location":"#contributing","title":"Contributing","text":"

Headscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be discussed with the Maintainers before being submitted.

Please see Contributing for more information.

"},{"location":"#about","title":"About","text":"

Headscale is maintained by Kristoffer Dalby and Juan Font.

"},{"location":"about/clients/","title":"Client and operating system support","text":"

We aim to support the last 10 releases of the Tailscale client on all provided operating systems and platforms. Some platforms might require additional configuration to connect with headscale.

OS Supports headscale Linux Yes OpenBSD Yes FreeBSD Yes Windows Yes (see docs and /windows on your headscale for more information) Android Yes (see docs for more information) macOS Yes (see docs and /apple on your headscale for more information) iOS Yes (see docs and /apple on your headscale for more information) tvOS Yes (see docs and /apple on your headscale for more information)"},{"location":"about/contributing/","title":"Contributing","text":"

Headscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be discussed with the maintainers before being added to the project. This model has been chosen to reduce the risk of burnout by limiting the maintenance overhead of reviewing and validating third-party code.

"},{"location":"about/contributing/#why-do-we-have-this-model","title":"Why do we have this model?","text":"

Headscale has a small maintainer team that tries to balance working on the project, fixing bugs and reviewing contributions.

When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops.

Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly.

The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added.

This means that when someone contributes, we are mostly happy about it, but we do have to run it through a series of checks to establish if we actually can maintain this feature.

"},{"location":"about/contributing/#what-do-we-require","title":"What do we require?","text":"

A general description is provided here and an explicit list is provided in our pull request template.

All new features have to start out with a design document, which should be discussed on the issue tracker (not discord). It should include a use case for the feature, how it can be implemented, who will implement it and a plan for maintaining it.

All features have to be end-to-end tested (integration tests) and have good unit test coverage to ensure that they work as expected. This will also ensure that the feature continues to work as expected over time. If a change cannot be tested, a strong case for why this is not possible needs to be presented.

The contributor should help to maintain the feature over time. In case the feature is not maintained probably, the maintainers reserve themselves the right to remove features they redeem as unmaintainable. This should help to improve the quality of the software and keep it in a maintainable state.

"},{"location":"about/contributing/#bug-fixes","title":"Bug fixes","text":"

Headscale is open to code contributions for bug fixes without discussion.

"},{"location":"about/contributing/#documentation","title":"Documentation","text":"

If you find mistakes in the documentation, please submit a fix to the documentation.

"},{"location":"about/faq/","title":"Frequently Asked Questions","text":""},{"location":"about/faq/#what-is-the-design-goal-of-headscale","title":"What is the design goal of headscale?","text":"

Headscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscale's goal is to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. It implements a narrow scope, a single Tailscale network (tailnet), suitable for a personal use, or a small open-source organisation.

"},{"location":"about/faq/#how-can-i-contribute","title":"How can I contribute?","text":"

Headscale is \"Open Source, acknowledged contribution\", this means that any contribution will have to be discussed with the Maintainers before being submitted.

Please see Contributing for more information.

"},{"location":"about/faq/#why-is-acknowledged-contribution-the-chosen-model","title":"Why is 'acknowledged contribution' the chosen model?","text":"

Both maintainers have full-time jobs and families, and we want to avoid burnout. We also want to avoid frustration from contributors when their PRs are not accepted.

We are more than happy to exchange emails, or to have dedicated calls before a PR is submitted.

"},{"location":"about/faq/#whenwhy-is-feature-x-going-to-be-implemented","title":"When/Why is Feature X going to be implemented?","text":"

We don't know. We might be working on it. If you're interested in contributing, please post a feature request about it.

Please be aware that there are a number of reasons why we might not accept specific contributions:

  • It is not possible to implement the feature in a way that makes sense in a self-hosted environment.
  • Given that we are reverse-engineering Tailscale to satisfy our own curiosity, we might be interested in implementing the feature ourselves.
  • You are not sending unit and integration tests with it.
"},{"location":"about/faq/#do-you-support-y-method-of-deploying-headscale","title":"Do you support Y method of deploying headscale?","text":"

We currently support deploying headscale using our binaries and the DEB packages. Visit our installation guide using official releases for more information.

In addition to that, you may use packages provided by the community or from distributions. Learn more in the installation guide using community packages.

For convenience, we also build container images with headscale. But please be aware that we don't officially support deploying headscale using Docker. On our Discord server we have a \"docker-issues\" channel where you can ask for Docker-specific help to the community.

"},{"location":"about/faq/#scaling-how-many-clients-does-headscale-support","title":"Scaling / How many clients does Headscale support?","text":"

It depends. As often stated, Headscale is not enterprise software and our focus is homelabbers and self-hosters. Of course, we do not prevent people from using it in a commercial/professional setting and often get questions about scaling.

Please note that when Headscale is developed, performance is not part of the consideration as the main audience is considered to be users with a moddest amount of devices. We focus on correctness and feature parity with Tailscale SaaS over time.

To understand if you might be able to use Headscale for your usecase, I will describe two scenarios in an effort to explain what is the central bottleneck of Headscale:

  1. An environment with 1000 servers

  2. they rarely \"move\" (change their endpoints)

  3. new nodes are added rarely

  4. An environment with 80 laptops/phones (end user devices)

  5. nodes move often, e.g. switching from home to office

Headscale calculates a map of all nodes that need to talk to each other, creating this \"world map\" requires a lot of CPU time. When an event that requires changes to this map happens, the whole \"world\" is recalculated, and a new \"world map\" is created for every node in the network.

This means that under certain conditions, Headscale can likely handle 100s of devices (maybe more), if there is little to no change happening in the network. For example, in Scenario 1, the process of computing the world map is extremly demanding due to the size of the network, but when the map has been created and the nodes are not changing, the Headscale instance will likely return to a very low resource usage until the next time there is an event requiring the new map.

In the case of Scenario 2, the process of computing the world map is less demanding due to the smaller size of the network, however, the type of nodes will likely change frequently, which would lead to a constant resource usage.

Headscale will start to struggle when the two scenarios overlap, e.g. many nodes with frequent changes will cause the resource usage to remain constantly high. In the worst case scenario, the queue of nodes waiting for their map will grow to a point where Headscale never will be able to catch up, and nodes will never learn about the current state of the world.

We expect that the performance will improve over time as we improve the code base, but it is not a focus. In general, we will never make the tradeoff to make things faster on the cost of less maintainable or readable code. We are a small team and have to optimise for maintainabillity.

"},{"location":"about/faq/#which-database-should-i-use","title":"Which database should I use?","text":"

We recommend the use of SQLite as database for headscale:

  • SQLite is simple to setup and easy to use
  • It scales well for all of headscale's usecases
  • Development and testing happens primarily on SQLite
  • PostgreSQL is still supported, but is considered to be in \"maintenance mode\"

The headscale project itself does not provide a tool to migrate from PostgreSQL to SQLite. Please have a look at the related tools documentation for migration tooling provided by the community.

The choice of database has little to no impact on the performance of the server, see Scaling / How many clients does Headscale support? for understanding how Headscale spends its resources.

"},{"location":"about/faq/#why-is-my-reverse-proxy-not-working-with-headscale","title":"Why is my reverse proxy not working with headscale?","text":"

We don't know. We don't use reverse proxies with headscale ourselves, so we don't have any experience with them. We have community documentation on how to configure various reverse proxies, and a dedicated \"reverse-proxy-issues\" channel on our Discord server where you can ask for help to the community.

"},{"location":"about/faq/#can-i-use-headscale-and-tailscale-on-the-same-machine","title":"Can I use headscale and tailscale on the same machine?","text":"

Running headscale on a machine that is also in the tailnet can cause problems with subnet routers, traffic relay nodes, and MagicDNS. It might work, but it is not supported.

"},{"location":"about/faq/#why-do-two-nodes-see-each-other-in-their-status-even-if-an-acl-allows-traffic-only-in-one-direction","title":"Why do two nodes see each other in their status, even if an ACL allows traffic only in one direction?","text":"

A frequent use case is to allow traffic only from one node to another, but not the other way around. For example, the workstation of an administrator should be able to connect to all nodes but the nodes themselves shouldn't be able to connect back to the administrator's node. Why do all nodes see the administrator's workstation in the output of tailscale status?

This is essentially how Tailscale works. If traffic is allowed to flow in one direction, then both nodes see each other in their output of tailscale status. Traffic is still filtered according to the ACL, with the exception of tailscale ping which is always allowed in either direction.

See also https://tailscale.com/kb/1087/device-visibility.

"},{"location":"about/features/","title":"Features","text":"

Headscale aims to implement a self-hosted, open source alternative to the Tailscale control server. Headscale's goal is to provide self-hosters and hobbyists with an open-source server they can use for their projects and labs. This page provides on overview of Headscale's feature and compatibility with the Tailscale control server:

  • Full \"base\" support of Tailscale's features
  • Node registration
    • Interactive
    • Pre authenticated key
  • DNS
    • MagicDNS
    • Global and restricted nameservers (split DNS)
    • search domains
    • Extra DNS records (Headscale only)
  • Taildrop (File Sharing)
  • Routes
    • Subnet routers
    • Exit nodes
  • Dual stack (IPv4 and IPv6)
  • Ephemeral nodes
  • Embedded DERP server
  • Access control lists (GitHub label \"policy\")
    • ACL management via API
    • Some Autogroups, currently: autogroup:internet, autogroup:nonroot, autogroup:member, autogroup:tagged
    • Auto approvers for subnet routers and exit nodes
    • Tailscale SSH
  • Node registration using Single-Sign-On (OpenID Connect) (GitHub label \"OIDC\")
    • Basic registration
    • Update user profile from identity provider
    • OIDC groups cannot be used in ACLs
  • Funnel (#1040)
  • Serve (#1234)
  • Network flow logs (#1687)
"},{"location":"about/help/","title":"Getting help","text":"

Join our Discord server for announcements and community support.

Please report bugs via GitHub issues

"},{"location":"about/releases/","title":"Releases","text":"

All headscale releases are available on the GitHub release page. Those releases are available as binaries for various platforms and architectures, packages for Debian based systems and source code archives. Container images are available on Docker Hub and GitHub Container Registry.

An Atom/RSS feed of headscale releases is available here.

See the \"announcements\" channel on our Discord server for news about headscale.

"},{"location":"about/sponsor/","title":"Sponsor","text":"

If you like to support the development of headscale, please consider a donation via ko-fi.com/headscale. Thank you!

"},{"location":"ref/acls/","title":"ACLs","text":"

Headscale implements the same policy ACLs as Tailscale.com, adapted to the self-hosted environment.

For instance, instead of referring to users when defining groups you must use users (which are the equivalent to user/logins in Tailscale.com).

Please check https://tailscale.com/kb/1018/acls/ for further information.

When using ACL's the User borders are no longer applied. All machines whichever the User have the ability to communicate with other hosts as long as the ACL's permits this exchange.

"},{"location":"ref/acls/#acls-use-case-example","title":"ACLs use case example","text":"

Let's build an example use case for a small business (It may be the place where ACL's are the most useful).

We have a small company with a boss, an admin, two developers and an intern.

The boss should have access to all servers but not to the user's hosts. Admin should also have access to all hosts except that their permissions should be limited to maintaining the hosts (for example purposes). The developers can do anything they want on dev hosts but only watch on productions hosts. Intern can only interact with the development servers.

There's an additional server that acts as a router, connecting the VPN users to an internal network 10.20.0.0/16. Developers must have access to those internal resources.

Each user have at least a device connected to the network and we have some servers.

  • database.prod
  • database.dev
  • app-server1.prod
  • app-server1.dev
  • billing.internal
  • router.internal

"},{"location":"ref/acls/#acl-setup","title":"ACL setup","text":"

ACLs have to be written in huJSON.

When registering the servers we will need to add the flag --advertise-tags=tag:<tag1>,tag:<tag2>, and the user that is registering the server should be allowed to do it. Since anyone can add tags to a server they can register, the check of the tags is done on headscale server and only valid tags are applied. A tag is valid if the user that is registering it is allowed to do it.

To use ACLs in headscale, you must edit your config.yaml file. In there you will find a policy.path parameter. This will need to point to your ACL file. More info on how these policies are written can be found here.

Please reload or restart Headscale after updating the ACL file. Headscale may be reloaded either via its systemd service (sudo systemctl reload headscale) or by sending a SIGHUP signal (sudo kill -HUP $(pidof headscale)) to the main process. Headscale logs the result of ACL policy processing after each reload.

Here are the ACL's to implement the same permissions as above:

acl.json
{\n  // groups are collections of users having a common scope. A user can be in multiple groups\n  // groups cannot be composed of groups\n  \"groups\": {\n    \"group:boss\": [\"boss@\"],\n    \"group:dev\": [\"dev1@\", \"dev2@\"],\n    \"group:admin\": [\"admin1@\"],\n    \"group:intern\": [\"intern1@\"]\n  },\n  // tagOwners in tailscale is an association between a TAG and the people allowed to set this TAG on a server.\n  // This is documented [here](https://tailscale.com/kb/1068/acl-tags#defining-a-tag)\n  // and explained [here](https://tailscale.com/blog/rbac-like-it-was-meant-to-be/)\n  \"tagOwners\": {\n    // the administrators can add servers in production\n    \"tag:prod-databases\": [\"group:admin\"],\n    \"tag:prod-app-servers\": [\"group:admin\"],\n\n    // the boss can tag any server as internal\n    \"tag:internal\": [\"group:boss\"],\n\n    // dev can add servers for dev purposes as well as admins\n    \"tag:dev-databases\": [\"group:admin\", \"group:dev\"],\n    \"tag:dev-app-servers\": [\"group:admin\", \"group:dev\"]\n\n    // interns cannot add servers\n  },\n  // hosts should be defined using its IP addresses and a subnet mask.\n  // to define a single host, use a /32 mask. You cannot use DNS entries here,\n  // as they're prone to be hijacked by replacing their IP addresses.\n  // see https://github.com/tailscale/tailscale/issues/3800 for more information.\n  \"hosts\": {\n    \"postgresql.internal\": \"10.20.0.2/32\",\n    \"webservers.internal\": \"10.20.10.1/29\"\n  },\n  \"acls\": [\n    // boss have access to all servers\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:boss\"],\n      \"dst\": [\n        \"tag:prod-databases:*\",\n        \"tag:prod-app-servers:*\",\n        \"tag:internal:*\",\n        \"tag:dev-databases:*\",\n        \"tag:dev-app-servers:*\"\n      ]\n    },\n\n    // admin have only access to administrative ports of the servers, in tcp/22\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:admin\"],\n      \"proto\": \"tcp\",\n      \"dst\": [\n        \"tag:prod-databases:22\",\n        \"tag:prod-app-servers:22\",\n        \"tag:internal:22\",\n        \"tag:dev-databases:22\",\n        \"tag:dev-app-servers:22\"\n      ]\n    },\n\n    // we also allow admin to ping the servers\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:admin\"],\n      \"proto\": \"icmp\",\n      \"dst\": [\n        \"tag:prod-databases:*\",\n        \"tag:prod-app-servers:*\",\n        \"tag:internal:*\",\n        \"tag:dev-databases:*\",\n        \"tag:dev-app-servers:*\"\n      ]\n    },\n\n    // developers have access to databases servers and application servers on all ports\n    // they can only view the applications servers in prod and have no access to databases servers in production\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:dev\"],\n      \"dst\": [\n        \"tag:dev-databases:*\",\n        \"tag:dev-app-servers:*\",\n        \"tag:prod-app-servers:80,443\"\n      ]\n    },\n    // developers have access to the internal network through the router.\n    // the internal network is composed of HTTPS endpoints and Postgresql\n    // database servers.\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:dev\"],\n      \"dst\": [\"10.20.0.0/16:443,5432\"]\n    },\n\n    // servers should be able to talk to database in tcp/5432. Database should not be able to initiate connections to\n    // applications servers\n    {\n      \"action\": \"accept\",\n      \"src\": [\"tag:dev-app-servers\"],\n      \"proto\": \"tcp\",\n      \"dst\": [\"tag:dev-databases:5432\"]\n    },\n    {\n      \"action\": \"accept\",\n      \"src\": [\"tag:prod-app-servers\"],\n      \"dst\": [\"tag:prod-databases:5432\"]\n    },\n\n    // interns have access to dev-app-servers only in reading mode\n    {\n      \"action\": \"accept\",\n      \"src\": [\"group:intern\"],\n      \"dst\": [\"tag:dev-app-servers:80,443\"]\n    },\n\n    // We still have to allow internal users communications since nothing guarantees that each user have\n    // their own users.\n    { \"action\": \"accept\", \"src\": [\"boss@\"], \"dst\": [\"boss@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"dev1@\"], \"dst\": [\"dev1@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"dev2@\"], \"dst\": [\"dev2@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"admin1@\"], \"dst\": [\"admin1@:*\"] },\n    { \"action\": \"accept\", \"src\": [\"intern1@\"], \"dst\": [\"intern1@:*\"] }\n  ]\n}\n
"},{"location":"ref/configuration/","title":"Configuration","text":"
  • Headscale loads its configuration from a YAML file
  • It searches for config.yaml in the following paths:
    • /etc/headscale
    • $HOME/.headscale
    • the current working directory
  • To load the configuration from a different path, use:
    • the command line flag -c, --config
    • the environment variable HEADSCALE_CONFIG
  • Validate the configuration file with: headscale configtest

Get the example configuration from the GitHub repository

Always select the same GitHub tag as the released version you use to ensure you have the correct example configuration. The main branch might contain unreleased changes.

View on GitHubDownload with wgetDownload with curl
  • Development version: https://github.com/juanfont/headscale/blob/main/config-example.yaml
  • Version 0.26.0: https://github.com/juanfont/headscale/blob/v0.26.0/config-example.yaml
# Development version\nwget -O config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml\n\n# Version 0.26.0\nwget -O config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.26.0/config-example.yaml\n
# Development version\ncurl -o config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml\n\n# Version 0.26.0\ncurl -o config.yaml https://raw.githubusercontent.com/juanfont/headscale/v0.26.0/config-example.yaml\n
"},{"location":"ref/dns/","title":"DNS","text":"

Headscale supports most DNS features from Tailscale. DNS related settings can be configured within dns section of the configuration file.

"},{"location":"ref/dns/#setting-extra-dns-records","title":"Setting extra DNS records","text":"

Headscale allows to set extra DNS records which are made available via MagicDNS. Extra DNS records can be configured either via static entries in the configuration file or from a JSON file that Headscale continuously watches for changes:

  • Use the dns.extra_records option in the configuration file for entries that are static and don't change while Headscale is running. Those entries are processed when Headscale is starting up and changes to the configuration require a restart of Headscale.
  • For dynamic DNS records that may be added, updated or removed while Headscale is running or DNS records that are generated by scripts the option dns.extra_records_path in the configuration file is useful. Set it to the absolute path of the JSON file containing DNS records and Headscale processes this file as it detects changes.

An example use case is to serve multiple apps on the same host via a reverse proxy like NGINX, in this case a Prometheus monitoring stack. This allows to nicely access the service with \"http://grafana.myvpn.example.com\" instead of the hostname and port combination \"http://hostname-in-magic-dns.myvpn.example.com:3000\".

Limitations

Currently, only A and AAAA records are processed by Tailscale.

  1. Configure extra DNS records using one of the available configuration options:

    Static entries, via dns.extra_recordsDynamic entries, via dns.extra_records_path config.yaml
    dns:\n  ...\n  extra_records:\n    - name: \"grafana.myvpn.example.com\"\n      type: \"A\"\n      value: \"100.64.0.3\"\n\n    - name: \"prometheus.myvpn.example.com\"\n      type: \"A\"\n      value: \"100.64.0.3\"\n  ...\n

    Restart your headscale instance.

    extra-records.json
    [\n  {\n    \"name\": \"grafana.myvpn.example.com\",\n    \"type\": \"A\",\n    \"value\": \"100.64.0.3\"\n  },\n  {\n    \"name\": \"prometheus.myvpn.example.com\",\n    \"type\": \"A\",\n    \"value\": \"100.64.0.3\"\n  }\n]\n

    Headscale picks up changes to the above JSON file automatically.

    Good to know

    • The dns.extra_records_path option in the configuration file needs to reference the JSON file containing extra DNS records.
    • Be sure to \"sort keys\" and produce a stable output in case you generate the JSON file with a script. Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.
  2. Verify that DNS records are properly set using the DNS querying tool of your choice:

    Query with digQuery with drill
    dig +short grafana.myvpn.example.com\n100.64.0.3\n
    drill -Q grafana.myvpn.example.com\n100.64.0.3\n
  3. Optional: Setup the reverse proxy

    The motivating example here was to be able to access internal monitoring services on the same host without specifying a port, depicted as NGINX configuration snippet:

    nginx.conf
    server {\n    listen 80;\n    listen [::]:80;\n\n    server_name grafana.myvpn.example.com;\n\n    location / {\n        proxy_pass http://localhost:3000;\n        proxy_set_header Host $http_host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n    }\n\n}\n
"},{"location":"ref/oidc/","title":"OpenID Connect","text":"

Headscale supports authentication via external identity providers using OpenID Connect (OIDC). It features:

  • Autoconfiguration via OpenID Connect Discovery Protocol
  • Proof Key for Code Exchange (PKCE) code verification
  • Authorization based on a user's domain, email address or group membership
  • Synchronization of standard OIDC claims

Please see limitations for known issues and limitations.

"},{"location":"ref/oidc/#configuration","title":"Configuration","text":"

OpenID requires configuration in Headscale and your identity provider:

  • Headscale: The oidc section of the Headscale configuration contains all available configuration options along with a description and their default values.
  • Identity provider: Please refer to the official documentation of your identity provider for specific instructions. Additionally, there might be some useful hints in the Identity provider specific configuration section below.
"},{"location":"ref/oidc/#basic-configuration","title":"Basic configuration","text":"

A basic configuration connects Headscale to an identity provider and typically requires:

  • OpenID Connect Issuer URL from the identity provider. Headscale uses the OpenID Connect Discovery Protocol 1.0 to automatically obtain OpenID configuration parameters (example: https://sso.example.com).
  • Client ID from the identity provider (example: headscale).
  • Client secret generated by the identity provider (example: generated-secret).
  • Redirect URI for your identity provider (example: https://headscale.example.com/oidc/callback).
HeadscaleIdentity provider
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n
  • Create a new confidential client (Client ID, Client secret)
  • Add Headscale's OIDC callback URL as valid redirect URL: https://headscale.example.com/oidc/callback
  • Configure additional parameters to improve user experience such as: name, description, logo, \u2026
"},{"location":"ref/oidc/#enable-pkce-recommended","title":"Enable PKCE (recommended)","text":"

Proof Key for Code Exchange (PKCE) adds an additional layer of security to the OAuth 2.0 authorization code flow by preventing authorization code interception attacks, see: https://datatracker.ietf.org/doc/html/rfc7636. PKCE is recommended and needs to be configured for Headscale and the identity provider alike:

HeadscaleIdentity provider
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  pkce:\n    enabled: true\n
  • Enable PKCE for the headscale client
  • Set the PKCE challenge method to \"S256\"
"},{"location":"ref/oidc/#authorize-users-with-filters","title":"Authorize users with filters","text":"

Headscale allows to filter for allowed users based on their domain, email address or group membership. These filters can be helpful to apply additional restrictions and control which users are allowed to join. Filters are disabled by default, users are allowed to join once the authentication with the identity provider succeeds. In case multiple filters are configured, a user needs to pass all of them.

Allowed domainsAllowed users/emailsAllowed groups
  • Check the email domain of each authenticating user against the list of allowed domains and only authorize users whose email domain matches example.com.
  • Access allowed: alice@example.com
  • Access denied: bob@example.net
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  allowed_domains:\n    - \"example.com\"\n
  • Check the email address of each authenticating user against the list of allowed email addresses and only authorize users whose email is part of the allowed_users list.
  • Access allowed: alice@example.com, bob@example.net
  • Access denied: mallory@example.net
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  allowed_users:\n    - \"alice@example.com\"\n    - \"bob@example.net\"\n
  • Use the OIDC groups claim of each authenticating user to get their group membership and only authorize users which are members in at least one of the referenced groups.
  • Access allowed: users in the headscale_users group
  • Access denied: users without groups, users with other groups
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  scope: [\"openid\", \"profile\", \"email\", \"groups\"]\n  allowed_groups:\n    - \"headscale_users\"\n
"},{"location":"ref/oidc/#customize-node-expiration","title":"Customize node expiration","text":"

The node expiration is the amount of time a node is authenticated with OpenID Connect until it expires and needs to reauthenticate. The default node expiration is 180 days. This can either be customized or set to the expiration from the Access Token.

Customize node expirationUse expiration from Access Token
oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  expiry: 30d   # Use 0 to disable node expiration\n

Please keep in mind that the Access Token is typically a short-lived token that expires within a few minutes. You will have to configure token expiration in your identity provider to avoid frequent reauthentication.

oidc:\n  issuer: \"https://sso.example.com\"\n  client_id: \"headscale\"\n  client_secret: \"generated-secret\"\n  use_expiry_from_token: true\n

Expire a node and force re-authentication

A node can be expired immediately via:

headscale node expire -i <NODE_ID>\n

"},{"location":"ref/oidc/#reference-a-user-in-the-policy","title":"Reference a user in the policy","text":"

You may refer to users in the Headscale policy via:

  • Email address
  • Username
  • Provider identifier (only available in the database or from your identity provider)

A user identifier in the policy must contain a single @

The Headscale policy requires a single @ to reference a user. If the username or provider identifier doesn't already contain a single @, it needs to be appended at the end. For example: the username ssmith has to be written as ssmith@ to be correctly identified as user within the policy.

Email address or username might be updated by users

Many identity providers allow users to update their own profile. Depending on the identity provider and its configuration, the values for username or email address might change over time. This might have unexpected consequences for Headscale where a policy might no longer work or a user might obtain more access by hijacking an existing username or email address.

"},{"location":"ref/oidc/#supported-oidc-claims","title":"Supported OIDC claims","text":"

Headscale uses the standard OIDC claims to populate and update its local user profile on each login. OIDC claims are read from the ID Token or from the UserInfo endpoint.

Headscale profile OIDC claim Notes / examples email address email Only used when email_verified: true display name name eg: Sam Smith username preferred_username Depends on identity provider, eg: ssmith, ssmith@idp.example.com, \\\\example.com\\ssmith profile picture picture URL to a profile picture or avatar provider identifier iss, sub A stable and unique identifier for a user, typically a combination of iss and sub OIDC claims groups Only used to filter for allowed groups"},{"location":"ref/oidc/#limitations","title":"Limitations","text":"
  • Support for OpenID Connect aims to be generic and vendor independent. It offers only limited support for quirks of specific identity providers.
  • OIDC groups cannot be used in ACLs.
  • The username provided by the identity provider needs to adhere to this pattern:
    • The username must be at least two characters long.
    • It must only contain letters, digits, hyphens, dots, underscores, and up to a single @.
    • The username must start with a letter.
  • A user's email address is only synchronized to the local user profile when the identity provider marks the email address as verified (email_verified: true).

Please see the GitHub label \"OIDC\" for OIDC related issues.

"},{"location":"ref/oidc/#identity-provider-specific-configuration","title":"Identity provider specific configuration","text":"

Third-party software and services

This section of the documentation is specific for third-party software and services. We recommend users read the third-party documentation on how to configure and integrate an OIDC client. Please see the Configuration section for a description of Headscale's OIDC related configuration settings.

Any identity provider with OpenID Connect support should \"just work\" with Headscale. The following identity providers are known to work:

  • Authelia
  • Authentik
  • Kanidm
  • Keycloak
"},{"location":"ref/oidc/#authelia","title":"Authelia","text":"

Authelia is fully supported by Headscale.

"},{"location":"ref/oidc/#additional-configuration-to-authorize-users-based-on-filters","title":"Additional configuration to authorize users based on filters","text":"

Authelia (4.39.0 or newer) no longer provides standard OIDC claims such as email or groups via the ID Token. The OIDC email and groups claims are used to authorize users with filters. This extra configuration step is only needed if you need to authorize access based on one of the following user properties:

  • domain
  • email address
  • group membership

Please follow the instructions from Authelia's documentation on how to Restore Functionality Prior to Claims Parameter.

"},{"location":"ref/oidc/#authentik","title":"Authentik","text":"
  • Authentik is fully supported by Headscale.
  • Headscale does not JSON Web Encryption. Leave the field Encryption Key in the providers section unset.
"},{"location":"ref/oidc/#google-oauth","title":"Google OAuth","text":"

No username due to missing preferred_username

Google OAuth does not send the preferred_username claim when the scope profile is requested. The username in Headscale will be blank/not set.

In order to integrate Headscale with Google, you'll need to have a Google Cloud Console account.

Google OAuth has a verification process if you need to have users authenticate who are outside of your domain. If you only need to authenticate users from your domain name (ie @example.com), you don't need to go through the verification process.

However if you don't have a domain, or need to add users outside of your domain, you can manually add emails via Google Console.

"},{"location":"ref/oidc/#steps","title":"Steps","text":"
  1. Go to Google Console and login or create an account if you don't have one.
  2. Create a project (if you don't already have one).
  3. On the left hand menu, go to APIs and services -> Credentials
  4. Click Create Credentials -> OAuth client ID
  5. Under Application Type, choose Web Application
  6. For Name, enter whatever you like
  7. Under Authorised redirect URIs, add Headscale's OIDC callback URL: https://headscale.example.com/oidc/callback
  8. Click Save at the bottom of the form
  9. Take note of the Client ID and Client secret, you can also download it for reference if you need it.
  10. Configure Headscale following the \"Basic configuration\" steps. The issuer URL for Google OAuth is: https://accounts.google.com.
"},{"location":"ref/oidc/#kanidm","title":"Kanidm","text":"
  • Kanidm is fully supported by Headscale.
  • Groups for the allowed groups filter need to be specified with their full SPN, for example: headscale_users@sso.example.com.
"},{"location":"ref/oidc/#keycloak","title":"Keycloak","text":"

Keycloak is fully supported by Headscale.

"},{"location":"ref/oidc/#additional-configuration-to-use-the-allowed-groups-filter","title":"Additional configuration to use the allowed groups filter","text":"

Keycloak has no built-in client scope for the OIDC groups claim. This extra configuration step is only needed if you need to authorize access based on group membership.

  • Create a new client scope groups for OpenID Connect:
    • Configure a Group Membership mapper with name groups and the token claim name groups.
    • Enable the mapper for the ID Token, Access Token and UserInfo endpoint.
  • Configure the new client scope for your Headscale client:
    • Edit the Headscale client.
    • Search for the client scope group.
    • Add it with assigned type Default.
  • Configure the allowed groups in Headscale. Keep in mind that groups in Keycloak start with a leading /.
"},{"location":"ref/oidc/#microsoft-entra-id","title":"Microsoft Entra ID","text":"

In order to integrate Headscale with Microsoft Entra ID, you'll need to provision an App Registration with the correct scopes and redirect URI.

Configure Headscale following the \"Basic configuration\" steps. The issuer URL for Microsoft Entra ID is: https://login.microsoftonline.com/<tenant-UUID>/v2.0. The following extra_params might be useful:

  • domain_hint: example.com to use your own domain
  • prompt: select_account to force an account picker during login
"},{"location":"ref/remote-cli/","title":"Controlling headscale with remote CLI","text":"

This documentation has the goal of showing a user how-to control a headscale instance from a remote machine with the headscale command line binary.

"},{"location":"ref/remote-cli/#prerequisite","title":"Prerequisite","text":"
  • A workstation to run headscale (any supported platform, e.g. Linux).
  • A headscale server with gRPC enabled.
  • Connections to the gRPC port (default: 50443) are allowed.
  • Remote access requires an encrypted connection via TLS.
  • An API key to authenticate with the headscale server.
"},{"location":"ref/remote-cli/#create-an-api-key","title":"Create an API key","text":"

We need to create an API key to authenticate with the remote headscale server when using it from our workstation.

To create an API key, log into your headscale server and generate a key:

headscale apikeys create --expiration 90d\n

Copy the output of the command and save it for later. Please note that you can not retrieve a key again, if the key is lost, expire the old one, and create a new key.

To list the keys currently associated with the server:

headscale apikeys list\n

and to expire a key:

headscale apikeys expire --prefix \"<PREFIX>\"\n
"},{"location":"ref/remote-cli/#download-and-configure-headscale","title":"Download and configure headscale","text":"
  1. Download the headscale binary from GitHub's release page. Make sure to use the same version as on the server.

  2. Put the binary somewhere in your PATH, e.g. /usr/local/bin/headscale

  3. Make headscale executable:

    chmod +x /usr/local/bin/headscale\n
  4. Provide the connection parameters for the remote headscale server either via a minimal YAML configuration file or via environment variables:

    Minimal YAML configuration fileEnvironment variables config.yaml
    cli:\n    address: <HEADSCALE_ADDRESS>:<PORT>\n    api_key: <API_KEY_FROM_PREVIOUS_STEP>\n
    export HEADSCALE_CLI_ADDRESS=\"<HEADSCALE_ADDRESS>:<PORT>\"\nexport HEADSCALE_CLI_API_KEY=\"<API_KEY_FROM_PREVIOUS_STEP>\"\n

    Bug

    Headscale currently requires at least an empty configuration file when environment variables are used to specify connection details. See issue 2193 for more information.

    This instructs the headscale binary to connect to a remote instance at <HEADSCALE_ADDRESS>:<PORT>, instead of connecting to the local instance.

  5. Test the connection

    Let us run the headscale command to verify that we can connect by listing our nodes:

    headscale nodes list\n

    You should now be able to see a list of your nodes from your workstation, and you can now control the headscale server from your workstation.

"},{"location":"ref/remote-cli/#behind-a-proxy","title":"Behind a proxy","text":"

It is possible to run the gRPC remote endpoint behind a reverse proxy, like Nginx, and have it run on the same port as headscale.

While this is not a supported feature, an example on how this can be set up on NixOS is shown here.

"},{"location":"ref/remote-cli/#troubleshooting","title":"Troubleshooting","text":"
  • Make sure you have the same headscale version on your server and workstation.
  • Ensure that connections to the gRPC port are allowed.
  • Verify that your TLS certificate is valid and trusted.
  • If you don't have access to a trusted certificate (e.g. from Let's Encrypt), either:
    • Add your self-signed certificate to the trust store of your OS or
    • Disable certificate verification by either setting cli.insecure: true in the configuration file or by setting HEADSCALE_CLI_INSECURE=1 via an environment variable. We do not recommend to disable certificate validation.
"},{"location":"ref/routes/","title":"Routes","text":"

Headscale supports route advertising and can be used to manage subnet routers and exit nodes for a tailnet.

  • Subnet routers may be used to connect an existing network such as a virtual private cloud or an on-premise network with your tailnet. Use a subnet router to access devices where Tailscale can't be installed or to gradually rollout Tailscale.
  • Exit nodes can be used to route all Internet traffic for another Tailscale node. Use it to securely access the Internet on an untrusted Wi-Fi or to access online services that expect traffic from a specific IP address.
"},{"location":"ref/routes/#subnet-router","title":"Subnet router","text":"

The setup of a subnet router requires double opt-in, once from a subnet router and once on the control server to allow its use within the tailnet. Optionally, use autoApprovers to automatically approve routes from a subnet router.

"},{"location":"ref/routes/#setup-a-subnet-router","title":"Setup a subnet router","text":""},{"location":"ref/routes/#configure-a-node-as-subnet-router","title":"Configure a node as subnet router","text":"

Register a node and advertise the routes it should handle as comma separated list:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-routes=10.0.0.0/8,192.168.0.0/24\n

If the node is already registered, it can advertise new routes or update previously announced routes with:

$ sudo tailscale set --advertise-routes=10.0.0.0/8,192.168.0.0/24\n

Finally, enable IP forwarding to route traffic.

"},{"location":"ref/routes/#enable-the-subnet-router-on-the-control-server","title":"Enable the subnet router on the control server","text":"

The routes of a tailnet can be displayed with the headscale nodes list-routes command. A subnet router with the hostname myrouter announced the IPv4 networks 10.0.0.0/8 and 192.168.0.0/24. Those need to be approved before they can be used.

$ headscale nodes list-routes\nID | Hostname | Approved | Available                  | Serving (Primary)\n1  | myrouter |          | 10.0.0.0/8, 192.168.0.0/24 |\n

Approve all desired routes of a subnet router by specifying them as comma separated list:

$ headscale nodes approve-routes --identifier 1 --routes 10.0.0.0/8,192.168.0.0/24\nNode updated\n

The node myrouter can now route the IPv4 networks 10.0.0.0/8 and 192.168.0.0/24 for the tailnet.

$ headscale nodes list-routes\nID | Hostname | Approved                   | Available                  | Serving (Primary)\n1  | myrouter | 10.0.0.0/8, 192.168.0.0/24 | 10.0.0.0/8, 192.168.0.0/24 | 10.0.0.0/8, 192.168.0.0/24\n
"},{"location":"ref/routes/#use-the-subnet-router","title":"Use the subnet router","text":"

To accept routes advertised by a subnet router on a node:

$ sudo tailscale set --accept-routes\n

Please refer to the official Tailscale documentation for how to use a subnet router on different operating systems.

"},{"location":"ref/routes/#restrict-the-use-of-a-subnet-router-with-acl","title":"Restrict the use of a subnet router with ACL","text":"

The routes announced by subnet routers are available to the nodes in a tailnet. By default, without an ACL enabled, all nodes can accept and use such routes. Configure an ACL to explicitly manage who can use routes.

The ACL snippet below defines three hosts, a subnet router router, a regular node node and service.example.net as internal service that can be reached via a route on the subnet router router. It allows the node node to access service.example.net on port 80 and 443 which is reachable via the subnet router. Access to the subnet router itself is denied.

Access the routes of a subnet router without the subnet router itself
{\n  \"hosts\": {\n    // the router is not referenced but announces 192.168.0.0/24\"\n    \"router\": \"100.64.0.1/32\",\n    \"node\": \"100.64.0.2/32\",\n    \"service.example.net\": \"192.168.0.1/32\"\n  },\n  \"acls\": [\n    {\n      \"action\": \"accept\",\n      \"src\": [\"node\"],\n      \"dst\": [\"service.example.net:80,443\"]\n    }\n  ]\n}\n
"},{"location":"ref/routes/#automatically-approve-routes-of-a-subnet-router","title":"Automatically approve routes of a subnet router","text":"

The initial setup of a subnet router usually requires manual approval of their announced routes on the control server before they can be used by a node in a tailnet. Headscale supports the autoApprovers section of an ACL to automate the approval of routes served with a subnet router.

The ACL snippet below defines the tag tag:router owned by the user alice. This tag is used for routes in the autoApprovers section. The IPv4 route 192.168.0.0/24 is automatically approved once announced by a subnet router owned by the user alice and that also advertises the tag tag:router.

Subnet routers owned by alice and tagged with tag:router are automatically approved
{\n  \"tagOwners\": {\n    \"tag:router\": [\"alice@\"]\n  },\n  \"autoApprovers\": {\n    \"routes\": {\n      \"192.168.0.0/24\": [\"tag:router\"]\n    }\n  },\n  \"acls\": [\n    // more rules\n  ]\n}\n

Advertise the route 192.168.0.0/24 from a subnet router that also advertises the tag tag:router when joining the tailnet:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-tags tag:router --advertise-routes 192.168.0.0/24\n

Please see the official Tailscale documentation for more information on auto approvers.

"},{"location":"ref/routes/#exit-node","title":"Exit node","text":"

The setup of an exit node requires double opt-in, once from an exit node and once on the control server to allow its use within the tailnet. Optionally, use autoApprovers to automatically approve an exit node.

"},{"location":"ref/routes/#setup-an-exit-node","title":"Setup an exit node","text":""},{"location":"ref/routes/#configure-a-node-as-exit-node","title":"Configure a node as exit node","text":"

Register a node and make it advertise itself as an exit node:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-exit-node\n

If the node is already registered, it can advertise exit capabilities like this:

$ sudo tailscale set --advertise-exit-node\n

Finally, enable IP forwarding to route traffic.

"},{"location":"ref/routes/#enable-the-exit-node-on-the-control-server","title":"Enable the exit node on the control server","text":"

The routes of a tailnet can be displayed with the headscale nodes list-routes command. An exit node can be recognized by its announced routes: 0.0.0.0/0 for IPv4 and ::/0 for IPv6. The exit node with the hostname myexit is already available, but needs to be approved:

$ headscale nodes list-routes\nID | Hostname | Approved | Available       | Serving (Primary)\n1  | myexit   |          | 0.0.0.0/0, ::/0 |\n

For exit nodes, it is sufficient to approve either the IPv4 or IPv6 route. The other will be approved automatically.

$ headscale nodes approve-routes --identifier 1 --routes 0.0.0.0/0\nNode updated\n

The node myexit is now approved as exit node for the tailnet:

$ headscale nodes list-routes\nID | Hostname | Approved        | Available       | Serving (Primary)\n1  | myexit   | 0.0.0.0/0, ::/0 | 0.0.0.0/0, ::/0 | 0.0.0.0/0, ::/0\n
"},{"location":"ref/routes/#use-the-exit-node","title":"Use the exit node","text":"

The exit node can now be used on a node with:

$ sudo tailscale set --exit-node myexit\n

Please refer to the official Tailscale documentation for how to use an exit node on different operating systems.

"},{"location":"ref/routes/#restrict-the-use-of-an-exit-node-with-acl","title":"Restrict the use of an exit node with ACL","text":"

An exit node is offered to all nodes in a tailnet. By default, without an ACL enabled, all nodes in a tailnet can select and use an exit node. Configure autogroup:internet in an ACL rule to restrict who can use any of the available exit nodes.

Example use of autogroup:internet
{\n  \"acls\": [\n    {\n      \"action\": \"accept\",\n      \"src\": [\"...\"],\n      \"dst\": [\"autogroup:internet:*\"]\n    }\n  ]\n}\n
"},{"location":"ref/routes/#automatically-approve-an-exit-node-with-auto-approvers","title":"Automatically approve an exit node with auto approvers","text":"

The initial setup of an exit node usually requires manual approval on the control server before it can be used by a node in a tailnet. Headscale supports the autoApprovers section of an ACL to automate the approval of a new exit node as soon as it joins the tailnet.

The ACL snippet below defines the tag tag:exit owned by the user alice. This tag is used for exitNode in the autoApprovers section. A new exit node which is owned by the user alice and that also advertises the tag tag:exit is automatically approved:

Exit nodes owned by alice and tagged with tag:exit are automatically approved
{\n  \"tagOwners\": {\n    \"tag:exit\": [\"alice@\"]\n  },\n  \"autoApprovers\": {\n    \"exitNode\": [\"tag:exit\"]\n  },\n  \"acls\": [\n    // more rules\n  ]\n}\n

Advertise a node as exit node and also advertise the tag tag:exit when joining the tailnet:

$ sudo tailscale up --login-server <YOUR_HEADSCALE_URL> --advertise-tags tag:exit --advertise-exit-node\n

Please see the official Tailscale documentation for more information on auto approvers.

"},{"location":"ref/routes/#high-availability","title":"High availability","text":"

Headscale has limited support for high availability routing. Multiple subnet routers with overlapping routes or multiple exit nodes can be used to provide high availability for users. If one router node goes offline, another one can serve the same routes to clients. Please see the official Tailscale documentation on high availability for details.

Bug

In certain situations it might take up to 16 minutes for Headscale to detect a node as offline. A failover node might not be selected fast enough, if such a node is used as subnet router or exit node causing service interruptions for clients. See issue 2129 for more information.

"},{"location":"ref/routes/#troubleshooting","title":"Troubleshooting","text":""},{"location":"ref/routes/#enable-ip-forwarding","title":"Enable IP forwarding","text":"

A subnet router or exit node is routing traffic on behalf of other nodes and thus requires IP forwarding. Check the official Tailscale documentation for how to enable IP forwarding.

"},{"location":"ref/tls/","title":"Running the service via TLS (optional)","text":""},{"location":"ref/tls/#bring-your-own-certificate","title":"Bring your own certificate","text":"

Headscale can be configured to expose its web service via TLS. To configure the certificate and key file manually, set the tls_cert_path and tls_key_path configuration parameters. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_cert_path: \"\"\ntls_key_path: \"\"\n

The certificate should contain the full chain, else some clients, like the Tailscale Android client, will reject it.

"},{"location":"ref/tls/#lets-encrypt-acme","title":"Let's Encrypt / ACME","text":"

To get a certificate automatically via Let's Encrypt, set tls_letsencrypt_hostname to the desired certificate hostname. This name must resolve to the IP address(es) headscale is reachable on (i.e., it must correspond to the server_url configuration parameter). The certificate and Let's Encrypt account credentials will be stored in the directory configured in tls_letsencrypt_cache_dir. If the path is relative, it will be interpreted as relative to the directory the configuration file was read from.

config.yaml
tls_letsencrypt_hostname: \"\"\ntls_letsencrypt_listen: \":http\"\ntls_letsencrypt_cache_dir: \".cache\"\ntls_letsencrypt_challenge_type: HTTP-01\n
"},{"location":"ref/tls/#challenge-types","title":"Challenge types","text":"

Headscale only supports two values for tls_letsencrypt_challenge_type: HTTP-01 (default) and TLS-ALPN-01.

"},{"location":"ref/tls/#http-01","title":"HTTP-01","text":"

For HTTP-01, headscale must be reachable on port 80 for the Let's Encrypt automated validation, in addition to whatever port is configured in listen_addr. By default, headscale listens on port 80 on all local IPs for Let's Encrypt automated validation.

If you need to change the ip and/or port used by headscale for the Let's Encrypt validation process, set tls_letsencrypt_listen to the appropriate value. This can be handy if you are running headscale as a non-root user (or can't run setcap). Keep in mind, however, that Let's Encrypt will only connect to port 80 for the validation callback, so if you change tls_letsencrypt_listen you will also need to configure something else (e.g. a firewall rule) to forward the traffic from port 80 to the ip:port combination specified in tls_letsencrypt_listen.

"},{"location":"ref/tls/#tls-alpn-01","title":"TLS-ALPN-01","text":"

For TLS-ALPN-01, headscale listens on the ip:port combination defined in listen_addr. Let's Encrypt will only connect to port 443 for the validation callback, so if listen_addr is not set to port 443, something else (e.g. a firewall rule) will be required to forward the traffic from port 443 to the ip:port combination specified in listen_addr.

"},{"location":"ref/tls/#technical-description","title":"Technical description","text":"

Headscale uses autocert, a Golang library providing ACME protocol verification, to facilitate certificate renewals via Let's Encrypt. Certificates will be renewed automatically, and the following can be expected:

  • Certificates provided from Let's Encrypt have a validity of 3 months from date issued.
  • Renewals are only attempted by headscale when 30 days or less remains until certificate expiry.
  • Renewal attempts by autocert are triggered at a random interval of 30-60 minutes.
  • No log output is generated when renewals are skipped, or successful.
"},{"location":"ref/tls/#checking-certificate-expiry","title":"Checking certificate expiry","text":"

If you want to validate that certificate renewal completed successfully, this can be done either manually, or through external monitoring software. Two examples of doing this manually:

  1. Open the URL for your headscale server in your browser of choice, and manually inspecting the expiry date of the certificate you receive.
  2. Or, check remotely from CLI using openssl:
$ openssl s_client -servername [hostname] -connect [hostname]:443 | openssl x509 -noout -dates\n(...)\nnotBefore=Feb  8 09:48:26 2024 GMT\nnotAfter=May  8 09:48:25 2024 GMT\n
"},{"location":"ref/tls/#log-output-from-the-autocert-library","title":"Log output from the autocert library","text":"

As these log lines are from the autocert library, they are not strictly generated by headscale itself.

acme/autocert: missing server name\n

Likely caused by an incoming connection that does not specify a hostname, for example a curl request directly against the IP of the server, or an unexpected hostname.

acme/autocert: host \"[foo]\" not configured in HostWhitelist\n

Similarly to the above, this likely indicates an invalid incoming request for an incorrect hostname, commonly just the IP itself.

The source code for autocert can be found here

"},{"location":"ref/integration/reverse-proxy/","title":"Running headscale behind a reverse proxy","text":"

Community documentation

This page is not actively maintained by the headscale authors and is written by community members. It is not verified by headscale developers.

It might be outdated and it might miss necessary steps.

Running headscale behind a reverse proxy is useful when running multiple applications on the same server, and you want to reuse the same external IP and port - usually tcp/443 for HTTPS.

"},{"location":"ref/integration/reverse-proxy/#websockets","title":"WebSockets","text":"

The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients.

WebSockets support is also required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our config-example.yaml.

"},{"location":"ref/integration/reverse-proxy/#cloudflare","title":"Cloudflare","text":"

Running headscale behind a cloudflare proxy or cloudflare tunnel is not supported and will not work as Cloudflare does not support WebSocket POSTs as required by the Tailscale protocol. See this issue

"},{"location":"ref/integration/reverse-proxy/#tls","title":"TLS","text":"

Headscale can be configured not to use TLS, leaving it to the reverse proxy to handle. Add the following configuration values to your headscale config file.

config.yaml
server_url: https://<YOUR_SERVER_NAME> # This should be the FQDN at which headscale will be served\nlisten_addr: 0.0.0.0:8080\nmetrics_listen_addr: 0.0.0.0:9090\ntls_cert_path: \"\"\ntls_key_path: \"\"\n
"},{"location":"ref/integration/reverse-proxy/#nginx","title":"nginx","text":"

The following example configuration can be used in your nginx setup, substituting values as necessary. <IP:PORT> should be the IP address and port where headscale is running. In most cases, this will be http://localhost:8080.

nginx.conf
map $http_upgrade $connection_upgrade {\n    default      upgrade;\n    ''           close;\n}\n\nserver {\n    listen 80;\n    listen [::]:80;\n\n    listen 443      ssl http2;\n    listen [::]:443 ssl http2;\n\n    server_name <YOUR_SERVER_NAME>;\n\n    ssl_certificate <PATH_TO_CERT>;\n    ssl_certificate_key <PATH_CERT_KEY>;\n    ssl_protocols TLSv1.2 TLSv1.3;\n\n    location / {\n        proxy_pass http://<IP:PORT>;\n        proxy_http_version 1.1;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection $connection_upgrade;\n        proxy_set_header Host $server_name;\n        proxy_redirect http:// https://;\n        proxy_buffering off;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n        proxy_set_header X-Forwarded-Proto $scheme;\n        add_header Strict-Transport-Security \"max-age=15552000; includeSubDomains\" always;\n    }\n}\n
"},{"location":"ref/integration/reverse-proxy/#istioenvoy","title":"istio/envoy","text":"

If you using Istio ingressgateway or Envoy as reverse proxy, there are some tips for you. If not set, you may see some debug log in proxy as below:

Sending local reply with details upgrade_failed\n
"},{"location":"ref/integration/reverse-proxy/#envoy","title":"Envoy","text":"

You need to add a new upgrade_type named tailscale-control-protocol. see details

"},{"location":"ref/integration/reverse-proxy/#istio","title":"Istio","text":"

Same as envoy, we can use EnvoyFilter to add upgrade_type.

apiVersion: networking.istio.io/v1alpha3\nkind: EnvoyFilter\nmetadata:\n  name: headscale-behind-istio-ingress\n  namespace: istio-system\nspec:\n  configPatches:\n    - applyTo: NETWORK_FILTER\n      match:\n        listener:\n          filterChain:\n            filter:\n              name: envoy.filters.network.http_connection_manager\n      patch:\n        operation: MERGE\n        value:\n          typed_config:\n            \"@type\": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n            upgrade_configs:\n              - upgrade_type: tailscale-control-protocol\n
"},{"location":"ref/integration/reverse-proxy/#caddy","title":"Caddy","text":"

The following Caddyfile is all that is necessary to use Caddy as a reverse proxy for headscale, in combination with the config.yaml specifications above to disable headscale's built in TLS. Replace values as necessary - <YOUR_SERVER_NAME> should be the FQDN at which headscale will be served, and <IP:PORT> should be the IP address and port where headscale is running. In most cases, this will be localhost:8080.

Caddyfile
<YOUR_SERVER_NAME> {\n    reverse_proxy <IP:PORT>\n}\n

Caddy v2 will automatically provision a certificate for your domain/subdomain, force HTTPS, and proxy websockets - no further configuration is necessary.

For a slightly more complex configuration which utilizes Docker containers to manage Caddy, headscale, and Headscale-UI, Guru Computing's guide is an excellent reference.

"},{"location":"ref/integration/reverse-proxy/#apache","title":"Apache","text":"

The following minimal Apache config will proxy traffic to the headscale instance on <IP:PORT>. Note that upgrade=any is required as a parameter for ProxyPass so that WebSockets traffic whose Upgrade header value is not equal to WebSocket (i. e. Tailscale Control Protocol) is forwarded correctly. See the Apache docs for more information on this.

apache.conf
<VirtualHost *:443>\n    ServerName <YOUR_SERVER_NAME>\n\n    ProxyPreserveHost On\n    ProxyPass / http://<IP:PORT>/ upgrade=any\n\n    SSLEngine On\n    SSLCertificateFile <PATH_TO_CERT>\n    SSLCertificateKeyFile <PATH_CERT_KEY>\n</VirtualHost>\n
"},{"location":"ref/integration/tools/","title":"Tools related to headscale","text":"

Community contributions

This page contains community contributions. The projects listed here are not maintained by the headscale authors and are written by community members.

This page collects third-party tools, client libraries, and scripts related to headscale.

Name Repository Link Description tailscale-manager Github Dynamically manage Tailscale route advertisements headscalebacktosqlite Github Migrate headscale from PostgreSQL back to SQLite headscale-pf Github Populates user groups based on user groups in Jumpcloud or Authentik headscale-client-go Github A Go client implementation for the Headscale HTTP API."},{"location":"ref/integration/web-ui/","title":"Web interfaces for headscale","text":"

Community contributions

This page contains community contributions. The projects listed here are not maintained by the headscale authors and are written by community members.

Headscale doesn't provide a built-in web interface but users may pick one from the available options.

Name Repository Link Description headscale-ui Github A web frontend for the headscale Tailscale-compatible coordination server HeadscaleUi GitHub A static headscale admin ui, no backend environment required Headplane GitHub An advanced Tailscale inspired frontend for headscale headscale-admin Github Headscale-Admin is meant to be a simple, modern web interface for headscale ouroboros Github Ouroboros is designed for users to manage their own devices, rather than for admins unraid-headscale-admin Github A simple headscale admin UI for Unraid, it offers Local (docker exec) and API Mode headscale-console Github WebAssembly-based client supporting SSH, VNC and RDP with optional self-service capabilities

You can ask for support on our Discord server in the \"web-interfaces\" channel.

"},{"location":"setup/requirements/","title":"Requirements","text":"

Headscale should just work as long as the following requirements are met:

  • A server with a public IP address for headscale. A dual-stack setup with a public IPv4 and a public IPv6 address is recommended.
  • Headscale is served via HTTPS on port 4431.
  • A reasonably modern Linux or BSD based operating system.
  • A dedicated local user account to run headscale.
  • A little bit of command line knowledge to configure and operate headscale.
"},{"location":"setup/requirements/#assumptions","title":"Assumptions","text":"

The headscale documentation and the provided examples are written with a few assumptions in mind:

  • Headscale is running as system service via a dedicated local user headscale.
  • The configuration is loaded from /etc/headscale/config.yaml.
  • SQLite is used as database.
  • The data directory for headscale (used for private keys, ACLs, SQLite database, \u2026) is located in /var/lib/headscale.
  • URLs and values that need to be replaced by the user are either denoted as <VALUE_TO_CHANGE> or use placeholder values such as headscale.example.com.

Please adjust to your local environment accordingly.

  1. The Tailscale client assumes HTTPS on port 443 in certain situations. Serving headscale either via HTTP or via HTTPS on a port other than 443 is possible but sticking with HTTPS on port 443 is strongly recommended for production setups. See issue 2164 for more information.\u00a0\u21a9

"},{"location":"setup/upgrade/","title":"Upgrade an existing installation","text":"

Update an existing headscale installation to a new version:

  • Read the announcement on the GitHub releases page for the new version. It lists the changes of the release along with possible breaking changes.
  • Create a backup of your database.
  • Update headscale to the new version, preferably by following the same installation method.
  • Compare and update the configuration file.
  • Restart headscale.
"},{"location":"setup/install/community/","title":"Community packages","text":"

Several Linux distributions and community members provide packages for headscale. Those packages may be used instead of the official releases provided by the headscale maintainers. Such packages offer improved integration for their targeted operating system and usually:

  • setup a dedicated local user account to run headscale
  • provide a default configuration
  • install headscale as system service

Community packages might be outdated

The packages mentioned on this page might be outdated or unmaintained. Use the official releases to get the current stable version or to test pre-releases.

"},{"location":"setup/install/community/#arch-linux","title":"Arch Linux","text":"

Arch Linux offers a package for headscale, install via:

pacman -S headscale\n

The AUR package headscale-git can be used to build the current development version.

"},{"location":"setup/install/community/#fedora-rhel-centos","title":"Fedora, RHEL, CentOS","text":"

A third-party repository for various RPM based distributions is available at: https://copr.fedorainfracloud.org/coprs/jonathanspw/headscale/. The site provides detailed setup and installation instructions.

"},{"location":"setup/install/community/#nix-nixos","title":"Nix, NixOS","text":"

A Nix package is available as: headscale. See the NixOS package site for installation details.

"},{"location":"setup/install/community/#gentoo","title":"Gentoo","text":"
emerge --ask net-vpn/headscale\n

Gentoo specific documentation is available here.

"},{"location":"setup/install/community/#openbsd","title":"OpenBSD","text":"

Headscale is available in ports. The port installs headscale as system service with rc.d and provides usage instructions upon installation.

pkg_add headscale\n
"},{"location":"setup/install/container/","title":"Running headscale in a container","text":"

Community documentation

This page is not actively maintained by the headscale authors and is written by community members. It is not verified by headscale developers.

It might be outdated and it might miss necessary steps.

This documentation has the goal of showing a user how-to set up and run headscale in a container. A container runtime such as Docker or Podman is required. The container image can be found on Docker Hub and GitHub Container Registry. The container image URLs are:

  • Docker Hub: docker.io/headscale/headscale:<VERSION>
  • GitHub Container Registry: ghcr.io/juanfont/headscale:<VERSION>
"},{"location":"setup/install/container/#configure-and-run-headscale","title":"Configure and run headscale","text":"
  1. Create a directory on the Docker host to store headscale's configuration and the SQLite database:

    mkdir -p ./headscale/{config,lib,run}\ncd ./headscale\n
  2. Download the example configuration for your chosen version and save it as: $(pwd)/config/config.yaml. Adjust the configuration to suit your local environment. See Configuration for details.

  3. Start headscale from within the previously created ./headscale directory:

    docker run \\\n  --name headscale \\\n  --detach \\\n  --volume \"$(pwd)/config:/etc/headscale\" \\\n  --volume \"$(pwd)/lib:/var/lib/headscale\" \\\n  --volume \"$(pwd)/run:/var/run/headscale\" \\\n  --publish 127.0.0.1:8080:8080 \\\n  --publish 127.0.0.1:9090:9090 \\\n  docker.io/headscale/headscale:<VERSION> \\\n  serve\n

    Note: use 0.0.0.0:8080:8080 instead of 127.0.0.1:8080:8080 if you want to expose the container externally.

    This command mounts the local directories inside the container, forwards port 8080 and 9090 out of the container so the headscale instance becomes available and then detaches so headscale runs in the background.

    A similar configuration for docker-compose:

    docker-compose.yaml
    services:\n  headscale:\n    image: docker.io/headscale/headscale:<VERSION>\n    restart: unless-stopped\n    container_name: headscale\n    ports:\n      - \"127.0.0.1:8080:8080\"\n      - \"127.0.0.1:9090:9090\"\n    volumes:\n      # Please set <HEADSCALE_PATH> to the absolute path\n      # of the previously created headscale directory.\n      - <HEADSCALE_PATH>/config:/etc/headscale\n      - <HEADSCALE_PATH>/lib:/var/lib/headscale\n      - <HEADSCALE_PATH>/run:/var/run/headscale\n    command: serve\n
  4. Verify headscale is running:

    Follow the container logs:

    docker logs --follow headscale\n

    Verify running containers:

    docker ps\n

    Verify headscale is available:

    curl http://127.0.0.1:9090/metrics\n
  5. Create a headscale user:

    docker exec -it headscale \\\n  headscale users create myfirstuser\n
"},{"location":"setup/install/container/#register-a-machine-normal-login","title":"Register a machine (normal login)","text":"

On a client machine, execute the tailscale up command to login:

tailscale up --login-server YOUR_HEADSCALE_URL\n

To register a machine when running headscale in a container, take the headscale command and pass it to the container:

docker exec -it headscale \\\n  headscale nodes register --user myfirstuser --key <YOUR_MACHINE_KEY>\n
"},{"location":"setup/install/container/#register-a-machine-using-a-pre-authenticated-key","title":"Register a machine using a pre authenticated key","text":"

Generate a key using the command line:

docker exec -it headscale \\\n  headscale preauthkeys create --user myfirstuser --reusable --expiration 24h\n

This will return a pre-authenticated key that can be used to connect a node to headscale with the tailscale up command:

tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>\n
"},{"location":"setup/install/container/#debugging-headscale-running-in-docker","title":"Debugging headscale running in Docker","text":"

The Headscale container image is based on a \"distroless\" image that does not contain a shell or any other debug tools. If you need to debug headscale running in the Docker container, you can use the -debug variant, for example docker.io/headscale/headscale:x.x.x-debug.

"},{"location":"setup/install/container/#running-the-debug-docker-container","title":"Running the debug Docker container","text":"

To run the debug Docker container, use the exact same commands as above, but replace docker.io/headscale/headscale:x.x.x with docker.io/headscale/headscale:x.x.x-debug (x.x.x is the version of headscale). The two containers are compatible with each other, so you can alternate between them.

"},{"location":"setup/install/container/#executing-commands-in-the-debug-container","title":"Executing commands in the debug container","text":"

The default command in the debug container is to run headscale, which is located at /ko-app/headscale inside the container.

Additionally, the debug container includes a minimalist Busybox shell.

To launch a shell in the container, use:

docker run -it docker.io/headscale/headscale:x.x.x-debug sh\n

You can also execute commands directly, such as ls /ko-app in this example:

docker run docker.io/headscale/headscale:x.x.x-debug ls /ko-app\n

Using docker exec -it allows you to run commands in an existing container.

"},{"location":"setup/install/official/","title":"Official releases","text":"

Official releases for headscale are available as binaries for various platforms and DEB packages for Debian and Ubuntu. Both are available on the GitHub releases page.

"},{"location":"setup/install/official/#using-packages-for-debianubuntu-recommended","title":"Using packages for Debian/Ubuntu (recommended)","text":"

It is recommended to use our DEB packages to install headscale on a Debian based system as those packages configure a local user to run headscale, provide a default configuration and ship with a systemd service file. Supported distributions are Ubuntu 22.04 or newer, Debian 11 or newer.

  1. Download the latest headscale package for your platform (.deb for Ubuntu and Debian).

    HEADSCALE_VERSION=\"\" # See above URL for latest version, e.g. \"X.Y.Z\" (NOTE: do not add the \"v\" prefix!)\nHEADSCALE_ARCH=\"\" # Your system architecture, e.g. \"amd64\"\nwget --output-document=headscale.deb \\\n \"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb\"\n
  2. Install headscale:

    sudo apt install ./headscale.deb\n
  3. Configure headscale by editing the configuration file:

    sudo nano /etc/headscale/config.yaml\n
  4. Enable and start the headscale service:

    sudo systemctl enable --now headscale\n
  5. Verify that headscale is running as intended:

    sudo systemctl status headscale\n
"},{"location":"setup/install/official/#using-standalone-binaries-advanced","title":"Using standalone binaries (advanced)","text":"

Advanced

This installation method is considered advanced as one needs to take care of the local user and the systemd service themselves. If possible, use the DEB packages or a community package instead.

This section describes the installation of headscale according to the Requirements and assumptions. Headscale is run by a dedicated local user and the service itself is managed by systemd.

  1. Download the latest headscale binary from GitHub's release page:

    sudo wget --output-document=/usr/local/bin/headscale \\\nhttps://github.com/juanfont/headscale/releases/download/v<HEADSCALE VERSION>/headscale_<HEADSCALE VERSION>_linux_<ARCH>\n
  2. Make headscale executable:

    sudo chmod +x /usr/local/bin/headscale\n
  3. Add a dedicated local user to run headscale:

    sudo useradd \\\n --create-home \\\n --home-dir /var/lib/headscale/ \\\n --system \\\n --user-group \\\n --shell /usr/sbin/nologin \\\n headscale\n
  4. Download the example configuration for your chosen version and save it as: /etc/headscale/config.yaml. Adjust the configuration to suit your local environment. See Configuration for details.

    sudo mkdir -p /etc/headscale\nsudo nano /etc/headscale/config.yaml\n
  5. Copy headscale's systemd service file to /etc/systemd/system/headscale.service and adjust it to suit your local setup. The following parameters likely need to be modified: ExecStart, WorkingDirectory, ReadWritePaths.

  6. In /etc/headscale/config.yaml, override the default headscale unix socket with a path that is writable by the headscale user or group:

    config.yaml
    unix_socket: /var/run/headscale/headscale.sock\n
  7. Reload systemd to load the new configuration file:

    systemctl daemon-reload\n
  8. Enable and start the new headscale service:

    systemctl enable --now headscale\n
  9. Verify that headscale is running as intended:

    systemctl status headscale\n
"},{"location":"setup/install/source/","title":"Build from source","text":"

Community documentation

This page is not actively maintained by the headscale authors and is written by community members. It is not verified by headscale developers.

It might be outdated and it might miss necessary steps.

Headscale can be built from source using the latest version of Go and Buf (Protobuf generator). See the Contributing section in the GitHub README for more information.

"},{"location":"setup/install/source/#openbsd","title":"OpenBSD","text":""},{"location":"setup/install/source/#install-from-source","title":"Install from source","text":"
# Install prerequisites\npkg_add go git\n\ngit clone https://github.com/juanfont/headscale.git\n\ncd headscale\n\n# optionally checkout a release\n# option a. you can find official release at https://github.com/juanfont/headscale/releases/latest\n# option b. get latest tag, this may be a beta release\nlatestTag=$(git describe --tags `git rev-list --tags --max-count=1`)\n\ngit checkout $latestTag\n\ngo build -ldflags=\"-s -w -X github.com/juanfont/headscale/hscontrol/types.Version=$latestTag\" -X github.com/juanfont/headscale/hscontrol/types.GitCommitHash=HASH\" github.com/juanfont/headscale\n\n# make it executable\nchmod a+x headscale\n\n# copy it to /usr/local/sbin\ncp headscale /usr/local/sbin\n
"},{"location":"setup/install/source/#install-from-source-via-cross-compile","title":"Install from source via cross compile","text":"
# Install prerequisites\n# 1. go v1.20+: headscale newer than 0.21 needs go 1.20+ to compile\n# 2. gmake: Makefile in the headscale repo is written in GNU make syntax\n\ngit clone https://github.com/juanfont/headscale.git\n\ncd headscale\n\n# optionally checkout a release\n# option a. you can find official release at https://github.com/juanfont/headscale/releases/latest\n# option b. get latest tag, this may be a beta release\nlatestTag=$(git describe --tags `git rev-list --tags --max-count=1`)\n\ngit checkout $latestTag\n\nmake build GOOS=openbsd\n\n# copy headscale to openbsd machine and put it in /usr/local/sbin\n
"},{"location":"usage/getting-started/","title":"Getting started","text":"

This page helps you get started with headscale and provides a few usage examples for the headscale command line tool headscale.

Prerequisites

  • Headscale is installed and running as system service. Read the setup section for installation instructions.
  • The configuration file exists and is adjusted to suit your environment, see Configuration for details.
  • Headscale is reachable from the Internet. Verify this by opening client specific setup instructions in your browser, e.g. https://headscale.example.com/windows
  • The Tailscale client is installed, see Client and operating system support for more information.
"},{"location":"usage/getting-started/#getting-help","title":"Getting help","text":"

The headscale command line tool provides built-in help. To show available commands along with their arguments and options, run:

NativeContainer
# Show help\nheadscale help\n\n# Show help for a specific command\nheadscale <COMMAND> --help\n
# Show help\ndocker exec -it headscale \\\n  headscale help\n\n# Show help for a specific command\ndocker exec -it headscale \\\n  headscale <COMMAND> --help\n
"},{"location":"usage/getting-started/#manage-headscale-users","title":"Manage headscale users","text":"

In headscale, a node (also known as machine or device) is always assigned to a headscale user. Such a headscale user may have many nodes assigned to them and can be managed with the headscale users command. Invoke the built-in help for more information: headscale users --help.

"},{"location":"usage/getting-started/#create-a-headscale-user","title":"Create a headscale user","text":"NativeContainer
headscale users create <USER>\n
docker exec -it headscale \\\n  headscale users create <USER>\n
"},{"location":"usage/getting-started/#list-existing-headscale-users","title":"List existing headscale users","text":"NativeContainer
headscale users list\n
docker exec -it headscale \\\n  headscale users list\n
"},{"location":"usage/getting-started/#register-a-node","title":"Register a node","text":"

One has to register a node first to use headscale as coordination with Tailscale. The following examples work for the Tailscale client on Linux/BSD operating systems. Alternatively, follow the instructions to connect Android, Apple or Windows devices.

"},{"location":"usage/getting-started/#normal-interactive-login","title":"Normal, interactive login","text":"

On a client machine, run the tailscale up command and provide the FQDN of your headscale instance as argument:

tailscale up --login-server <YOUR_HEADSCALE_URL>\n

Usually, a browser window with further instructions is opened and contains the value for <YOUR_MACHINE_KEY>. Approve and register the node on your headscale server:

NativeContainer
headscale nodes register --user <USER> --key <YOUR_MACHINE_KEY>\n
docker exec -it headscale \\\n  headscale nodes register --user <USER> --key <YOUR_MACHINE_KEY>\n
"},{"location":"usage/getting-started/#using-a-preauthkey","title":"Using a preauthkey","text":"

It is also possible to generate a preauthkey and register a node non-interactively. First, generate a preauthkey on the headscale instance. By default, the key is valid for one hour and can only be used once (see headscale preauthkeys --help for other options):

NativeContainer
headscale preauthkeys create --user <USER>\n
docker exec -it headscale \\\n  headscale preauthkeys create --user <USER>\n

The command returns the preauthkey on success which is used to connect a node to the headscale instance via the tailscale up command:

tailscale up --login-server <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>\n
"},{"location":"usage/connect/android/","title":"Connecting an Android client","text":"

This documentation has the goal of showing how a user can use the official Android Tailscale client with headscale.

"},{"location":"usage/connect/android/#installation","title":"Installation","text":"

Install the official Tailscale Android client from the Google Play Store or F-Droid.

"},{"location":"usage/connect/android/#configuring-the-headscale-url","title":"Configuring the headscale URL","text":"
  • Open the app and select the settings menu in the upper-right corner
  • Tap on Accounts
  • In the kebab menu icon (three dots) in the upper-right corner select Use an alternate server
  • Enter your server URL (e.g https://headscale.example.com) and follow the instructions
"},{"location":"usage/connect/apple/","title":"Connecting an Apple client","text":"

This documentation has the goal of showing how a user can use the official iOS and macOS Tailscale clients with headscale.

Instructions on your headscale instance

An endpoint with information on how to connect your Apple device is also available at /apple on your running instance.

"},{"location":"usage/connect/apple/#ios","title":"iOS","text":""},{"location":"usage/connect/apple/#installation","title":"Installation","text":"

Install the official Tailscale iOS client from the App Store.

"},{"location":"usage/connect/apple/#configuring-the-headscale-url","title":"Configuring the headscale URL","text":"
  • Open the Tailscale app
  • Click the account icon in the top-right corner and select Log in\u2026.
  • Tap the top-right options menu button and select Use custom coordination server.
  • Enter your instance url (e.g https://headscale.example.com)
  • Enter your credentials and log in. Headscale should now be working on your iOS device.
"},{"location":"usage/connect/apple/#macos","title":"macOS","text":""},{"location":"usage/connect/apple/#installation_1","title":"Installation","text":"

Choose one of the available Tailscale clients for macOS and install it.

"},{"location":"usage/connect/apple/#configuring-the-headscale-url_1","title":"Configuring the headscale URL","text":""},{"location":"usage/connect/apple/#command-line","title":"Command line","text":"

Use Tailscale's login command to connect with your headscale instance (e.g https://headscale.example.com):

tailscale login --login-server <YOUR_HEADSCALE_URL>\n
"},{"location":"usage/connect/apple/#gui","title":"GUI","text":"
  • Option + Click the Tailscale icon in the menu and hover over the Debug menu
  • Under Custom Login Server, select Add Account...
  • Enter the URL of your headscale instance (e.g https://headscale.example.com) and press Add Account
  • Follow the login procedure in the browser
"},{"location":"usage/connect/apple/#tvos","title":"tvOS","text":""},{"location":"usage/connect/apple/#installation_2","title":"Installation","text":"

Install the official Tailscale tvOS client from the App Store.

Danger

Don't open the Tailscale App after installation!

"},{"location":"usage/connect/apple/#configuring-the-headscale-url_2","title":"Configuring the headscale URL","text":"
  • Open Settings (the Apple tvOS settings) > Apps > Tailscale
  • Under ALTERNATE COORDINATION SERVER URL, select URL
  • Enter the URL of your headscale instance (e.g https://headscale.example.com) and press OK
  • Return to the tvOS Home screen
  • Open Tailscale
  • Click the button Install VPN configuration and confirm the appearing popup by clicking the Allow button
  • Scan the QR code and follow the login procedure
"},{"location":"usage/connect/windows/","title":"Connecting a Windows client","text":"

This documentation has the goal of showing how a user can use the official Windows Tailscale client with headscale.

Instructions on your headscale instance

An endpoint with information on how to connect your Windows device is also available at /windows on your running instance.

"},{"location":"usage/connect/windows/#installation","title":"Installation","text":"

Download the Official Windows Client and install it.

"},{"location":"usage/connect/windows/#configuring-the-headscale-url","title":"Configuring the headscale URL","text":"

Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g https://headscale.example.com):

tailscale login --login-server <YOUR_HEADSCALE_URL>\n

Follow the instructions in the opened browser window to finish the configuration.

"},{"location":"usage/connect/windows/#troubleshooting","title":"Troubleshooting","text":""},{"location":"usage/connect/windows/#unattended-mode","title":"Unattended mode","text":"

By default, Tailscale's Windows client is only running when the user is logged in. If you want to keep Tailscale running all the time, please enable \"Unattended mode\":

  • Click on the Tailscale tray icon and select Preferences
  • Enable Run unattended
  • Confirm the \"Unattended mode\" message

See also Keep Tailscale running when I'm not logged in to my computer

"},{"location":"usage/connect/windows/#failing-node-registration","title":"Failing node registration","text":"

If you are seeing repeated messages like:

[GIN] 2022/02/10 - 16:39:34 | 200 |    1.105306ms |       127.0.0.1 | POST     \"/machine/redacted\"\n

in your headscale output, turn on DEBUG logging and look for:

2022-02-11T00:59:29Z DBG Machine registration has expired. Sending a authurl to register machine=redacted\n

This typically means that the registry keys above was not set appropriately.

To reset and try again, it is important to do the following:

  1. Shut down the Tailscale service (or the client running in the tray)
  2. Delete Tailscale Application data folder, located at C:\\Users\\<USERNAME>\\AppData\\Local\\Tailscale and try to connect again.
  3. Ensure the Windows node is deleted from headscale (to ensure fresh setup)
  4. Start Tailscale on the Windows machine and retry the login.
"}]} \ No newline at end of file diff --git a/development/sitemap.xml b/development/sitemap.xml index da5152c5..8651cedc 100644 --- a/development/sitemap.xml +++ b/development/sitemap.xml @@ -2,114 +2,114 @@ https://juanfont.github.io/headscale/development/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/clients/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/contributing/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/faq/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/features/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/help/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/releases/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/about/sponsor/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/acls/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/configuration/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/dns/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/oidc/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/remote-cli/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/routes/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/tls/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/integration/reverse-proxy/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/integration/tools/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/ref/integration/web-ui/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/setup/requirements/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/setup/upgrade/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/setup/install/community/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/setup/install/container/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/setup/install/official/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/setup/install/source/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/usage/getting-started/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/usage/connect/android/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/usage/connect/apple/ - 2025-07-04 + 2025-07-13 https://juanfont.github.io/headscale/development/usage/connect/windows/ - 2025-07-04 + 2025-07-13 \ No newline at end of file diff --git a/development/sitemap.xml.gz b/development/sitemap.xml.gz index 2ffbdc3fea0279ba64a1cc753c189d6bd8098a8b..22e223cefe5a18cf668a89e1c912f7595ab6e2dd 100644 GIT binary patch delta 413 zcmV;O0b>5H1Fi!HABzYG0QYi{2OWQt6hR-DadtgHdjO3@+YDq;5ve%2edx@f=)PT0 z8(RWReIWTu^y%qsO@~*gjFiva{c!9KkOM``^SS%|>u3M1dm8UfttJLJsBz|Vw}_M< z&TZQ^Fbb@`@IZBT92T9hyDhro@Vy)F4mx%R*SF7cN)*Q33#*&w=JP~Zh8cflX_o=X zE#M;upTI?Ug+yhAEbip?&;It}SyqbU!|~yiCTZs-~3`S5YtR&FuMPYvhNOc8P5+!B?g;`*}$*IEL20>pcy}cXc6C#sw+u+hS z3w5pyYAcHO(opqhq(i$xwW86AV1@@Xi%&@}lyz+x<=r4z7LOUKIWn3i4Cs?NFs4?6 zg&J$@XK*vf{zK9$Tos6BxU{k|1no6*tlBcz<0>VAIjRkFr0pM0)Xxov7VP)|%u9QY Ha}WRk+Css= delta 413 zcmV;O0b>5H1Fi!HABzYGfEj0z2OWQI(4r5_IJ=&pJ%C1{Z5Fbqh*X^1zI0|#bl)zh zjV*zuK9Kw+`uy~|rPB*kM#`7&!!ULy$bll}<6NTyU!s_N%^LeH$!-9XZw(Ef8 zR`8L7Pv9cFK%%li7I${XXMcO~EE~n?aeVyLkDvSTqng~;Zri#)`~DXKf2AfaB-9Dp z;Zf6y*Cv`YGf=aCw!;EmS_L~9TOk!QFclJbHWVE$l$ojlo(i+`A+Zq@75a=zt6uc9 zHo_FMmFN)0U<9SYMgqNF6jpzLR99dnQDR0=m<5)*oGKh`5cH+e>zhG7Au<`a9j<+| zQ0Lm9wxW2?4OM?eI}ZQ*|wHZ-VBmu@tC2SBcpl7fIgW6V`?>6 zsIkU=0k?o0KP0`vRe@-RODiiw&|Wjgsx5;(u2K@1quMb?+W+B1eSZyT!A`#cfyO^v Ha}WRk1ir|Z