agentsclimarketplace

Tailscale ops

Skill Enzojol/claude-homelab-skills/skills/tailscale-ops

Safe Tailscale/tailnet administration: ACL and grants policy editing with validation-before-apply and lockout prevention, connectivity diagnostics (DERP relay, NAT traversal, netcheck), subnet router and exit node troubleshooting, serve vs funnel vs Cloudflare Tunnel decisions. Use for any question about tailscale, headscale, tailnet, ACL policy, grants, exit nodes, subnet routers, MagicDNS, mesh VPN connectivity, or exposing homelab services.From its SKILL.md

Install
npx -y skills add Enzojol/claude-homelab-skills --skill tailscale-ops

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 22 days oldThe repository was created 22 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

9.3 KB, ~2.4k tokens by cl100k_base, as published. Nobody here has run it

tailscale-ops

Judgment layer for Tailscale administration. Raw API access is easy — this skill exists to prevent the two disasters that matter: locking yourself out of your own tailnet and applying a broken policy. Read operations are free. Policy writes follow a strict workflow. No exceptions.

Credentials

All scripts source ~/.config/claude-homelab/credentials.env if it exists, falling back to environment variables. Expected variables:

TAILSCALE_API_KEY=tskey-api-...
TAILSCALE_TAILNET=-        # "-" means the default tailnet of the key
TAILSCALE_API_BASE=        # optional — set for headscale (e.g. https://headscale.example.com)

If credentials are missing or a request returns 401: do not improvise. Show the current state, point the user to /homelab, and stop.

If the user pastes a token into the chat: warn them immediately — the token is now in the conversation history. Tell them to revoke it in the admin console (Settings → Keys), then run bash ~/.claude/skills/tailscale-ops/scripts/setup.sh in their own terminal so the replacement never touches the chat.

API basics

Base URL: https://api.tailscale.com/api/v2. Auth: Authorization: Bearer $TAILSCALE_API_KEY.

ActionEndpoint
Read policy fileGET /tailnet/-/acl (Accept: application/hujson)
Validate policyPOST /tailnet/-/acl/validate
Apply policyPOST /tailnet/-/acl (If-Match: "<etag>")
List devicesGET /tailnet/-/devices
Device routesGET /device/{id}/routes
Approve routesPOST /device/{id}/routes

- in paths means "default tailnet for this key" — always usable.

Policy modification workflow (MANDATORY)

Every change to the tailnet policy file (ACLs, grants, tags, autoApprovers, ssh, anything) follows these five steps in order. Never skip, never reorder.

  1. Read current policy: GET /tailnet/-/acl with header Accept: application/hujson to preserve comments. Save the ETag response header.
  2. Propose the diff: show the user a unified diff between current and proposed policy — never just the new file. Explain in one line per hunk what changes and why.
  3. Validate: POST /tailnet/-/acl/validate with the full proposed file. Or run scripts/validate-policy.sh <file>. A 200 with empty body or {"message":""} means valid. Any message content = validation error — show it verbatim and fix before continuing.
  4. Anti-lockout check (see below). If it fails or cannot be verified, refuse to apply.
  5. Apply only after explicit user confirmation — the user must say yes to the diff, not to a summary. Apply with If-Match: "<etag>" from step 1 so a concurrent edit fails loudly instead of being clobbered.

tests and sshTests sections in the policy run at validate time. When adding or changing ACL rules, add a tests entry asserting the access that must keep working — validation then guards it forever.

Anti-lockout rule

Before applying ANY policy, verify the admin keeps access:

  1. Identify the admin's devices: GET /tailnet/-/devices, match on the admin's login name.
  2. Check the proposed policy still contains at least one rule (ACL or grant) whose src matches the admin user (directly, via group, or via *) with a dst that is not empty.
  3. Strongest check: add a tests block to the proposed policy asserting the admin's access (e.g. {"src": "[email protected]", "accept": ["tag:server:22"]}) and let /acl/validate prove it.
  4. If in doubt, refuse to apply. Say exactly what could not be verified. A rejected apply costs a minute; a lockout costs the tailnet.

Special case: policies where "acls": [] and "grants": [] (deny-all) or removal of the admin from all groups — refuse outright, require the user to type the confirmation phrase "I understand this may lock me out".

Runbook: connectivity diagnosis

