A small utility for testing the OAuth authorization code flow to programmatically provision Tailscale nodes on a tailnet using tsnet.
Hydralisk runs a local web server that walks through the full OAuth consent flow, exchanges the authorization code for an access token, and uses it as an auth key to spin up a tsnet node that joins the tailnet.
Your tailnet must have the following feature flags enabled:
stateless-access-tokenoauth-authorization-flow
Contact your Tailscale account team or enable them via the admin console if available.
OAuth apps can currently only be created via the API. You must be a tailnet admin.
curl -X POST https://login.tailscale.com/api/v2/tailnet/-/oauth-apps \
--header 'Authorization: Bearer <api-access-token>' \
-H "Content-Type: application/json" \
-d '{
"name": "your-oauth-app-name",
"redirectUris": ["http://localhost:31545/callback"],
"scopes": ["auth_keys:create", "auth_keys:create:once"]
}'The response contains the clientSecret you'll need in the next step:
{
"id": "your-app-id",
"name": "your-oauth-app-name",
"clientSecret": "tskey-app-<secret>",
"redirectURIs": ["http://localhost:31545/callback"],
"scopes": ["auth_keys:create", "auth_keys:create:once"],
...
}If using a custom --listen address, make sure the redirectUris in the OAuth app match your --redirect-uri.
make dev
make run ARGS="--secret tskey-app-<secret>"Or using an environment variable:
export HYDRALISK_SECRET="tskey-app-<secret>"
make runHydralisk starts on http://localhost:31545 by default.
- Open http://localhost:31545 in your browser.
- Enter a user login (e.g.
alice@example.com) in the text field. - Click Provision (single use) or Provision (new consent) to start the OAuth consent flow.
- A new tab opens where you log in and approve the authorization request.
- After approval, you're redirected back to Hydralisk. A new tsnet node (
hydralisk-N) begins connecting to your tailnet. - The dashboard updates in real time as the node connects. Once ready, the node appears in your tailnet's machine list.
After the first consent flow, the user's tokens appear on the dashboard. You can:
- Click Reuse access token to provision another node without any consent flow.
- Click Refresh token to obtain a new access token using the stored refresh token (the refresh token is rotated on each use).
Nodes removed from the tailnet (via the admin console or API) are automatically detected and stopped by Hydralisk.
All flags can also be set via environment variables. Flags take precedence over env vars.
| Flag | Env var | Default | Description |
|---|---|---|---|
--secret |
HYDRALISK_SECRET |
(required) | OAuth app secret (tskey-app-<clientID>-<secret>). The client ID is extracted automatically. |
--listen |
HYDRALISK_LISTEN |
localhost:31545 |
Address the web UI listens on |
--control-url |
HYDRALISK_CONTROL_URL |
https://login.tailscale.com |
Tailscale control server URL |
--redirect-uri |
HYDRALISK_REDIRECT_URI |
http://<listen>/callback |
OAuth redirect URI |
Access and refresh tokens are stored per-user. Multiple users can authorize the app, and each user's tokens are tracked independently. Refresh tokens are rotated on every use -- the old token is invalidated and replaced with the new one returned by the token endpoint.
The web UI exposes four provisioning strategies:
| Mode | Scope | Behavior |
|---|---|---|
| Single use | auth_keys:create:once |
Runs the full consent flow every time. The token is single-use. |
| Reuse access token | — | Reuses the stored access token for a specific user. No server round-trip. |
| Refresh token | — | Uses a specific user's refresh token to obtain a new access token. The refresh token is rotated. |
| New consent | auth_keys:create |
Runs the full consent flow, stores tokens for the user for future reuse. |
The Single use and New consent modes require entering a user login before starting the consent flow. The Reuse access token and Refresh token buttons appear per-user once tokens have been stored.
- The user enters their login and clicks a consent-flow button in the web UI.
- The browser opens the OAuth authorization URL on the control server.
- After the user approves, the control server redirects back to
/callbackwith an authorization code. - Hydralisk exchanges the code for an access/refresh token pair and stores them for the user.
- A new tsnet node (
hydralisk-<N>) starts in the background using the access token as its auth key. - The node joins the tailnet and begins listening on port 80 with a basic HTTP server.
- If the node is removed from the tailnet, Hydralisk detects this and stops the node automatically.
For returning users, the Reuse access token and Refresh token buttons skip the consent flow entirely.
The web UI polls /status to show real-time progress as each node connects.
| Path | Description |
|---|---|
/ |
Web UI dashboard |
/authorize?mode=<mode>&login=<user> |
Starts a consent flow (once, new) for the given user login |
/authorize?mode=<mode>&user=<user> |
Reuses tokens (access, refresh) for the given user |
/callback |
OAuth redirect handler |
/status |
JSON status of all provisioned nodes |
/purge?user=<user>&token=<access|refresh> |
Clears a user's tokens (omit token to remove user, omit both to purge all) |
