Docs
Check your own tailnet, from any door.
Geoa Tailnet Guard is one service with four doors: this site, a read-only CLI, a JSON API and an MCP server your coding agent can call. Same findings, same verdicts, same review-only fix pack.
01 Connect your agent
The MCP server lives at https://tailnet.geoa.io/mcp (Streamable HTTP, no login). It exposes exactly 8 tools: list_findings, explain_finding, audit_status_json, audit_policy, audit_node_report, propose_fix, patch_status, watch_tailnet. All are read-only except watch_tailnet, which stores a watch.
Claude Code
claude mcp add --transport http geoa-tailnet https://tailnet.geoa.io/mcpbash <(curl -s https://tailnet.geoa.io/plugin/install.sh)Then ask /geoa-tailnet TW-01, /geoa-tailnet check or /geoa-tailnet fix.
Cursor
{
"mcpServers": {
"geoa-tailnet": {
"url": "https://tailnet.geoa.io/mcp"
}
}
}Project-level file; use ~/.cursor/mcp.json for every project. Cursor lists the 8 tools under MCP in settings.
Codex
[mcp_servers.geoa-tailnet]
url = "https://tailnet.geoa.io/mcp"Restart Codex; /mcp shows geoa-tailnet with its tools.
Any agent
Set up the Geoa Tailnet Guard MCP server for me: name geoa-tailnet, transport Streamable HTTP, URL https://tailnet.geoa.io/mcp, no login. Confirm it lists the tools list_findings, explain_finding, audit_status_json, audit_policy, audit_node_report, propose_fix, patch_status, watch_tailnet, then explain finding TW-01 with its source citation.Works with any MCP client that speaks Streamable HTTP. Tools: list_findings, explain_finding, audit_status_json, audit_policy, audit_node_report, propose_fix, patch_status, watch_tailnet.
02 Claude Code
- Add the server (once per machine; --scope user for every project):
claude mcp add --transport http geoa-tailnet https://tailnet.geoa.io/mcp - Or install the /geoa-tailnet slash command and the skill, which also adds the server. It copies geoa-tailnet.md to ~/.claude/commands/ and the skill to ~/.claude/skills/geoa-tailnet/; --project DIR installs for one project, --dry-run shows what it would do, --uninstall removes it.
bash <(curl -s https://tailnet.geoa.io/plugin/install.sh) - Check it: claude mcp list shows geoa-tailnet: https://tailnet.geoa.io/mcp (HTTP) - ✔ Connected.
Example prompts
- /geoa-tailnet TW-01 · one finding: when it applies, the mitigation, the cited source line, patch status.
- /geoa-tailnet check · runs tailscale status --json (read-only) and audits it.
- /geoa-tailnet policy ./policy.hujson · audits your exported policy file.
- /geoa-tailnet fix · the review-only fix pack: policy diff, node commands, admin checklist, Tailnet Lock plan.
- /geoa-tailnet patches · which findings have a vendor fix (only when a release or bulletin names it).
- "Watch TW-01 and TW-07 with geoa-tailnet and give me the feed URL." (watch_tailnet)
Policy text and node names reach the agent inside [BEGIN UNTRUSTED ...] blocks: data to summarise, never instructions to follow. The command and the skill never run tailscale debug prefs and never ask for keys.
03 Cursor
Create .cursor/mcp.json in the project (or ~/.cursor/mcp.json for every project) with the JSON under Connect, then open Settings, MCP and enable geoa-tailnet. Ask: "Use geoa-tailnet to explain TW-06 and tell me whether it has a fix."
04 Codex
Add the [mcp_servers.geoa-tailnet] table from Connect to ~/.codex/config.toml and restart. /mcp inside Codex lists the server and its tools.
05 Any agent
Any MCP client that speaks Streamable HTTP works. If your agent can edit its own configuration, paste this:
Set up the Geoa Tailnet Guard MCP server for me: name geoa-tailnet, transport Streamable HTTP, URL https://tailnet.geoa.io/mcp, no login. Confirm it lists the tools list_findings, explain_finding, audit_status_json, audit_policy, audit_node_report, propose_fix, patch_status, watch_tailnet, then explain finding TW-01 with its source citation.06 The read-only CLI
One POSIX shell script, run on the node you want to check. It runs only read-only tailscale commands (version, version --daemon, serve status, funnel status, lock status, status and the preferences dump), reduces their output to booleans and counts on your machine and prints a report. It never changes Tailscale state, never writes a file and never prints keys, auth keys, login names or raw command output. Node names are left out unless you pass --include-names.
curl -fsS https://tailnet.geoa.io/cli/geoa-tailnet.sh | sh -s -- --jsoncurl -fsS https://tailnet.geoa.io/cli/geoa-tailnet.sh | sh -s -- --submit https://tailnet.geoa.io/api/node-reportNothing is sent anywhere unless you pass --submit URL (https, or http to 127.0.0.1 / localhost for a local preview). Other flags: --stale-days N (default 30), --env-file PATH (where the two tailscaled environment knobs live), --help. Read the script before piping it to a shell: /cli/geoa-tailnet.sh.
07 The three inputs
- Status: the output of tailscale status --json from any node. Gives node count, online and offline nodes, stale nodes, Tailscale SSH per node, key expiry, versions and the control plane.
- Policy file: your tailnet policy (HuJSON: comments and trailing commas allowed), read against the policy file reference. Only the documented sections the findings need are read: ssh, acls, grants, groups, tagOwners, autoApprovers, nodeAttrs. No field is invented: the per-rule forwarding keys the report mentions are shown as present but unverified, never counted as a control. Export your policy file: admin console -> Access controls (https://console.tailscale.com/admin/acls) -> JSON editor -> select all, copy, save as policy.hujson, then paste it into the policy box or run the audit again with it.
- Node report: the JSON printed by the CLI above, for node-level facts (Tailscale SSH server, the forwarding knob, Serve and Funnel forwards, app connector, Tailnet Lock, client and daemon versions).
Or, with your own API access token in the environment (never pasted here): curl -s -H "Authorization: Bearer $TS_API_KEY" -H "Accept: application/hujson" https://api.tailscale.com/api/v2/tailnet/-/acl > policy.hujsonA finding that needs an input you did not give is UNKNOWN, and the verdict names the missing input. Reading the policy through the Tailscale API with a key you hand to this service is planned for v2; this version never asks for an API key.
08 JSON API
Plain JSON, no account, no API key. Errors are { "error": "..." } with 400 (bad input), 401/403 (manage token), 404, 413 (too large), 415 (not JSON), 429 (rate limited, with retry-after).
POST /api/audit
Audit any mix of the three inputs. Send each as text (as pasted) or as a JSON value. Returns the ranked verdicts and the review-only fix pack. Nothing is stored.
curl -s -X POST -H 'content-type: application/json' -d '{"status":"<tailscale status --json>","policy":"<policy.hujson>","node":"<CLI --json output>","stale_days":30}' https://tailnet.geoa.io/api/auditReturns { audit:{ generated_at, inputs, counts, headline, findings:[{ id, title, severity, verdict, because, missing_inputs, fix }], status, policy, node }, fix_pack:{ review_only, priorities, policy, node_steps, admin_checklist, tailnet_lock, headscale } }
POST /api/node-report
What the CLI sends with --submit: one node report as the body. Plain text back unless you ask for JSON. ?save=1 also stores a proof report and returns its link and manage token.
curl -s -X POST -H 'content-type: application/json' -H 'accept: application/json' -d '<CLI --json output>' https://tailnet.geoa.io/api/node-reportReturns same as /api/audit; with ?save=1 also report:{ id, url, manage_token, expires_at }
POST /api/report
Run an audit and store a shareable proof report of its RESULT (never the raw inputs). include_names: true keeps node names, tags and SSH rules; the default leaves them out.
curl -s -X POST -H 'content-type: application/json' -d '{"status":"...","policy":"...","label":"home tailnet","include_names":false}' https://tailnet.geoa.io/api/reportReturns { id, url, manage_token, expires_at, note, headline, counts }
GET /api/report/:id · GET /r/:id
The report as JSON and as a page: Checked, Fixed, Verified per finding.
curl -s https://tailnet.geoa.io/api/report/r_xxxxxxxx.signatureReturns { id, url, label, created_at, updated_at, expires_at, signature, report, states }
POST /api/report/:id/fixed
Owner only: mark a finding fixed (or fixed:false to clear the mark).
curl -s -X POST -H 'content-type: application/json' -H 'Authorization: Bearer mt_...' -d '{"finding":"TW-01","fixed":true}' https://tailnet.geoa.io/api/report/r_xxxxxxxx.signature/fixedReturns { ok, states }
POST /api/report/:id/verify
Owner only: re-audit on fresh inputs. The Verified column comes only from this.
curl -s -X POST -H 'content-type: application/json' -H 'Authorization: Bearer mt_...' -d '{"status":"...","policy":"..."}' https://tailnet.geoa.io/api/report/r_xxxxxxxx.signature/verifyReturns { ok, verification:{ at, headline, counts, findings:{ "TW-01":{ before, verdict, result } } } }
DELETE /api/report/:id
Owner only: delete the report.
curl -s -X DELETE -H 'Authorization: Bearer mt_...' https://tailnet.geoa.io/api/report/r_xxxxxxxx.signatureReturns 204
GET /api/patch-status
Vendor patch status per finding (?finding=TW-06 for one).
curl -s https://tailnet.geoa.io/api/patch-status?finding=TW-06Returns { last_check, sources, watched, rule, kb_checked_at, findings:[{ id, title, product, status, fixed_version, evidence }] }
POST /api/watch
Watch findings for vendor fixes: a private RSS feed and an optional webhook.
curl -s -X POST -H 'content-type: application/json' -d '{"findings":["TW-01","TW-07"],"webhook":"https://hooks.example.com/geoa","label":"home"}' https://tailnet.geoa.io/api/watchReturns { id, feed_url, findings, label, manage_token, webhook_secret?, expires_at, note }
GET · DELETE /api/watch/:id
Owner only: read or stop a watch.
curl -s -H 'Authorization: Bearer mt_...' https://tailnet.geoa.io/api/watch/w_xxxxxxxx.signatureReturns { id, label, findings, channels, alerts, feed_url }
GET /healthz
Liveness, number of findings, mounted modules.
curl -s https://tailnet.geoa.io/healthzReturns { ok, service, version, mode, offline, findings: 11, fixed, lastPatchCheck, modules }
09 Proof reports
A proof report records one audit as three columns per finding: Checked (the verdict this service computed), Fixed (your own mark) and Verified (a later re-audit on fresh inputs). Create one from the result page, with POST /api/report, or with the CLI's --submit https://tailnet.geoa.io/api/node-report?save=1.
- The report stores the audit result (verdicts, counts, versions), never your raw status, policy or node report. Names, tags and SSH rules are left out unless you set include_names.
- The link carries a signed id; the content is signed too, and a report changed outside the service is not shown.
- The manage token (mt_...) is shown once. Send it as Authorization: Bearer mt_... to mark fixes, record a re-audit or delete the report. It is stored only as a hash.
- Reports expire 180 days after their last change. The pages are noindex and send no referrer.
10 Feeds and watches
- https://tailnet.geoa.io/feed.xml · patch status of all 11 findings plus every vendor release or bulletin event the watcher saw (RSS 2.0).
- https://tailnet.geoa.io/feed/watch/<watch id>.xml · a private feed for the findings you watch (POST /api/watch or the
watch_tailnettool). - Webhook: one JSON POST per alert, signed with the webhook secret (shown once): x-geoa-signature: sha256=<hmac of the body>, plus x-geoa-event and x-geoa-delivery. Public https endpoints only; one attempt, no retries.
The watcher reads Tailscale releases, headscale releases and the Tailscale security bulletins. A finding flips to fixed only when a release or bulletin names it (Tailworm, the finding number or its CVE); a release that does not name it is listed as an event and changes nothing. Watches expire 180 days after their feed was last read.
11 How verdicts are ranked
AFFECTED: every condition the finding needs is present in your inputs. CONDITIONAL: the main condition is present and a second one cannot be ruled out from what you gave. UNKNOWN: an input the finding depends on is missing; the verdict names it. NOT AFFECTED: a required condition is absent. Results are ranked in that order, then by severity.
Every condition and mitigation cites the Tailworm report by file and line, or an official Tailscale or headscale document or source file. Where the report and Tailscale disagree, both are shown side by side and labelled, with Geoa's reading after them.
12 Sources
- Tailworm report: Security Assessment Report: Tailscale v1.104.2 and headscale v0.29.5 (Tailworm, Cyberdione Labs, published 24 to 25 Oct 2026). Tested versions: Tailscale 1.104.2, headscale 0.29.5. Only its conditions and mitigations are encoded here; nothing from its reproduction sections.
- Vendor response: As of 2026-10-25 Tailscale has published no security bulletin, release note or changelog entry that acknowledges or fixes the Tailworm findings. The latest client is 1.104.2, the version the report tested.
- Earlier bulletins: TS-2026-004 (2026-06-03, SSH Unix socket forwarding did not respect symlink permissions, fixed in 1.98.10); TS-2026-005 (2026-06-03, Serve Unix socket proxy targets not restricted to root, fixed in 1.98.9).
- Official documentation: policy file, grants, Tailscale SSH, serve, app connectors, Tailnet Lock, key expiry.
13 Privacy
Pasted status, policy and node reports are processed in memory and dropped when the response is sent. Nothing is stored unless you create a proof report or a watch, and then only the result described above. No API keys, auth keys or private keys are accepted, stored or logged; the CLI never reads or prints them. Request logs (debug level only) hold the method, path, status and timing, never bodies or query strings. There are no cookies, no analytics and no third-party requests: every asset is served from this origin.
14 Limits
- Request body: 6 MB; status paste: 4 MB; policy file: 1024 KB; node report: 256 KB. JSON and HuJSON nesting and node counts are capped too.
- Rate limits per client address: 120 requests per minute overall; 20 per minute for audits, reports and watches (also through MCP). Over the limit: 429 with retry-after.
- Stored reports and watches: 10 reports and 10 watches per client address.
- MCP: 8 sessions per client address; idle sessions close after 30 minutes and do not survive a restart.
15 Honest notes
- Review-only means review-only: nothing here changes your tailnet. You apply the fix pack yourself, in the admin console and on your nodes.
- NOT AFFECTED covers the 11 Tailworm findings and the inputs you gave. It is not a general security review of your tailnet.
- Patch status is only as good as the vendor's wording: a fix shipped without naming the finding stays "no fix published" until a release, bulletin or changelog names it.
- This instance runs in public mode. Geoa Tailnet Guard is not affiliated with Tailscale Inc.; "Tailscale" and "Tailworm" are used descriptively.