Tsadm
another tailscale API CLI client
#tsadm
tsadm is a proposed CLI tool for the Tailscale API:
The goal is to provide a stable, scriptable command line shape for the public Tailscale API while preserving the API's resource model.
#Configuration
The tailnet is selected by --tailnet, then $TAILNET, then the tsadm config
default. It can be a profile alias, a tailnet ID such as
T1234, a tailnet name such as foo.bar.ts.net or foo.github, or - for the
tailnet associated with the authenticated account when the API supports it.
tsadm --tailnet=X policy get
Authentication should use an API access token or OAuth access token accepted by the Tailscale API. Proposed lookup order:
--api-keyor--bearer-token$TAILSCALE_API_KEYor$TAILSCALE_ACCESS_TOKEN- tailnet profile selected by
--tailnet,$TAILNET, or thetsadm configdefault - platform keychain entry for
tsadm
Tailnet profiles live in $TSADM_CONFIG_DIR, or ~/.config/tsadm by default.
Each alias is a file named after the alias, containing JSON:
{
"tailnet": "TV9D11111111CNTRL",
"apiKey": "file:/home/me/keys/foo-tailnet.key"
}
bearerToken may be used instead of apiKey. The older shorter field name
key is accepted as an alias for apiKey when reading profiles. The configured
default is a symlink named default pointing to the alias file, for example
~/.config/tsadm/default -> foo.
Credentials starting with tskey-client- are OAuth client secrets and are
exchanged for bearer access tokens automatically, even if they were entered as
an API key.
config add-tailnet consumes positional arguments as alias, tailnet, then
credential, and prompts for any omitted suffix. It checks the credentials with
a cheap API GET by default; pass --check=false to skip that check.
Useful global flags:
--tailnet=<tailnet> Tailnet to operate on; defaults to $TAILNET,
then the tsadm config default.
--api-key=<key> Use an API key with HTTP Basic authentication.
A value of file:<path> reads the key from a file.
--bearer-token=<token> Use an OAuth bearer token.
A value of file:<path> reads the token from a file.
--base-url=<url> Override API base URL; defaults to the public API.
--hujson Emit HUJSON where supported, currently policy files.
--fields=<list> Request selected API fields when supported.
--filter=<k=v> Add a server-side list filter; repeatable.
--from-file=<path> Read request body or resource spec from a file.
--yes Skip confirmation for destructive operations.
Input conventions:
tsadm <resource> set --from-file=file.json tsadm <resource> set < file.json tsadm <resource> patch --from-file=patch.json tsadm <resource> patch key=value key2=value2
Output conventions:
tsadm <resource> list # concise text table by default tsadm <resource> list --json # API-shaped JSON, where supported tsadm <resource> get --json ID
Policy files are the exception: policy get preserves HUJSON by default, and
policy get --json converts the policy file to JSON.
Flag placement is strict. Global flags must appear before the resource name and
apply to all commands. Leaf flags are only accepted where each leaf command
defines them; for commands with positional operands, put --json before the
operand, such as tsadm device get --json <device-id>.
#Implemented Commands
This repository currently implements these commands:
tsadm policy get [--json] tsadm policy set [--from-file=file] [--etag=etag] tsadm policy validate [--from-file=file] tsadm config add-tailnet [--bearer-token] [--check=false] [<alias> [<tailnet> [<credential>]]] tsadm config list-tailnets [--json] tsadm config set-default-tailnet <alias> tsadm config check [<alias>] tsadm device list [--all] [--json] tsadm device get [--all] [--json] <device-id> tsadm device delete <device-id> tsadm device authorize <device-id> tsadm device deauthorize <device-id> tsadm device rename <device-id> <name> tsadm device reset-name <device-id> tsadm device expire-key <device-id> tsadm device tags set <device-id> [tag...] tsadm device ip set <device-id> <ipv4> tsadm device routes list [--json] <device-id> tsadm device routes set <device-id> [route...] tsadm device routes set --from-file=routes.json <device-id> tsadm device attrs get [--json] <device-id> tsadm device attrs set [--expiry=time] [--comment=text] <device-id> <attribute> <value> tsadm device attrs delete <device-id> <attribute> tsadm device attrs patch --from-file=attrs.json <device-id> tsadm key list [--user-only] [--json] tsadm key get [--json] <key-id> tsadm key create --from-file=key.json [--json] tsadm key delete <key-id> tsadm dns nameservers list [--json] tsadm dns nameservers set [addr...] tsadm dns nameservers set --from-file=nameservers.json tsadm dns preferences get [--json] tsadm dns preferences set --magic-dns=true|false tsadm dns preferences set --from-file=preferences.json tsadm dns searchpaths list [--json] tsadm dns searchpaths set [domain...] tsadm dns searchpaths set --from-file=searchpaths.json tsadm dns split get [--json] tsadm dns split set --from-file=split-dns.json tsadm dns split update --from-file=split-dns.json [--json] tsadm dns split patch --domain=corp.example.com --nameserver=10.0.0.53 tsadm dns split delete-domain corp.example.com tsadm dns config get [--json] tsadm dns config set --from-file=dns.json tsadm user list [--json] tsadm user get [--json] <user-id> tsadm user role set <user-id> owner|admin|it-admin|network-admin|billing-admin|auditor|member tsadm user approve <user-id> tsadm user suspend <user-id> tsadm user restore <user-id> tsadm user delete <user-id> tsadm user-invite list [--json] tsadm user-invite create [--email=email] [--role=role] [--from-file=file] [--json] tsadm user-invite get [--json] <invite-id> tsadm user-invite resend <invite-id> tsadm user-invite delete <invite-id> tsadm device invites list [--json] <device-id> tsadm device invite create [--email=email] [--multi-use] [--allow-exit-node] [--from-file=file] [--json] <device-id> tsadm device-invite get [--json] <invite-id> tsadm device-invite resend <invite-id> tsadm device-invite delete <invite-id> tsadm device-invite accept [--from-file=file] [--json] <invite-url-or-code> tsadm setting get [--json] tsadm setting patch [setting flags] tsadm setting patch --from-file=settings.json tsadm contact get [--json] tsadm contact set account|support|security user@example.com tsadm contact resend-verification account|support|security tsadm webhook list [--json] tsadm webhook create [--json] --url=https://example.com/hook --subscription=nodeCreated tsadm webhook create [--json] --from-file=webhook.json tsadm webhook get [--json] <endpoint-id> tsadm webhook patch [--json] --subscription=nodeCreated <endpoint-id> tsadm webhook patch [--json] --from-file=patch.json <endpoint-id> tsadm webhook delete <endpoint-id> tsadm webhook test <endpoint-id> tsadm webhook rotate-secret [--json] <endpoint-id> tsadm service list [--json] tsadm service get [--json] <service-name> tsadm service set --from-file=service.json <service-name> tsadm service delete <service-name> tsadm service devices list [--json] <service-name> tsadm service device approval get [--json] <service-name> <device-id> tsadm service device approval set --approved=true|false <service-name> <device-id> tsadm posture integrations list [--json] tsadm posture integrations get [--json] <integration-id> tsadm logs stream get [--json] configuration|network tsadm logs aws-external-id get-or-create [--reusable] [--json]
Singular resource names are preferred. Existing plural aliases, such as devices list and users list, continue to work.
The sections below remain the broader proposed command surface.
#Policy File
The Tailscale API exposes the policy file at /tailnet/{tailnet}/acl.
tsadm policy get tsadm policy get --json | jq . tsadm policy set < policy.hujson tsadm policy set --from-file=policy.hujson tsadm policy validate < policy.hujson tsadm policy validate --from-file=policy.hujson tsadm policy preview --src=<user-or-tag> --dst=<host:port> tsadm policy preview --from-file=request.json
policy get should preserve HUJSON by default. --json converts HUJSON to JSON.
#Devices
Device list operations are tailnet-scoped. Device mutation operations use the stable device ID returned by the API.
tsadm device list tsadm device list --fields=id,name,hostname,tags tsadm device list --filter=isEphemeral=true --filter=tags=tag:prod tsadm device attrs patch --from-file=attrs.json <device-id> tsadm device get <device-id> tsadm device delete <device-id> tsadm device expire-key <device-id> tsadm device authorize <device-id> tsadm device deauthorize <device-id> tsadm device rename <device-id> <name> tsadm device reset-name <device-id> tsadm device tags set <device-id> tag:foo tag:bar tsadm device key set --key-expiry-disabled=true <device-id> tsadm device ip set <device-id> <ipv4> tsadm device routes list <device-id> tsadm device routes set <device-id> 10.0.0.0/16 192.168.1.0/24 tsadm device routes set --from-file=routes.json <device-id> tsadm device attrs get <device-id> tsadm device attrs set <device-id> custom:asset-tag value tsadm device attrs set --expiry=2026-12-31T00:00:00Z <device-id> custom:asset-tag value tsadm device attrs delete <device-id> custom:asset-tag
#Auth Keys And Trust Credentials
The API's keys resource covers auth keys and trust credentials.
tsadm key list tsadm key list --user-only tsadm key create --from-file=key.json tsadm key create --ephemeral --reusable --preauthorized --tag=tag:ci tsadm key get <key-id> tsadm key set <key-id> --from-file=key.json tsadm key delete <key-id>
#DNS
DNS commands mirror the API's nameserver, preference, search path, split DNS, and whole-configuration endpoints.
tsadm dns nameservers list tsadm dns nameservers set 1.1.1.1 8.8.8.8 tsadm dns nameservers set --from-file=nameservers.json tsadm dns preferences get tsadm dns preferences set --magic-dns=true tsadm dns preferences set --from-file=preferences.json tsadm dns searchpaths list tsadm dns searchpaths set corp.example.com svc.example.com tsadm dns searchpaths set --from-file=searchpaths.json tsadm dns split get tsadm dns split set --from-file=split-dns.json tsadm dns split patch --domain=corp.example.com --nameserver=10.0.0.53 tsadm dns split delete-domain corp.example.com tsadm dns config get tsadm dns config set --from-file=dns.json
#Users
tsadm user list tsadm user get <user-id> tsadm user role set <user-id> owner|admin|it-admin|network-admin|billing-admin|auditor|member tsadm user approve <user-id> tsadm user suspend <user-id> tsadm user restore <user-id> tsadm user delete <user-id>
#User Invites
tsadm user-invites list tsadm user-invite create --email=user@example.com --role=member tsadm user-invite create --from-file=invite.json tsadm user-invite get <invite-id> tsadm user-invite resend <invite-id> tsadm user-invite delete <invite-id>
#Device Invites
tsadm device invites list <device-id> tsadm device invite create <device-id> --email=user@example.com tsadm device invite create <device-id> --from-file=invite.json tsadm device-invite get <invite-id> tsadm device-invite resend <invite-id> tsadm device-invite delete <invite-id> tsadm device-invite accept --from-file=invite.json
#Logging
tsadm logs configuration list tsadm logs network list tsadm logs stream status configuration|network tsadm logs stream get configuration|network tsadm logs stream set configuration|network --from-file=stream.json tsadm logs stream disable configuration|network tsadm logs aws-external-id get-or-create tsadm logs aws-external-id validate <id> --role-arn=<arn> tsadm logs aws-external-id validate <id> --from-file=request.json
#Device Posture Integrations
tsadm posture integrations list tsadm posture integration create --from-file=integration.json tsadm posture integration get <integration-id> tsadm posture integration patch <integration-id> --from-file=patch.json tsadm posture integration delete <integration-id>
Custom device posture attributes are under the device attrs command.
#Contacts
Contact types are the API's contact preference types: account, support,
and security.
tsadm contact get tsadm contact set account|support|security user@example.com tsadm contact resend-verification account|support|security
#Webhooks
tsadm webhook list tsadm webhook create --url=https://example.com/hook --subscription=nodeCreated tsadm webhook create --from-file=webhook.json tsadm webhook get <endpoint-id> tsadm webhook patch --subscription=nodeCreated <endpoint-id> tsadm webhook patch --from-file=patch.json <endpoint-id> tsadm webhook delete <endpoint-id> tsadm webhook test <endpoint-id> tsadm webhook rotate-secret <endpoint-id>
#Tailnet Settings
tsadm setting get tsadm setting patch --devices-approval-on=true tsadm setting patch --users-approval-on=true tsadm setting patch --acls-externally-managed-on=true --acls-external-link=https://example.com/policy tsadm setting patch --from-file=settings.json
#Services
Tailscale Services are tailnet-scoped resources named like svc:example.
tsadm service list tsadm service get <service-name> tsadm service set --from-file=service.json <service-name> tsadm service delete <service-name> tsadm service devices list <service-name> tsadm service device approval get <service-name> <device-id> tsadm service device approval set --approved=true <service-name> <device-id>
#Raw Escape Hatch
For endpoints that are new or not yet wrapped by a first-class command:
tsadm api get /tailnet/{tailnet}/devices
tsadm api post /tailnet/{tailnet}/acl/validate --from-file=policy.hujson
tsadm api patch /tailnet/{tailnet}/settings --from-file=settings.json
tsadm api delete /device/{deviceId}
{tailnet} should be expanded from --tailnet or $TAILNET. Other path
parameters must be supplied literally or with --param name=value.