Skip to content
 
 

Repository files navigation

Tailnet authorizer

tailnet-idp exchanges the network identity of an authenticated Tailscale peer for a five-minute RS256 JWT. It is deliberately not an OAuth provider and does not enroll a separate tsnet node.

The production ingress is a Unix socket mounted by Tailscale Serve. The server accepts that socket only when Linux peer credentials identify uid 0; checking the expected tailscaled command and process name adds defense in depth. Root is the security boundary because it can already read the signing key. Tailscaled removes caller-supplied identity headers, overwrites X-Forwarded-For with the authenticated WireGuard peer address, and proxies the request over that socket. Only after verifying the Unix peer does the authorizer use that address with the local tailscaled LocalAPI WhoIs method.

This boundary matters: X-Forwarded-For, Tailscale-User-*, and claimed IPs are not trusted on their own. A direct Unix client, an arbitrary loopback HTTP client, or a request that bypasses the authenticated listener fails closed. Person headers are cross-checked with the LocalAPI result; tagged peers are machine principals and must not carry person headers.

Contract

  • POST /v1/token accepts only JSON { "audience": "linky" }.
  • Browser exchange allows only the configured Linky control origin.
  • JWTs use RS256 and carry iss, aud, sub, iat, nbf, exp, and jti.
  • User-owned peers become person:<stable-user-id> with display name and email.
  • Tagged peers become machine:<stable-node-id> with display name and a truthful machine_identifier.
  • A separate mode-0600 Unix socket exchanges the already-enrolled local machine identity without accepting caller identity fields.
  • /.well-known/openid-configuration and /.well-known/jwks.json publish the stable issuer and public verification key.
  • The RSA private key lives in the configured state directory as a durable mode-0600 file.

There is intentionally no /v1/directory. LocalAPI can authoritatively resolve the peer making the current request, but it does not promise a complete tailnet user directory. Claiming completeness would require separate directory authority, so the product gives up global people search in exchange for having no Tailscale OAuth client, users:read grant, auth-key generation, or tsnet enrollment lifecycle.

Run

The service is normally launched by phosphorco/rosetta-machine:

go build -o tailnet-idp .
./tailnet-idp \
  -state-dir "$HOME/.local/state/rosetta-machine/tailnet-idp" \
  -issuer https://rosetta.example.ts.net \
  -serve-socket "$XDG_RUNTIME_DIR/tailnet-idp/serve.sock" \
  -local-socket "$XDG_RUNTIME_DIR/tailnet-idp/token.sock" \
  -allowed-origin https://linky.example.ts.net

Mount the public socket with an HTTPS Tailscale Serve HTTP proxy. Do not expose it through a generic reverse proxy or direct TCP listener: the verified tailscaled Unix peer is part of the authentication proof.

Test

go test ./...

Tests cover user and machine principals, exact token input, signature and lifetime, durable JWKS state, CORS confinement, missing/ambiguous peer metadata, direct-handler header forgery, and Unix peer uid/executable forgery.

This project retains the BSD-3-Clause license from its original tailscale/tsidp base.

About

A simple OIDC / OAuth Identity Provider (IdP) server for your tailnet.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages