Skip to content

Latest commit

 

History

History
184 lines (143 loc) · 6.88 KB

README.md

File metadata and controls

184 lines (143 loc) · 6.88 KB

tsctl

tsctl is a command-line client for inspecting Tailscale tailnets and devices, and for updating ACL policy or a device IPv4 address, through the Tailscale REST API v2.

Build

cargo build --release -p tsctl

The binary is written to target/release/tsctl.

Authentication

Provide either an API access token or an OAuth client.

API access token

export TS_API_KEY='tskey-api-...'

OAuth client

Create the OAuth client in the Tailscale admin console under Trust credentials and grant Devices → Read (scope devices:core:read, or the legacy devices / devices:read scopes). Without a devices scope, list and view return HTTP 403. Listing all OAuth clients with list -c needs all:read (or a user API key). oauth_keys:read is enough to resolve the current --client-id only. Human view -n reads ACL nodeAttrs / autoApprovers / tagOwners and needs policy_file:read. update acl writes need policy_file (legacy acl): tags (-t), route auto-approvers (-r), --ippool / -p, and --disable-cgnat. update node writes need devices:core (legacy devices) write, not policy_file.

export TS_CLIENT_ID='k...'
export TS_CLIENT_SECRET='tskey-client-...'

tsctl exchanges the client credentials for a short-lived access token before calling the API. By default it requests no extra scope parameter, so the token inherits every scope granted to the OAuth client (same as Tailscale's documented curl example). To narrow the token, set TS_OAUTH_SCOPE or --oauth-scope (for example devices:core:read).

Keep credentials out of configuration files and source control.

The API URL defaults to https://api.tailscale.com/api/v2/. It can be overridden with TS_API_URL or --api-url.

Commands

List devices in the tailnet associated with the token:

tsctl list -n -

List devices in a named tailnet:

tsctl list -n example.com

List OAuth client IDs in a tailnet:

tsctl list -c
tsctl list -n example.com --clients

-c/--clients calls GET /tailnet/{tailnet}/keys?all=true and keeps entries with keyType=client. Tailscale only enumerates OAuth clients for credentials with all:read / all (or a user API access token). When you authenticate with --client-id / TS_CLIENT_ID, that client is always included (details are filled in when oauth_keys:read or all:read allows GET /keys/{id}).

View a tailnet with ACL first (nodeAttrs, autoApprovers, and tagOwners as nested sections under ACL:). Keys and values that contain : or ' are wrapped in single quotes (' inside a value is doubled). Human ACL output is indented text, not JSON or tables. - is the tailnet that belongs to the token:

tsctl view -n -
tsctl view -n example.com

View one device using its device ID, node ID, MagicDNS name, or hostname:

tsctl view -d n1234567890CNTRL
tsctl view -d gateway.example.ts.net

-n/--tailnet is the tailnet organization ID (or -), not a device name. -d/--device selects a single device.

Return the original REST response as JSON or YAML:

tsctl view -d n1234567890CNTRL --output json
tsctl view -n example.com -o yaml

tsctl view requires exactly one of -n/--tailnet and -d/--device. Omit -o/--output for the human-readable view (nested ACL sections, then the node table). -o json / -o yaml return the devices (or device) REST payload only; they do not include ACL.

update has two subcommands: acl (policy) and node (IPv4). Older tsctl update --ippool / tsctl update -t forms are rejected; the error includes a hint to use update acl.

Update ACL tagOwners (needed before an OAuth client can use a tag). Pass the tag with -t/--tag. Optionally pass -r/--route so tagged subnet routers can auto-approve that CIDR:

tsctl update acl -t tag:cluster-node
tsctl update acl -t tag:cluster-node -r 10.50.0.0/18
tsctl update acl -n example.com --tag tag:cluster-node --route 10.50.0.0/18
tsctl update acl -t tag:cluster-node -r 10.50.0.0/18 -f
tsctl update acl --ippool 100.96.0.0/16
tsctl update acl -p 100.96.0.0/16 -f
tsctl update acl --disable-cgnat
tsctl update acl --ippool 100.96.0.0/16 --disable-cgnat -f
tsctl update acl -p 100.64.0.0/16 -p 100.96.0.0/16 -f

This GETs /tailnet/{tailnet}/acl, then POSTs the policy back with If-Match when something changed:

  • inserts the tag under tagOwners owned by autogroup:admin if missing
  • if -r is set, adds autoApprovers.routes.<cidr> = ["<tag>"] (keeps any existing approvers for that CIDR)
  • if -p/--ippool is set, sets nodeAttrs { "target": ["*"], "ipPool": [...] } to exactly the CIDRs passed (repeat -p for more than one). Replaces CIDRs on that * entry; does not merge with other nodeAttrs targets
  • if --disable-cgnat is set, adds attr: ["disable-linux-cgnat-drop-rule"] on the * nodeAttrs entry (keeps ipPool and any other attrs)

-t/--tag, -p/--ippool, or --disable-cgnat is required for update acl. -r/--route still needs -t. Tailscale assigns device IPv4 from nodeAttrs.ipPool; autoApprovers.routes only auto-approves advertised subnet CIDRs. If the tag, route, ipPool, and attr are already present, no write is sent. A write asks for confirmation (y/yes). Use -f/--force to skip the prompt. Non-interactive runs require -f. When authenticating with an OAuth client, the prompt includes that client ID.

autoApprovers applies when a route is first advertised. It does not approve routes that are already advertised and pending. Re-advertise 10.50.0.0/18 on the vpn-gateway nodes (or approve Machines → Subnet routes in the admin console) so PrimaryRoutes can become ["10.50.0.0/18"]. The credential needs the policy_file scope (legacy acl) to write; GET alone needs policy_file:read. JSON is used for the round-trip, so HuJSON comments in the policy file are not preserved.

Set an existing node's Tailscale IPv4. Changing nodeAttrs.ipPool does not move addresses already assigned. -d/--node is a node ID, device ID, MagicDNS name, or hostname (same identifiers as view -d). The new --ip should be in Tailscale CGNAT 100.64.0.0/10 (the usual ipPool range). Addresses outside that range are rejected. This POSTs /device/{deviceID}/ip and drops existing sessions to that node:

tsctl update node -d n1234567890CNTRL --ip 100.96.0.10
tsctl update node -d gateway.example.ts.net --ip 100.96.0.10 -f

update node requires -d/--node and --ip. If the node already has that IPv4, no write is sent. A write asks for confirmation (y/yes). Use -f/--force to skip the prompt. Non-interactive runs require -f. The credential needs devices:core write (legacy devices; Tailscale's API name for a node). WorkerCluster OAuth clients that only have policy_file cannot call this.

Run tsctl <command> --help for complete syntax.