Repository navigation
TOML config file, MCP OAuth and Tailscale exposure - #15
Conversation
Add a TOML config file (--config, $TETHER_CONFIG, or config.toml in the per-user Tether directory) covering binding, auth and per-token rules, allow/deny lists, console capture, the MCP and remote faces, plus settings only the file can express: headers Chrome sends from Tether's tabs, client headers the proxy strips, cookies seeded at startup, and the Chrome profile to launch. Precedence is flag > env > file > default. Add a built-in OAuth 2.1 authorization server for the MCP face (metadata discovery, dynamic registration, PKCE, refresh tokens), so OAuth-only MCP clients can connect. The consent page asks for a Tether token; OAuth tokens act as it and open /mcp only. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2DYcpGzmzj1fmtyBvzsuj
| } | ||
| q.Set("iss", s.base(r)) | ||
| u.RawQuery = q.Encode() | ||
| http.Redirect(w, r, u.String(), http.StatusSeeOther) |
There was a problem hiding this comment.
This redirect is the OAuth authorization response, so it has to go to a client-supplied URI. It isn't an open redirect.
redirect() only runs after parseAuthRequest has matched redirect_uri against the client's registered URIs. That match is exact, except that loopback ports are port-agnostic as RFC 8252 requires. An unknown client or an unregistered URI renders an error page and never redirects. Registered URIs are limited to https, loopback http, or an app scheme (checkRedirectURI). A code is only issued after the person enters a valid Tether token. This is the same constraint OAuth 2.1 §4.1.1 imposes, so I'm leaving the code as is. I suggest dismissing the alert as "won't fix / used by design".
Generated by Claude Code
[tailscale] (and --tailscale / --tailscale-publish) uses the installed tailscale CLI: listen on this machine's tailnet address, and optionally publish over HTTPS with `tailscale serve` or `tailscale funnel`, removed again on shutdown. Publishing sets public_url to the tailnet HTTPS name for MCP OAuth. Both need a token. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2DYcpGzmzj1fmtyBvzsuj
Requested by Adeel · project thread
What
Before: every setting was a flag or
TETHER_*variable, and per-token rules needed a separate JSON policy file. MCP clients that only sign in with OAuth (claude.ai connectors, some IDEs) could not connect. Exposing Tether on a tailnet meant hand-writing--addr "$(tailscale ip -4):8787".After:
tether servealso reads a TOML config file (--config,$TETHER_CONFIG, or~/.config/tether/config.tomlwhen it exists). Precedence is flag > env > file > default. The file covers:[server]addr / insecure_allow_public / public_url,[auth]token / token_env / token_file, plus[[auth.tokens]]with the same per-token rules as--policy-file.--policy-filekeeps working and now also accepts.toml.[policy]allow / deny.[headers].extrais sent by Chrome on every request from Tether's tabs.[headers].striplists client headers the proxy drops.[[cookies]]are seeded into the browser's cookie jar at startup. Values can come from env viavalue_env.[chrome].profile/--chrome-profilepicks a profile inside the user data dir when launching.[console]enabled / buffer.[mcp.oauth]/--mcp-oauthturns on a built-in OAuth 2.1 authorization server. It provides metadata discovery, dynamic registration, mandatory PKCE S256, refresh tokens, and optional pre-registered clients.[tailscale]/--tailscalelistens on this machine's tailnet IP.publish = "serve" | "funnel"(--tailscale-publish) publishes over HTTPS with the installed tailscale CLI and removes the publish again on shutdown. Publishing also setspublic_urlto the*.ts.netname for OAuth. Both options need a token.tether.example.tomlis a full commented example, anddocs/CONFIG.mdis the reference.Why
Adeel asked for a TOML config covering auth, binding, headers, cookies, allow/deny lists, MCP OAuth, Chrome profile, console capture and Tailscale.
How
internal/config/file.godecodes the file strictly: unknown keys are errors. It turns the file into flag defaults that sit below env and explicit flags, and resolves the file-only settings. Relative paths are resolved against the file's directory.internal/oauthkeeps almost no state. Dynamic clients, access tokens and refresh tokens are HMAC-signed with<data_dir>/oauth.key(0600), so they survive restarts. Auth codes are in memory and single use. The consent page asks for a Tether token, and the grant acts as that token's principal. Each grant is bound to a keyed fingerprint of that token, so removing or rotating the token kills the grant.internal/server, the OAuth endpoints are served before auth. OAuth access tokens are accepted under/mcponly. A 401 on/mcpcarriesWWW-Authenticate: Bearer … resource_metadata=….internal/tailnetshells out to the tailscale CLI:status --json,serve|funnel --bg, and… offon shutdown. If the CLI is missing, the bind falls back to scanning the network interfaces. It embeds no Tailscale code, because currenttailscale.com/tsnetrequires a much newer Go than this module targets.isHTTPS/sameOriginmoved tohttpxso the remote UI and the consent page share them.How tested
go vet,golangci-lint run(0 issues),go test -race ./...make test-integrationagainst headless Chromium, including a new test showing configured headers and cookies reach a real Chrome and stripped headers don't./mcp401 points at the metadata, discovery works, andoauth.keyis created.--tailscale-publish serveagainst a stand-in CLI: it publishes on start, removes the publish on shutdown, and the issuer becomes thets.netname.Security
/mcponly, and redirect URIs must be https, loopback http, or an app scheme.--tailscalebinds a non-loopback address, so the existing token rule applies.🤖 Generated with Claude Code
https://claude.ai/code/session_01X2DYcpGzmzj1fmtyBvzsuj