Tailscale
Authenticate users with their Tailscale identity for zero-trust access to your MCP servers.
Agentgateway can integrate with Tailscale to authenticate users based on
their Tailscale identity, which enables zero-trust access to your MCP servers. Agentgateway uses an
external authorization (extAuthz)
policy to call the local Tailscale daemon’s whois API and identify the user behind each
connection.
How it works
- The client connects from its Tailscale IP address (
100.x.x.x). - Agentgateway calls the Tailscale local
whoisAPI with the source IP address. - Tailscale returns the node and user information.
- Agentgateway allows or denies the request and logs the identity.
Before you begin
- Install agentgateway.
- Install Tailscale and connect it to your tailnet.
- Have another device on your tailnet to test from, or use the same machine through its Tailscale IP.
Step 1: Verify that Tailscale is running
-
Check that Tailscale is connected. You should see your machine listed with a
100.x.x.xIP address.tailscale status -
Note your Tailscale IP address. You use this address to test access later.
tailscale ip -4
Step 2: Configure and start agentgateway
-
Create a
config.yamlfile. The configuration uses anextAuthzpolicy to call the Tailscale daemon’s localwhoisAPI with the source IP address of each request, then extracts the node name and user email from the response. The socket path for the Tailscale daemon differs by platform.# yaml-language-server: $schema=https://agentgateway.dev/schema/configfrontendPolicies:accessLog:add:tailscale.node: extauthz.tailscaleNodetailscale.email: extauthz.tailscaleEmailgateways:default:port: 3000protocol: HTTProutes:- name: applicationbackends:- mcp:targets:- name: everythingstdio:cmd: npxargs: ["@modelcontextprotocol/server-everything"]policies:cors:allowOrigins: ["*"]allowHeaders: ["*"]exposeHeaders: ["Mcp-Session-Id"]extAuthz:host: unix:/run/tailscale/tailscaled.sockprotocol:http:path: |"/localapi/v0/whois?addr=" + source.addressaddRequestHeaders::authority: '"local-tailscaled.sock"'metadata:tailscaleNode: json(response.body).Node.NametailscaleEmail: json(response.body).UserProfile.LoginName# yaml-language-server: $schema=https://agentgateway.dev/schema/configfrontendPolicies:accessLog:add:tailscale.node: extauthz.tailscaleNodetailscale.email: extauthz.tailscaleEmailgateways:default:port: 3000protocol: HTTProutes:- name: applicationbackends:- mcp:targets:- name: everythingstdio:cmd: npxargs: ["@modelcontextprotocol/server-everything"]policies:cors:allowOrigins: ["*"]allowHeaders: ["*"]exposeHeaders: ["Mcp-Session-Id"]extAuthz:host: unix:/var/run/tailscale/tailscaled.sockprotocol:http:path: |"/localapi/v0/whois?addr=" + source.addressaddRequestHeaders::authority: '"local-tailscaled.sock"'metadata:tailscaleNode: json(response.body).Node.NametailscaleEmail: json(response.body).UserProfile.LoginNameReview the following table to understand this configuration.
Setting Description frontendPolicies.accessLog.addAdds the Tailscale identity to the access logs. extAuthz.hostThe Unix socket path to the Tailscale daemon. extAuthz.protocol.http.pathA CEL expression that calls the Tailscale whoisAPI with the client’s source IP address.addRequestHeaders.:authorityThe hostname that the Tailscale local API requires. metadata.tailscaleNodeExtracts the machine name from the Tailscale response. metadata.tailscaleEmailExtracts the user email from the Tailscale response. The value for
extAuthz.hostis the path to the Tailscale daemon’s local socket, which differs by platform. Use the path that matches the platform where agentgateway runs.Platform Socket path Linux /run/tailscale/tailscaled.sockmacOS /var/run/tailscale/tailscaled.sockWindows Named pipe (not supported through a Unix socket) NoteOn most Linux systems, /var/run is a symlink to /run , so /var/run/tailscale/tailscaled.sock and /run/tailscale/tailscaled.sock point to the same socket.
-
Start agentgateway.
agentgateway -f config.yamlExample output:
info state_manager loaded config from File("config.yaml")info app serving UI at http://localhost:15000/uiinfo proxy::gateway started bind bind="bind/3000"
Step 3: Test the authentication
-
Send a request from localhost. Because localhost does not have a Tailscale identity, the request is denied with a
403 Forbiddenresponse.curl -i http://localhost:3000/mcpExample output:
HTTP/1.1 403 Forbiddenexternal authorization failed -
Send a request through your Tailscale IP address. The request passes authentication and reaches the MCP server, which returns a
406 Not Acceptableresponse because the request does not include the requiredtext/event-streamheader.TAILSCALE_IP=$(tailscale ip -4)curl -i http://$TAILSCALE_IP:3000/mcpExample output:
HTTP/1.1 406 Not AcceptableNot Acceptable: Client must accept text/event-stream -
Send a complete MCP request through your Tailscale IP address with the required headers.
TAILSCALE_IP=$(tailscale ip -4)curl -X POST "http://$TAILSCALE_IP:3000/mcp" \-H "Content-Type: application/json" \-H "Accept: text/event-stream" \-d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}' -
Check the agentgateway logs. After a successful request, the logs show the Tailscale identity from the access log fields that you configured.
info request ... tailscale.node=your-machine-name [email protected]