docs: document joining nodes with an OAuth client

Prefix swap, baseURL, every attribute; examples for tailscale up,
container, tsnet, GitHub Action.
This commit is contained in:
Kristoffer Dalby
2026-06-27 10:13:05 +00:00
parent c3e48f039c
commit eebeab3c1e
4 changed files with 115 additions and 1 deletions
+9
View File
@@ -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=<headscale>`) 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
+92
View File
@@ -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=<your Headscale URL>`.
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-<id>-<secret>?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-<id>-<secret>?baseURL=https://headscale.example.com&ephemeral=false'
```
Container image:
```shell
docker run -d --name tailscale \
-e TS_AUTHKEY='tskey-client-<id>-<secret>?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-<id>-<secret>?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.
+2 -1
View File
@@ -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 <YOUR_HEADSCALE_URL> --authkey <YOUR_AUTH_KEY>
+12
View File
@@ -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