Skip to content

Package tsnetephemeral provisions ephemeral tsnet servers by minting a fresh Tailscale auth key from OAuth credentials on each startup.

License

Notifications You must be signed in to change notification settings

sjc5/tsnetephemeral

Repository files navigation

github.com/sjc5/tsnetephemeral

A Go package for running ephemeral Tailscale nodes in your applications. It handles OAuth + auth key generation automatically and uses in-memory tsnet state so node identity is not reused across restarts.

When to use this

The problem: You have services running on different machines (your laptop, a cloud VM, a Raspberry Pi) that need to talk to each other. Normally, you'd either put them on the public internet (risky, requires firewall rules, SSL certs, etc.) or set up a VPN (complex).

What Tailscale does: Tailscale creates a private network (called a "Tailnet") between your machines. You install Tailscale on each machine, and they can reach each other directly using private IPs or hostnames like my-server.your-tailnet.ts.net, with no public exposure or port forwarding.

What this package solves: Normally, to add a machine to your Tailnet, you either log in interactively or provide an "auth key" you generated in the Tailscale dashboard. But if you're running ephemeral services (containers, autoscaled instances, CI jobs, serverless), you don't want to:

  1. Manually create and rotate auth keys
  2. Have dead nodes cluttering your Tailnet after containers shut down

This package handles both. On startup, it uses OAuth credentials to generate a fresh, single-use auth key via the Tailscale API, joins your Tailnet, and marks the node as ephemeral so it disappears automatically when the process exits.

Setup

1. Create an OAuth client in Tailscale

  1. Go to the Trust credentials page in the Tailscale admin console
  2. Click Create OAuth client
  3. Give it a description (e.g., "ephemeral-nodes")
  4. Under OAuth client scopes, select Auth keys with Write permissions
  5. Under Tags, add the tags your nodes will use (e.g., tag:my-service)
  6. Select Create OAuth client and copy the Client ID and Client Secret immediately (the secret cannot be retrieved later)

2. Get your Tailnet ID

Your Tailnet ID is visible in the admin console URL or on the Settings > General page (for example, xyz123, example.com, or your-org.org.github).

3. Define your tag in Tailscale's Access Controls

Tags identify non-human devices (servers, containers, etc.) in your Tailnet. Before you can use a tag like tag:my-service, you need to register it in Tailscale.

  1. Go to the Access Controls page in the admin console
  2. You'll see a JSON policy file. Add your tag to the tagOwners section (create it if it doesn't exist):
{
	"tagOwners": {
		"tag:my-service": ["autogroup:admin"]
	}
}

This says "admins are allowed to assign tag:my-service to devices."

NOTE: Device tags do not automatically grant network access. If your tailnet ACLs are restrictive, add rules that allow traffic to and from your tag. See Tailscale ACL docs.

4. Provide credentials to your app

Pass OAuth credentials and tailnet ID via tsnetephemeral.Config.

Usage

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/sjc5/tsnetephemeral"
)

func main() {
    mgr := tsnetephemeral.NewManager(tsnetephemeral.Config{
        Hostname: "internal-api",
        Tags:     []string{"tag:my-service"},
        OAuthClientID:     os.Getenv("TAILSCALE_OAUTH_CLIENT_ID"),
        OAuthClientSecret: os.Getenv("TAILSCALE_OAUTH_CLIENT_SECRET"),
        TailnetID:         os.Getenv("TAILSCALE_TAILNET_ID"),

        // Optional tuning (defaults shown):
        UpTimeout:      30 * time.Second,
        RequestTimeout: 30 * time.Second,
        KeyExpiry:      5 * time.Minute,
    })
    if err := mgr.Validate(); err != nil {
        log.Fatal(err)
    }

    ts, err := mgr.InitServer(context.Background())
    if err != nil {
        log.Fatal(err)
    }
    defer ts.Close()

    // Listen only on Tailscale, with no public internet exposure.
    ln, err := ts.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }

    log.Println("listening on Tailscale as internal-api:8080")
    http.Serve(ln, yourHandler())
}

Other machines on your Tailnet can now reach this service at internal-api:8080 (or via MagicDNS: internal-api.your-tailnet.ts.net:8080).

Configuration options

  • OAuthClientID, OAuthClientSecret, TailnetID: Required credentials and target tailnet.
  • APIBaseURL: Optional Tailscale API base URL. Default: https://api.tailscale.com.
  • APIClient: Optional custom *http.Client for Tailscale API calls.
  • RequestTimeout: Timeout for package-managed API client. Default: 30s.
  • UpTimeout: Timeout for ts.Up(...). Default: 30s.
  • KeyExpiry: Requested auth key expiry. Default: 5m.
  • Preauthorized: Optional auth key preauthorization flag. Default: true.

How it works

  1. On startup, the package exchanges your OAuth credentials for an access token
  2. It uses that token to generate a single-use, ephemeral auth key via the Tailscale API
  3. It starts a tsnet.Server with that key, Ephemeral=true, and an in-memory tsnet state store
  4. When your process exits and calls ts.Close(), the node is automatically removed from your Tailnet

Integration smoke test

The repo includes a skipped-by-default integration test that calls the real Tailscale API to mint an ephemeral auth key. The test uses the tag tag:tsnetephemeral-tests.

Dashboard setup (exact values)

  1. Open Access Controls in the Tailscale admin console.
  2. In your policy JSON, add this tagOwners entry (or merge it into your existing tagOwners object):
{
	"tagOwners": {
		"tag:tsnetephemeral-tests": ["autogroup:admin"]
	}
}
  1. Save/apply the policy.
  2. Open Trust credentials.
  3. Click Create OAuth client.
  4. For scopes, choose one write scope for key creation: Auth keys: Write (new scopes UI) or Devices: Write (legacy scopes UI).
  5. In allowed tags for the OAuth client, add exactly: tag:tsnetephemeral-tests.
  6. Create the client and copy the values shown once: Client ID and Client Secret.
  7. Open Settings -> General in the admin console and copy your tailnet ID/name (for example, xyz123, example.com, or your-org.org.github).

Run the smoke test

Create a local .env file (or copy .env.example) with:

TAILSCALE_OAUTH_CLIENT_ID="your-client-id"
TAILSCALE_OAUTH_CLIENT_SECRET="your-client-secret"
TAILSCALE_TAILNET_ID="xyz123"

Then run:

make smoke

About

Package tsnetephemeral provisions ephemeral tsnet servers by minting a fresh Tailscale auth key from OAuth credentials on each startup.

Resources

License

Stars

Watchers

Forks

Packages