Files
claudetools/.claude/skills/tailscale/SKILL.md
Howard Enos d5f7eadf82 sync: auto-sync from HOWARD-HOME at 2026-07-06 08:29:31
Author: Howard Enos
Machine: HOWARD-HOME
Timestamp: 2026-07-06 08:29:31
2026-07-06 08:30:01 -07:00

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 POSTs grant_type=client_credentials&client_id=...&client_secret=... (secret fed over stdin, never argv) to /oauth/token and uses the returned short-lived access_token as the bearer. Required scopes: devices:core (list/delete/authorize devices) and auth_keys (create/list/delete keys).
  • Raw API access token (fallback): credentials.api_key (a tskey-api-... token) used directly as Authorization: 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-key refuse 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 -K config 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 use id.
  • 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