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.
POST /v1/tokenaccepts only JSON{ "audience": "linky" }.- Browser exchange allows only the configured Linky control origin.
- JWTs use RS256 and carry
iss,aud,sub,iat,nbf,exp, andjti. - 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 truthfulmachine_identifier. - A separate mode-0600 Unix socket exchanges the already-enrolled local machine identity without accepting caller identity fields.
/.well-known/openid-configurationand/.well-known/jwks.jsonpublish 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.
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.netMount 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.
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.