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.
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:
- Manually create and rotate auth keys
- 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.
- Go to the Trust credentials page in the Tailscale admin console
- Click Create OAuth client
- Give it a description (e.g., "ephemeral-nodes")
- Under OAuth client scopes, select Auth keys with Write permissions
- Under Tags, add the tags your nodes will use (e.g.,
tag:my-service) - Select Create OAuth client and copy the Client ID and Client Secret immediately (the secret cannot be retrieved later)
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).
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.
- Go to the Access Controls page in the admin console
- You'll see a JSON policy file. Add your tag to the
tagOwnerssection (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.
Pass OAuth credentials and tailnet ID via tsnetephemeral.Config.
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).
OAuthClientID,OAuthClientSecret,TailnetID: Required credentials and target tailnet.APIBaseURL: Optional Tailscale API base URL. Default:https://api.tailscale.com.APIClient: Optional custom*http.Clientfor Tailscale API calls.RequestTimeout: Timeout for package-managed API client. Default:30s.UpTimeout: Timeout forts.Up(...). Default:30s.KeyExpiry: Requested auth key expiry. Default:5m.Preauthorized: Optional auth key preauthorization flag. Default:true.
- On startup, the package exchanges your OAuth credentials for an access token
- It uses that token to generate a single-use, ephemeral auth key via the Tailscale API
- It starts a
tsnet.Serverwith that key,Ephemeral=true, and an in-memory tsnet state store - When your process exits and calls
ts.Close(), the node is automatically removed from your Tailnet
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.
- Open Access Controls in the Tailscale admin console.
- In your policy JSON, add this
tagOwnersentry (or merge it into your existingtagOwnersobject):
{
"tagOwners": {
"tag:tsnetephemeral-tests": ["autogroup:admin"]
}
}- Save/apply the policy.
- Open Trust credentials.
- Click Create OAuth client.
- For scopes, choose one write scope for key creation:
Auth keys: Write(new scopes UI) orDevices: Write(legacy scopes UI). - In allowed tags for the OAuth client, add exactly:
tag:tsnetephemeral-tests. - Create the client and copy the values shown once:
Client IDandClient Secret. - Open Settings -> General in the admin console and copy your tailnet
ID/name (for example,
xyz123,example.com, oryour-org.org.github).
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