When "X can't reach Y" / "it's slow" / "ping fails" — run in this exact order (or scripts/diagnose.sh [peer] which automates 1–3):

  1. tailscale status --json — check Self.Online, find the peer, check its Online, CurAddr (empty = no direct connection, using DERP), Relay (which DERP region).
  2. tailscale netcheck — read UDP: true|false, MappingVariesByDestIP (true = hard NAT), nearest DERP latency, PortMapping (UPnP/NAT-PMP/PCP available?).
  3. tailscale ping <peer> — up to ~10 probes; watch whether it upgrades from via DERP(xxx) to a direct via <ip>:<port> path.
  4. Interpret:
    • Peer offline → not a network problem. Check the peer machine, key expiry (Expired in status JSON), tailscale up on the peer.
    • DERP relay, never direct → both sides behind hard NAT / blocked UDP. Remediation order: (a) allow outbound UDP 41641 or open port 41641/udp on one side, (b) enable UPnP/NAT-PMP on one router, (c) accept DERP — it works, just adds latency.
    • UDP: false in netcheck → firewall blocks all UDP; even DERP over UDP fails, falls back to HTTPS DERP. Fix the firewall first, nothing else matters until UDP works.
    • Reachable but wrong service → not connectivity; check ACL policy (does a rule allow that port?) and host firewall on the peer.
    • ACL suspected → read the policy, check src/dst match; ask the user to test with tailscale ping (layer 3, ACL-gated) vs regular ping of the Tailscale IP.

Runbook: subnet router not routing

In order:

  1. Advertised vs approved: GET /device/{id}/routes — routes appear in advertisedRoutes but not enabledRoutes = not approved. Approve in admin console (Machines → route settings) or POST /device/{id}/routes with the routes to enable. Check autoApprovers in policy for future routes.
  2. IP forwarding on the router host (the #1 cause): sysctl net.ipv4.ip_forward and net.ipv6.conf.all.forwarding must be 1. Fix: echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf && sudo sysctl -p /etc/sysctl.d/99-tailscale.conf.
  3. Client side: Linux clients need --accept-routes; check with tailscale status --json (Self has no route info — check tailscale debug prefs | grep RouteAll).
  4. Route conflicts: the advertised subnet must not overlap the client's own LAN (two 192.168.1.0/24 = broken; renumber or use 4via6). Also check the ACL: dst must allow the subnet CIDR, not just tailnet IPs — autogroup:internet does NOT cover private subnets.
  5. Firewall on the router host: FORWARD chain must accept, and masquerading is handled by tailscaled unless --snat-subnet-routes=false was set (then the LAN needs a return route).

Decision matrix: serve vs funnel vs Cloudflare Tunnel

Criteriontailscale servetailscale funnelCloudflare Tunnel
Audiencetailnet onlypublic internetpublic internet
Auth built intailnet identitynone (public)Cloudflare Access optional
WAF / caching / DDoSn/a (private)noneyes
Custom domainno (ts.net)no (ts.net)yes
Portsany443, 8443, 10000 onlyany (via tunnel)
Setup costone commandone command + DNS wait (~10 min)cloudflared install + CF account

Concrete rules:

  • Internal dashboard, another tailnet user needs it → serve.
  • Quick public demo, webhook receiver, short-lived share → funnel.
  • Production public site, needs custom domain, caching, WAF, or protection from scrapers → Cloudflare Tunnel.
  • Never funnel an unauthenticated admin UI (Proxmox, router, NAS). If it must be public, put Cloudflare Access or authentication in front.

Details and full CLI syntax: references/serve-funnel.md.

References

  • references/policy-syntax.md — full HuJSON policy file syntax, ACLs AND grants (pinned from official docs; retrieval date in header).
  • references/acl-examples.md — official ACL examples.
  • references/grant-examples.md — official grants examples.
  • references/serve-funnel.md — serve + funnel docs.

Consult references before writing policy from memory — syntax evolves (grants notably). If a reference contradicts memory, the reference wins.

Scripts

  • scripts/validate-policy.sh <file> — validates a policy file against the API, human-readable verdict.
  • scripts/diagnose.sh [peer] — status + netcheck (+ ping if peer given), parsed into a structured diagnosis with probable cause.
  • scripts/setup.sh — credential onboarding; run by the USER in their own terminal, never by Claude.

Test rules (when operating on a real tailnet)

  • Read operations: always allowed.
  • NEVER apply a policy change to the real tailnet without the full 5-step workflow AND explicit confirmation on the diff.
  • For testing policy changes, /acl/validate is sufficient — it never modifies anything.

What ships with it: 7 files

188.5 KB alongside SKILL.md, 3 of them executable

scripts/

Keep looking

Skills are one crate of 326,852. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.