From eebeab3c1ea496360209fae016458b35b7d80020 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 27 Jun 2026 10:13:05 +0000 Subject: [PATCH] docs: document joining nodes with an OAuth client Prefix swap, baseURL, every attribute; examples for tailscale up, container, tsnet, GitHub Action. --- CHANGELOG.md | 9 ++++ docs/ref/api.md | 92 ++++++++++++++++++++++++++++++++++++++ docs/ref/registration.md | 3 +- hscontrol/api/v2/README.md | 12 +++++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0285007c..8ae74290 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,15 @@ keys remain all-access. [#3334](https://github.com/juanfont/headscale/pull/3334) +An OAuth client secret also joins nodes: `tailscale up`, the container image, +`tsnet` and the Tailscale GitHub Action take it as an auth key +(`tskey-client-…?baseURL=`) and mint a tagged key per node. Pre-auth +key registrations now accept `--advertise-tags` that are a subset of the key's +tags; any other tag is rejected, for new and re-registering nodes alike. See +[the API docs](https://headscale.net/stable/ref/api/). + +[#3351](https://github.com/juanfont/headscale/pull/3351) + ### BREAKING #### API diff --git a/docs/ref/api.md b/docs/ref/api.md index 8f492dfd..f1cd4c44 100644 --- a/docs/ref/api.md +++ b/docs/ref/api.md @@ -58,6 +58,98 @@ Headscale server at `/api/v1/docs` for details. https://headscale.example.com/api/v1/auth/register ``` +## Join nodes with an OAuth client + +Headscale also serves a subset of the Tailscale-compatible API at `/api/v2`, which +accepts **OAuth 2.0 client-credentials** in addition to API keys. The Tailscale +client can use an OAuth client secret in place of an auth key: it exchanges the +secret for an access token, mints a single-use tagged auth key and registers with +it. This works anywhere the client takes an auth key: `tailscale up`, the +container image, `tsnet` and the +[`tailscale/github-action`](https://github.com/tailscale/github-action). One +long-lived secret joins any number of nodes. + +Create an OAuth client with the `auth_keys` scope and the tags its nodes get. The +secret is shown once: + +```shell +headscale oauth-clients create --scope auth_keys --tag tag:ci +``` + +Build the auth key from the secret: + +1. Swap the prefix: `hskey-client-…` becomes `tskey-client-…`. The client only + runs the exchange for `tskey-client-`; Headscale accepts both. +1. Append `?baseURL=`. +1. Set `--advertise-tags`. Each tag must exist in the policy's `tagOwners` and be + one of the OAuth client's tags, or owned by one of them. + +```text +tskey-client--?baseURL=https://headscale.example.com +``` + +!!! warning + + Without `baseURL` the client sends the secret to `https://api.tailscale.com`. + +### Attributes + +These are all the attributes the client understands; any other is an error. +Order does not matter and an empty value means the default. + +| Attribute | Default | Effect | +| --------------- | --------------------------- | ------------------------------------------------------------------------------------------------ | +| `baseURL` | `https://api.tailscale.com` | Where the exchange and key creation go. Your Headscale URL, no trailing slash. | +| `ephemeral` | `true` | Node is removed after it goes offline (`node.ephemeral.inactivity_timeout`). `false` to keep it. | +| `preauthorized` | `false` | Accepted, no effect: Headscale always authorizes pre-auth-key nodes. | + +Booleans take any Go `strconv.ParseBool` value (`true`, `false`, `1`, `0`, …). + +### Examples + +`tailscale up`, either as the auth key or via `--client-secret` (which also takes +`file:/path/to/secret`): + +```shell +tailscale up --login-server https://headscale.example.com --advertise-tags tag:ci \ + --auth-key 'tskey-client--?baseURL=https://headscale.example.com&ephemeral=false' +``` + +Container image: + +```shell +docker run -d --name tailscale \ + -e TS_AUTHKEY='tskey-client--?baseURL=https://headscale.example.com' \ + -e TS_EXTRA_ARGS='--login-server=https://headscale.example.com --advertise-tags=tag:ci' \ + tailscale/tailscale +``` + +`tsnet` (`TS_CLIENT_SECRET` works too); import `tailscale.com/feature/oauthkey`: + +```go +srv := &tsnet.Server{ + ControlURL: "https://headscale.example.com", + AuthKey: "tskey-client--?baseURL=https://headscale.example.com", + AdvertiseTags: []string{"tag:ci"}, +} +``` + +GitHub Action, with the whole string stored as a repository secret: + +{% raw %} + +```yaml +- uses: tailscale/github-action@v4 + with: + authkey: ${{ secrets.HEADSCALE_AUTHKEY }} + args: --login-server=https://headscale.example.com --advertise-tags=tag:ci +``` + +{% endraw %} + +Use `authkey` even though upstream marks it deprecated: `oauth-secret` appends its +own `?…` to the secret, which corrupts `baseURL`. + ## Remote control The `headscale` binary can control a Headscale instance from a remote machine over the HTTP API. diff --git a/docs/ref/registration.md b/docs/ref/registration.md index cc8fd0e7..81f4c06b 100644 --- a/docs/ref/registration.md +++ b/docs/ref/registration.md @@ -131,7 +131,8 @@ Its best suited for automation. The above prints a pre authenticated key with the default settings (can be used once and is valid for one hour). Use this auth key to register a node non-interactively. You don't need to provide the `--advertise-tags` parameter as - the tags are automatically read from the pre authenticated key: + the tags are automatically read from the pre authenticated key. Advertising a subset of the key's tags is accepted; + any other tag is rejected: ```console tailscale up --login-server --authkey diff --git a/hscontrol/api/v2/README.md b/hscontrol/api/v2/README.md index 90fad363..c6fc68e8 100644 --- a/hscontrol/api/v2/README.md +++ b/hscontrol/api/v2/README.md @@ -75,6 +75,18 @@ operator is OAuth-only. Supporting OAuth lets all of them drive Headscale. **Argon2id** hash of the secret (no JWT, no signing keys). `OAuthClient` and `OAuthAccessToken` live in `types/oauth.go` and `db/oauth.go`. +## OAuth with the tailscale client and GitHub Action + +The stock `tailscale` client (`feature/oauthkey`) accepts an OAuth client secret +as an auth key: it exchanges it at `/api/v2/oauth/token`, mints a tagged key via +`CreateKey`, then registers re-advertising those tags. Server-side this relies on +two things: `db.AuthenticateOAuthClient` accepting the `tskey-client-` prefix +alias (the client only runs the exchange for it), and pre-auth key registration +tolerating `RequestTags` that are a subset of the key's tags. +`TestAPIv2OAuthTailscaleClientAuthKey` in `servertest/` drives the real client +code through this; `.github/workflows/tailscale-action-integration.yaml` covers +the GitHub Action. User-facing setup is in `docs/ref/api.md`. + ## Adding an endpoint Worked example: the keys resource (`keys.go`) = Tailscale auth keys = Headscale