172 lines
9.2 KiB
Markdown
172 lines
9.2 KiB
Markdown
---
|
|
name: tailscale
|
|
description: "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
|
|
|
|
```bash
|
|
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
|
|
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
|
|
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):
|
|
|
|
```json
|
|
{"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
|
|
|
|
- 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
|