9.2 KiB
name, description
| name | description |
|---|---|
| tailscale | Manage an ACG-administered Tailscale tailnet via the Tailscale REST API v2: list/inspect devices, delete a device/node, authorize a device, list/create/revoke tagged pre-auth keys. ADMIN rights (add + delete). Reads run freely; every device delete/authorize and key create/delete is gated --confirm. Triggers: tailscale, tailnet, add device, delete device/node, revoke/create auth key, tailscale offboard. |
Tailscale Skill
Standalone bash CLI for a Tailscale tailnet that ACG administers, via the Tailscale
REST API v2 (https://api.tailscale.com/api/v2). Read-only by default; every
destructive or state-changing operation (delete a device, authorize a device, create an
auth key, revoke an auth key) is gated behind --confirm and prints a preview first.
This skill implements the API side of ACG's per-client tailnet doctrine — see
wiki/patterns/tailscale-client-management.md. One tailnet per client, ACG holds an
Admin/Owner seat, devices enroll as tagged nodes via reusable + pre-approved pre-auth
keys pushed from GuruRMM. This skill is how you drive that tailnet from the terminal:
mint the tagged keys, watch devices land, authorize/prune nodes, and revoke keys at
offboarding.
Running the CLI
TS="bash $CLAUDETOOLS_ROOT/.claude/skills/tailscale/scripts/tailscale-api.sh"
$TS status # auth check + tailnet + device count
$TS devices # list devices (table)
$TS devices --json # machine output
$TS device <name|id|100.x> # device detail
$TS keys # list auth keys
$TS create-key --tag tag:roberts --reusable --preauth --expiry-days 90 --desc "onboard" --confirm
$TS delete-key <keyId> --confirm # revoke an auth key
$TS delete-device <name|id|100.x> --confirm # remove a node
$TS authorize <name|id|100.x> --confirm # approve a device
$TS --help # full usage (runs with no creds)
--vault <path> selects a different vault entry (per-client tailnets each have their own).
CLAUDETOOLS_ROOT resolves from the env var, else the repo root via git rev-parse,
else C:/claudetools.
Credentials & auth (vault-first, OAuth preferred)
The bearer is NEVER hardcoded. At runtime it is resolved from the SOPS vault entry
tailscale/api-access.sops.yaml (override with --vault or TAILSCALE_VAULT). Two
credential shapes are supported, OAuth preferred:
- OAuth client (preferred, non-expiring):
credentials.client_id+credentials.client_secret. The CLI POSTsgrant_type=client_credentials&client_id=...&client_secret=...(secret fed over stdin, never argv) to/oauth/tokenand uses the returned short-livedaccess_tokenas the bearer. Required scopes:devices:core(list/delete/authorize devices) andauth_keys(create/list/delete keys). - Raw API access token (fallback):
credentials.api_key(atskey-api-...token) used directly asAuthorization: Bearer <token>.
Tailnet is a path segment, resolved (first hit wins) from: env TAILSCALE_TAILNET,
credentials.tailnet, top-level plaintext tailnet:, else default - (the default
tailnet of the authenticated principal). Set a specific org name (e.g. example.com) only
when the account administers more than one tailnet.
The bearer is written to a 0600 temp curl config (-K) and removed on exit, so it never
appears in argv, the process list, or stdout.
Provision the vault entry (admin, one-time — entry does NOT exist yet)
OAuth client (preferred):
bash .claude/skills/vault/scripts/vault-helper.sh new tailscale/api-access.sops.yaml \
--kind oauth-client --name "ACG Tailscale API (OAuth client)" \
--url https://api.tailscale.com/api/v2 --tag tailscale \
--set client_id='<OAUTH_CLIENT_ID>' \
--set client_secret='<OAUTH_CLIENT_SECRET>' \
--set tailnet='-'
Raw API access token (fallback):
bash .claude/skills/vault/scripts/vault-helper.sh new tailscale/api-access.sops.yaml \
--kind api-key --name "ACG Tailscale API (access token)" \
--url https://api.tailscale.com/api/v2 --tag tailscale \
--set api_key='tskey-api-XXXXXXXXXXXXXXXX' \
--set tailnet='-'
vault-helper.sh new places every --set under the encrypted credentials: block, so
tailnet reads back via credentials.tailnet (the CLI also honors a top-level plaintext
tailnet: if an admin prefers it in cleartext). After creating: vault-helper.sh verify tailscale/api-access.sops.yaml, then publish with bash .claude/scripts/sync.sh. For a
per-client tailnet, store its own entry (e.g. tailscale/roberts.sops.yaml) and pass
--vault tailscale/roberts.sops.yaml.
Endpoint map (Tailscale REST API v2)
| Subcommand | Method + path | Notes |
|---|---|---|
devices |
GET /tailnet/{tailnet}/devices?fields=all |
full device inventory |
device <q> |
GET /device/{deviceId}?fields=all |
detail after name->id resolve |
delete-device <q> |
DELETE /device/{deviceId} |
GATED |
authorize <q> |
POST /device/{deviceId}/authorized {"authorized":true} |
GATED |
keys |
GET /tailnet/{tailnet}/keys |
list auth keys |
create-key |
POST /tailnet/{tailnet}/keys |
GATED; tagged reusable/preauth pattern |
delete-key <keyId> |
DELETE /tailnet/{tailnet}/keys/{keyId} |
GATED revoke |
status |
GET .../devices |
reachability + counts |
create-key body (matches the wiki's reusable + pre-approved + tagged pattern):
{"capabilities":{"devices":{"create":{"reusable":true,"ephemeral":false,"preauthorized":true,"tags":["tag:roberts"]}}},"expirySeconds":7776000,"description":"onboard roberts"}
Built from --tag (repeatable), --reusable, --preauth, --ephemeral,
--expiry-days N (default 90), --desc. The created key secret is returned once — the
CLI prints a [WARNING] and the secret; store it in the vault immediately, never in
chat/commit.
Hard rules
- Read before write. Device ops accept a NAME, the stable numeric/node id, or a 100.x address; the name is resolved to an id against the live device list before any action.
- Gated destructive ops.
delete-device,authorize,create-key,delete-keyrefuse to run without--confirm— they print the exact preview and exit 3. Never run a delete casually; deleting a node drops it off the tailnet. - Ambiguous-match STOP. If a device query matches >1 device, the CLI lists the candidates and refuses to act (exit 4). Re-run with a specific id or a unique name/address.
- Per-client isolation. Never merge a client into ACG's own tailnet and never share one
tailnet across clients — one tailnet per client, one vault entry per tailnet
(
wiki/patterns/tailscale-client-management.md). Confirm you are pointed at the right--vault/tailnet before a write. - Secrets stay out of argv/stdout/logs. Bearer via
-Kconfig file; OAuth secret via stdin. Do not paste key secrets into tickets/commits — vault the path.
Bot alert (after every write)
Mirroring the /rmm and screenconnect skills, every successful write posts a
one-line [TAILSCALE] ... alert via .claude/scripts/post-bot-alert.sh (soft-fail,
ASCII only). Read operations (status, devices, device, keys) do NOT alert. Covered
writes: delete-device, authorize, create-key, delete-key.
Verified API response shapes
- Device (list item / detail):
id(stable numeric string),nodeId,name(MagicDNS FQDN),hostname,addresses(["100.x.y.z", "fd7a:..."]),os,user,tags(["tag:roberts"]),authorized(bool),lastSeen,clientVersion,updateAvailable. Device ops useid. - Auth key (list / create):
id(keyId),key(the secret — create only, once),created,expires,capabilities.devices.create.{reusable,ephemeral,preauthorized,tags},description. - Errors: JSON
{"message":"..."}with a non-2xx status.
Error table
| Symptom | Cause | Fix |
|---|---|---|
[BLOCKED] no Tailscale credential |
vault entry missing / wrong fields | provision per above; check --vault path |
OAuth token exchange failed (HTTP 401) |
bad client_id/secret or missing scopes | verify OAuth client has devices:core + auth_keys |
HTTP 403 on a write |
token/OAuth scope lacks the capability | add auth_keys (keys) / devices:core (devices) scope |
HTTP 404 on device/key |
wrong id, or wrong tailnet | check status tailnet; re-resolve by name |
AMBIGUOUS (>N matches) |
name matches several nodes | pass a specific id or a unique name/address |
no device matches |
typo / device gone | run devices to see current inventory |
On a GENUINE functional error (auth failure, non-2xx API response) the CLI logs to
errorlog.md via log-skill-error.sh (soft-fail). Expected/handled conditions (a --confirm
gate blocking, an empty match, a preview) are NOT logged.
Reference
- Doctrine:
wiki/patterns/tailscale-client-management.md(per-client tailnets, tagged pre-auth keys, enrollment via GuruRMM, offboarding). - Enrollment script:
wiki/patterns/tailscale-client-enroll.ps1. - Tailscale API docs: https://tailscale.com/api and https://tailscale.com/kb/1085/auth-keys