GeoaTailnet Guard

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

Add the MCP server
claude mcp add --transport http geoa-tailnet https://tailnet.geoa.io/mcp
Install the /geoa-tailnet slash command and skill
bash <(curl -s https://tailnet.geoa.io/plugin/install.sh)

Then ask /geoa-tailnet TW-01, /geoa-tailnet check or /geoa-tailnet fix.

Cursor

.cursor/mcp.json
{
  "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

~/.codex/config.toml
[mcp_servers.geoa-tailnet]
url = "https://tailnet.geoa.io/mcp"

Restart Codex; /mcp shows geoa-tailnet with its tools.

Any agent

Paste this prompt into your 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

  1. Add the server (once per machine; --scope user for every project):
    claude mcp add --transport http geoa-tailnet https://tailnet.geoa.io/mcp
  2. 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)
  3. Check it: claude mcp list shows geoa-tailnet: https://tailnet.geoa.io/mcp (HTTP) - ✔ Connected.

Example prompts

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:

Prompt
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.

Run it and print JSON
curl -fsS https://tailnet.geoa.io/cli/geoa-tailnet.sh | sh -s -- --json
Opt in: also send the report here and get the verdicts back
curl -fsS https://tailnet.geoa.io/cli/geoa-tailnet.sh | sh -s -- --submit https://tailnet.geoa.io/api/node-report

Nothing 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

Export the policy with your own API access token (stays on your machine)
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.hujson

A 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
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/audit

Returns { 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
curl -s -X POST -H 'content-type: application/json' -H 'accept: application/json' -d '<CLI --json output>' https://tailnet.geoa.io/api/node-report

Returns 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
curl -s -X POST -H 'content-type: application/json' -d '{"status":"...","policy":"...","label":"home tailnet","include_names":false}' https://tailnet.geoa.io/api/report

Returns { 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
curl -s https://tailnet.geoa.io/api/report/r_xxxxxxxx.signature

Returns { 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
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/fixed

Returns { ok, states }

POST /api/report/:id/verify

Owner only: re-audit on fresh inputs. The Verified column comes only from this.

curl
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/verify

Returns { ok, verification:{ at, headline, counts, findings:{ "TW-01":{ before, verdict, result } } } }

DELETE /api/report/:id

Owner only: delete the report.

curl
curl -s -X DELETE -H 'Authorization: Bearer mt_...' https://tailnet.geoa.io/api/report/r_xxxxxxxx.signature

Returns 204

GET /api/patch-status

Vendor patch status per finding (?finding=TW-06 for one).

curl
curl -s https://tailnet.geoa.io/api/patch-status?finding=TW-06

Returns { 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
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/watch

Returns { 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
curl -s -H 'Authorization: Bearer mt_...' https://tailnet.geoa.io/api/watch/w_xxxxxxxx.signature

Returns { id, label, findings, channels, alerts, feed_url }

GET /healthz

Liveness, number of findings, mounted modules.

curl
curl -s https://tailnet.geoa.io/healthz

Returns { 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.

10 Feeds and watches

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

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

15 Honest notes