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.
cargo build --release -p tsctlThe binary is written to target/release/tsctl.
Provide either an API access token or an OAuth client.
export TS_API_KEY='tskey-api-...'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.
List devices in the tailnet associated with the token:
tsctl list -n -List devices in a named tailnet:
tsctl list -n example.comList 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.comView 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 yamltsctl 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 -fThis GETs /tailnet/{tailnet}/acl, then POSTs the policy back with If-Match
when something changed:
- inserts the tag under
tagOwnersowned byautogroup:adminif missing - if
-ris set, addsautoApprovers.routes.<cidr> = ["<tag>"](keeps any existing approvers for that CIDR) - if
-p/--ippoolis set, setsnodeAttrs{ "target": ["*"], "ipPool": [...] }to exactly the CIDRs passed (repeat-pfor more than one). Replaces CIDRs on that*entry; does not merge with othernodeAttrstargets - if
--disable-cgnatis set, addsattr: ["disable-linux-cgnat-drop-rule"]on the*nodeAttrsentry (keepsipPooland 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 -fupdate 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.