Tailscale

How catch gets a tagged identity and how services get their own tailnet nodes.

Yeet uses Tailscale in two places:

  • catch exposes host access through an embedded Tailscale node.
  • Services can get their own Tailscale identity with --net=ts.

Catch joins your tailnet, but the local yeet CLI does not run its own Tailscale client. Normal yeet commands use HTTP and WebSocket connections to the catch hostname, so your workstation must have network reachability to that Tailscale address. In the standard setup, install Tailscale on the workstation and connect it to the same tailnet. Commands that query local tailnet state, such as yeet list-hosts, also use the local Tailscale client API.

There are two separate questions here: can your workstation reach catch, and what is the caller allowed to do after it reaches catch? Tailscale handles both layers, but they fail differently.

First-time host setup

Catch must join your tailnet as a tagged node, usually tag:catch. Catch rejects user-owned nodes.

Before you run yeet init, set up Tailscale in this order:

  1. Add a yeet owner tag, such as tag:yeet.
  2. Let that owner tag own the catch tag, usually tag:catch.
  3. Let that owner tag own any service tags you plan to use later, such as tag:app.
  4. Open Trust credentials -> Credential -> OAuth in the Tailscale admin console.
  5. Choose All - Read & Write for a simple broad credential, or choose custom scopes with Auth Keys write (auth_keys) for least privilege.
  6. For catch-only installs, select tag:catch directly. For future service --net=ts, select the owner tag, such as tag:yeet, so the credential can create keys for tag:catch and service tags such as tag:app.
  7. Copy the tskey-client-... OAuth client secret.

Then run yeet init root@<machine-host> and paste that secret when prompted. Validate the host with yeet status.

Minimal policy shape:

{
  "tagOwners": {
    "tag:yeet": ["autogroup:admin"],
    "tag:catch": ["tag:yeet"],
    "tag:app": ["tag:yeet"],
  },
  "grants": [
    {
      "src": ["autogroup:admin"],
      "dst": ["tag:catch"],
      "ip": ["tcp:41548"],
      "app": {
        "yeetrun.com/app/yeet": [{ "allow": ["read", "manage", "ssh"] }],
      },
    },
    {
      "src": ["tag:catch"],
      "dst": ["tag:catch"],
      "ip": ["tcp:41548"],
    },
  ],
}

The first grant lets Tailscale admins run the first setup and normal admin commands. See Tailscale Access Grants for the read, manage, and ssh permissions attached to that grant. Keep autogroup:admin directly in the grant src; do not put it inside a custom policy-file group.

The catch node joins as tag:catch. With the least-privilege owner-tag setup, the OAuth client uses tag:yeet only so it can mint auth keys for tag:catch and later service tags.

If you are comfortable with broad Tailscale API access, All - Read & Write is the simplest OAuth scope choice. If you do not plan to use --net=ts for services, a least-privilege OAuth client can assign tag:catch directly. The owner-tag pattern is more flexible because the credential can mint the future service tags you deliberately allowed.

Run init

Interactive init prompts for the OAuth client secret during first setup:

yeet init root@<machine-host>

The OAuth secret belongs to catch itself. If the resulting catch node is not tagged, catch refuses connections. The install may have succeeded; the identity still failed.

Host naming

yeet init root@<machine-host> uses SSH. Normal commands use the catch Tailscale hostname and require your workstation to be able to reach that tailnet address:

CATCH_HOST=<catch-host> yeet status
yeet --host=<catch-host> status
yeet run <svc>@<catch-host> ./compose.yml

With no saved or explicit host, catch asks Tailscale for the hostname catch. For more than one catch host, choose a distinct name during setup:

yeet --host=morpheus-catch init root@<machine-host>

If another tailnet device already owns the requested catch hostname, Tailscale may assign a suffixed name such as catch-1. Use that assigned catch host, or remove the stale/conflicting Tailscale device and rerun yeet init. Host names are state too.

Service networking (--net=ts)

Use ts when a service should get its own tailnet identity, tailnet IP, tags, ACLs, or Tailscale Serve config.

For most services, use svc,ts instead of plain ts:

yeet run <svc> ./compose.yml --net=svc,ts --ts-tags=tag:app

svc,ts keeps yeet DNS, private service-to-service traffic, and ordinary internet egress through the catch host. It also gives the service its own Tailscale identity.

Use plain ts only for deliberately tailnet-only services:

yeet run <svc> ./compose.yml --net=ts --ts-tags=tag:app
yeet ip <svc>

Plain ts does not provide ordinary outbound internet by itself. Configure an exit node when a ts-only service needs internet access:

yeet run <svc> ./compose.yml --net=ts --ts-tags=tag:app --ts-exit=<exit-node>

For an existing Tailscale service:

yeet tailscale <svc> -- set --exit-node=<exit-node>

Make sure the OAuth client saved during catch setup can assign every tag you pass with --ts-tags. If the credential cannot mint the tag, deployment fails.

Options:

  • --ts-tags=<tag> sets service tags.
  • --ts-exit=<node> uses a Tailscale exit node.
  • --ts-ver=<ver> selects a Tailscale version for the service.
  • --ts-auth-key=<key> passes a direct auth key for that service.

See Networking for when to choose svc, lan, ts, or a combination.

Tailscale commands for a service

Run Tailscale commands for a service identity:

yeet tailscale <svc> -- status
yeet ts <svc> -- debug daemon-logs

Update the yeet-managed service Tailscale binary:

yeet ts <svc> update
yeet ts <svc> update --version=1.95.112

If you intentionally want Tailscale's own upstream update subcommand, pass it through after --:

yeet ts <svc> -- update

Tailscale Serve

Expose a service over HTTPS on your tailnet:

yeet ts <svc> serve --bg 3000

Serve requires tailnet HTTPS. With --bg, the Serve config persists across reboots.

Cleanup

When you remove a service, yeet attempts to delete that service's Tailscale device from your tailnet. Cleanup matters because old service identities are old access paths.

References