{"slug":"how-aperture-works","title":"How Aperture works","tags":["tailscale"],"agent_summary":"Last validated: Apr 9, 2026","trigger_phrases":[],"runnable":false,"markdown":"\r\n# How Aperture works\r\n\r\nLast validated: Apr 9, 2026\r\n\r\nAperture by Tailscaleis currently [in alpha](https://tailscale.com/docs/reference/tailscale-release-stages#alpha).\r\n\r\nAperture routes all AI requests through a central proxy, giving organizations consistent visibility into AI usage. This page explains the four core mechanisms: identity and authentication, request routing by model, telemetry capture, and session tracking. Together, these enable auditing, cost awareness, and operational insight while letting teams safely adopt LLM clients.\r\n\r\n## [Identity and authentication](https://tailscale.com/docs/aperture/how-aperture-works\\#identity-and-authentication)\r\n\r\nTraditional API proxies require clients to authenticate with tokens or API keys. Aperture eliminates this step by using [Tailscale's identity layer](https://tailscale.com/docs/concepts/tailscale-identity).\r\n\r\nEvery connection to a tailnet carries cryptographic proof of identity. When a request arrives at Aperture, the proxy queries Tailscale with the remote IP address. Tailscale responds with the user's login name (for example, `alice@example.com`), a persistent device identifier, and any tags assigned to that device.\r\n\r\nThis identity is trustworthy because it comes from Tailscale's control plane, not from the client. A user cannot forge their identity without compromising Tailscale's key exchange.\r\n\r\nThe identity flows through the entire system. Every metric record includes the login name and device ID. The dashboard filters by user. Access control uses roles derived from Tailscale tags.\r\n\r\nClients connecting using [ts-unplug](https://github.com/tailscale/ts-plug) also authenticate through Tailscale identity. Each ts-unplug instance joins the tailnet as its own device, so Aperture identifies requests by the ts-unplug device's login name. If each person runs their own ts-unplug instance, Aperture attributes activity to individual users. If multiple people share a single ts-unplug instance, all activity appears under the same identity.\r\n\r\n### [Tagged device identity](https://tailscale.com/docs/aperture/how-aperture-works\\#tagged-device-identity)\r\n\r\nWhen a [tagged Tailscale device](https://tailscale.com/docs/features/tags) connects to Aperture, it does not have a user account associated with it. Instead, Aperture creates a synthetic identity from the device's tags.\r\n\r\nAperture sorts the device's tags alphabetically and joins them with commas to create a stable login name. For example, a device with the tags `tag:prod` and `tag:api` appears as `tag:api,tag:prod` in dashboards, logs, and session tracking.\r\n\r\nIf a device has no user profile and no tags, identity resolution fails, and the device is denied access.\r\n\r\nTo grant access to a tagged device, use this synthetic identity in the grant's `src` field (this applies to both `grants` and `temp_grants` — they use the same `src` JSON field with identical matching behavior):\r\n\r\n- A single-tagged device with `tag:ci-runner`:\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n```json\r\n\"src\": [\"tag:ci-runner\"]\r\n```\r\n\r\n- A multi-tagged device with `tag:api` and `tag:prod`:\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n```json\r\n\"src\": [\"tag:api,tag:prod\"]\r\n```\r\n\r\n- A wildcard that matches all nodes, including tagged nodes:\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n```json\r\n\"src\": [\"*\"]\r\n```\r\n\r\n\r\n**Finding the correct tag string**: To discover the exact tag string for a node, you can:\r\n\r\n1. Temporarily set `src: [\"*\"]` to allow the node to connect, then observe the synthetic identity in Aperture's dashboards and tighten grants to the exact tag string.\r\n2. Construct it manually by sorting the node's tags alphabetically and joining them with commas.\r\n3. Find the node's tags in the Tailscale admin console.\r\n\r\nIf all sessions from tagged nodes appear to come from the same user, refer to [all sessions appear to come from the same user](https://tailscale.com/docs/aperture/troubleshooting#all-sessions-appear-to-come-from-the-same-user) in the troubleshooting guide.\r\n\r\n## [Request routing by model](https://tailscale.com/docs/aperture/how-aperture-works\\#request-routing-by-model)\r\n\r\nWhen a request arrives, Aperture extracts the model name from the request body (for example, `claude-sonnet-4-20250514` or `gpt-4o`). The proxy looks up which provider serves that model and forwards the request to that provider's API endpoint, injecting the correct authentication headers.\r\n\r\nFrom the client's perspective, the proxy appears as if it were the LLM provider itself. Clients connect to the proxy URL and send standard API requests. The proxy handles the routing transparently.\r\n\r\nThe following table summarizes the supported API formats. For full provider details, including compatibility flags, see the [provider compatibility reference](https://tailscale.com/docs/aperture/provider-compatibility).\r\n\r\n| Format | Endpoint | Providers |\r\n| --- | --- | --- |\r\n| OpenAI Chat | `POST /v1/chat/completions` | OpenAI, OpenRouter, llama.cpp |\r\n| OpenAI Responses | `POST /v1/responses` | OpenAI |\r\n| Anthropic Messages | `POST /v1/messages` | Anthropic |\r\n| Gemini | `POST /v1beta/models/{model}:generateContent` | Google |\r\n| Amazon Bedrock | `POST /bedrock/model/{model}/invoke` | Amazon Bedrock |\r\n| Vertex AI | `POST /v1/projects/{project}/locations/{region}/publishers/{publisher}/models/{model}` | Google Vertex AI |\r\n\r\n## [Telemetry capture](https://tailscale.com/docs/aperture/how-aperture-works\\#telemetry-capture)\r\n\r\nThe capture system records everything needed to reconstruct and analyze each LLM interaction:\r\n\r\n- **Request data**: HTTP method, path, headers (with sensitive values redacted), and full request body.\r\n- **Response data**: Status code, headers, and full response body.\r\n- **Extracted metrics**: Token counts by type (input, output, cached, and reasoning), model name, request duration, and tool use count.\r\n- **Session context**: Session ID linking related requests with supported providers and agents such as Claude Code and Codex.\r\n\r\nThe proxy processes telemetry asynchronously after the response completes, so clients receive responses without significant delay.\r\n\r\nThe proxy handles two technical challenges transparently:\r\n\r\n- **Compression**: The proxy decompresses response bodies that arrive compressed (`gzip`, `deflate`, or Brotli) before storing them.\r\n- **Streaming**: For streaming responses (Server-Sent Events), the proxy reconstructs a complete response object from the event stream for consistent metric extraction.\r\n\r\n## [Session tracking](https://tailscale.com/docs/aperture/how-aperture-works\\#session-tracking)\r\n\r\nA single coding task or conversation typically involves many LLM requests. A developer using Claude Code can generate 50 or more requests while debugging one issue. Without grouping, these appear as 50 unrelated events. With session tracking, they form a coherent unit you can analyze as a whole.\r\n\r\nAperture groups related requests into sessions by detecting session identifiers from different client types. The following table describes how each client type identifies sessions:\r\n\r\n| Client type | Identification method |\r\n| --- | --- |\r\n| Claude Code | Session ID included in each request body |\r\n| OpenAI Codex | Session ID sent as HTTP header |\r\n| OpenAI Chat | Fingerprint generated from conversation content |\r\n| Other clients | Random identifier assigned |\r\n\r\nSessions enable conversation-level analysis. The [**Logs** page](https://tailscale.com/docs/aperture/reference/dashboard) of the Aperture dashboard groups requests by session, showing the full context of a coding session or chat conversation. You can review token costs per conversation rather than per request, trace how a coding assistant deconstructed a task, or identify which conversation consumed the most resources.\r\n\r\n## [Next steps](https://tailscale.com/docs/aperture/how-aperture-works\\#next-steps)\r\n\r\n- [Get started with Aperture](https://tailscale.com/docs/aperture/get-started): sign up, configure providers, and connect your first LLM client.\r\n- [Control AI access](https://tailscale.com/docs/aperture/control-access): set up grants that define which models each user can access.\r\n- [Observe and export AI usage](https://tailscale.com/docs/aperture/observe-and-export): access dashboards and export usage data.\r\n\r\n![Project Logo](https://cdn.brandfetch.io/tailscale.com/fallback/lettermark/theme/dark/h/256/w/256/icon?c=1bfwsmEH20zzEfSNTed)\r\n\r\nAsk AI\r\n\r\nreCAPTCHA\r\n\r\nRecaptcha requires verification.\r\n\r\nprotected by **reCAPTCHA**\r\n","html":"<h1>How Aperture works</h1>\n<p>Last validated: Apr 9, 2026</p>\n<p>Aperture by Tailscaleis currently <a href=\"https://tailscale.com/docs/reference/tailscale-release-stages#alpha\">in alpha</a>.</p>\n<p>Aperture routes all AI requests through a central proxy, giving organizations consistent visibility into AI usage. This page explains the four core mechanisms: identity and authentication, request routing by model, telemetry capture, and session tracking. Together, these enable auditing, cost awareness, and operational insight while letting teams safely adopt LLM clients.</p>\n<h2><a href=\"https://tailscale.com/docs/aperture/how-aperture-works#identity-and-authentication\">Identity and authentication</a></h2>\n<p>Traditional API proxies require clients to authenticate with tokens or API keys. Aperture eliminates this step by using <a href=\"https://tailscale.com/docs/concepts/tailscale-identity\">Tailscale's identity layer</a>.</p>\n<p>Every connection to a tailnet carries cryptographic proof of identity. When a request arrives at Aperture, the proxy queries Tailscale with the remote IP address. Tailscale responds with the user's login name (for example, <code>alice@example.com</code>), a persistent device identifier, and any tags assigned to that device.</p>\n<p>This identity is trustworthy because it comes from Tailscale's control plane, not from the client. A user cannot forge their identity without compromising Tailscale's key exchange.</p>\n<p>The identity flows through the entire system. Every metric record includes the login name and device ID. The dashboard filters by user. Access control uses roles derived from Tailscale tags.</p>\n<p>Clients connecting using <a href=\"https://github.com/tailscale/ts-plug\">ts-unplug</a> also authenticate through Tailscale identity. Each ts-unplug instance joins the tailnet as its own device, so Aperture identifies requests by the ts-unplug device's login name. If each person runs their own ts-unplug instance, Aperture attributes activity to individual users. If multiple people share a single ts-unplug instance, all activity appears under the same identity.</p>\n<h3><a href=\"https://tailscale.com/docs/aperture/how-aperture-works#tagged-device-identity\">Tagged device identity</a></h3>\n<p>When a <a href=\"https://tailscale.com/docs/features/tags\">tagged Tailscale device</a> connects to Aperture, it does not have a user account associated with it. Instead, Aperture creates a synthetic identity from the device's tags.</p>\n<p>Aperture sorts the device's tags alphabetically and joins them with commas to create a stable login name. For example, a device with the tags <code>tag:prod</code> and <code>tag:api</code> appears as <code>tag:api,tag:prod</code> in dashboards, logs, and session tracking.</p>\n<p>If a device has no user profile and no tags, identity resolution fails, and the device is denied access.</p>\n<p>To grant access to a tagged device, use this synthetic identity in the grant's <code>src</code> field (this applies to both <code>grants</code> and <code>temp_grants</code> — they use the same <code>src</code> JSON field with identical matching behavior):</p>\n<ul>\n<li>A single-tagged device with <code>tag:ci-runner</code>:</li>\n</ul>\n<pre><code class=\"language-json\">\"src\": [\"tag:ci-runner\"]\n</code></pre>\n<ul>\n<li>A multi-tagged device with <code>tag:api</code> and <code>tag:prod</code>:</li>\n</ul>\n<pre><code class=\"language-json\">\"src\": [\"tag:api,tag:prod\"]\n</code></pre>\n<ul>\n<li>A wildcard that matches all nodes, including tagged nodes:</li>\n</ul>\n<pre><code class=\"language-json\">\"src\": [\"*\"]\n</code></pre>\n<p><strong>Finding the correct tag string</strong>: To discover the exact tag string for a node, you can:</p>\n<ol>\n<li>Temporarily set <code>src: [\"*\"]</code> to allow the node to connect, then observe the synthetic identity in Aperture's dashboards and tighten grants to the exact tag string.</li>\n<li>Construct it manually by sorting the node's tags alphabetically and joining them with commas.</li>\n<li>Find the node's tags in the Tailscale admin console.</li>\n</ol>\n<p>If all sessions from tagged nodes appear to come from the same user, refer to <a href=\"https://tailscale.com/docs/aperture/troubleshooting#all-sessions-appear-to-come-from-the-same-user\">all sessions appear to come from the same user</a> in the troubleshooting guide.</p>\n<h2><a href=\"https://tailscale.com/docs/aperture/how-aperture-works#request-routing-by-model\">Request routing by model</a></h2>\n<p>When a request arrives, Aperture extracts the model name from the request body (for example, <code>claude-sonnet-4-20250514</code> or <code>gpt-4o</code>). The proxy looks up which provider serves that model and forwards the request to that provider's API endpoint, injecting the correct authentication headers.</p>\n<p>From the client's perspective, the proxy appears as if it were the LLM provider itself. Clients connect to the proxy URL and send standard API requests. The proxy handles the routing transparently.</p>\n<p>The following table summarizes the supported API formats. For full provider details, including compatibility flags, see the <a href=\"https://tailscale.com/docs/aperture/provider-compatibility\">provider compatibility reference</a>.</p>\n<p>| Format | Endpoint | Providers |\r\n| --- | --- | --- |\r\n| OpenAI Chat | <code>POST /v1/chat/completions</code> | OpenAI, OpenRouter, llama.cpp |\r\n| OpenAI Responses | <code>POST /v1/responses</code> | OpenAI |\r\n| Anthropic Messages | <code>POST /v1/messages</code> | Anthropic |\r\n| Gemini | <code>POST /v1beta/models/{model}:generateContent</code> | Google |\r\n| Amazon Bedrock | <code>POST /bedrock/model/{model}/invoke</code> | Amazon Bedrock |\r\n| Vertex AI | <code>POST /v1/projects/{project}/locations/{region}/publishers/{publisher}/models/{model}</code> | Google Vertex AI |</p>\n<h2><a href=\"https://tailscale.com/docs/aperture/how-aperture-works#telemetry-capture\">Telemetry capture</a></h2>\n<p>The capture system records everything needed to reconstruct and analyze each LLM interaction:</p>\n<ul>\n<li><strong>Request data</strong>: HTTP method, path, headers (with sensitive values redacted), and full request body.</li>\n<li><strong>Response data</strong>: Status code, headers, and full response body.</li>\n<li><strong>Extracted metrics</strong>: Token counts by type (input, output, cached, and reasoning), model name, request duration, and tool use count.</li>\n<li><strong>Session context</strong>: Session ID linking related requests with supported providers and agents such as Claude Code and Codex.</li>\n</ul>\n<p>The proxy processes telemetry asynchronously after the response completes, so clients receive responses without significant delay.</p>\n<p>The proxy handles two technical challenges transparently:</p>\n<ul>\n<li><strong>Compression</strong>: The proxy decompresses response bodies that arrive compressed (<code>gzip</code>, <code>deflate</code>, or Brotli) before storing them.</li>\n<li><strong>Streaming</strong>: For streaming responses (Server-Sent Events), the proxy reconstructs a complete response object from the event stream for consistent metric extraction.</li>\n</ul>\n<h2><a href=\"https://tailscale.com/docs/aperture/how-aperture-works#session-tracking\">Session tracking</a></h2>\n<p>A single coding task or conversation typically involves many LLM requests. A developer using Claude Code can generate 50 or more requests while debugging one issue. Without grouping, these appear as 50 unrelated events. With session tracking, they form a coherent unit you can analyze as a whole.</p>\n<p>Aperture groups related requests into sessions by detecting session identifiers from different client types. The following table describes how each client type identifies sessions:</p>\n<p>| Client type | Identification method |\r\n| --- | --- |\r\n| Claude Code | Session ID included in each request body |\r\n| OpenAI Codex | Session ID sent as HTTP header |\r\n| OpenAI Chat | Fingerprint generated from conversation content |\r\n| Other clients | Random identifier assigned |</p>\n<p>Sessions enable conversation-level analysis. The <a href=\"https://tailscale.com/docs/aperture/reference/dashboard\"><strong>Logs</strong> page</a> of the Aperture dashboard groups requests by session, showing the full context of a coding session or chat conversation. You can review token costs per conversation rather than per request, trace how a coding assistant deconstructed a task, or identify which conversation consumed the most resources.</p>\n<h2><a href=\"https://tailscale.com/docs/aperture/how-aperture-works#next-steps\">Next steps</a></h2>\n<ul>\n<li><a href=\"https://tailscale.com/docs/aperture/get-started\">Get started with Aperture</a>: sign up, configure providers, and connect your first LLM client.</li>\n<li><a href=\"https://tailscale.com/docs/aperture/control-access\">Control AI access</a>: set up grants that define which models each user can access.</li>\n<li><a href=\"https://tailscale.com/docs/aperture/observe-and-export\">Observe and export AI usage</a>: access dashboards and export usage data.</li>\n</ul>\n<p><img src=\"https://cdn.brandfetch.io/tailscale.com/fallback/lettermark/theme/dark/h/256/w/256/icon?c=1bfwsmEH20zzEfSNTed\" alt=\"Project Logo\"></p>\n<p>Ask AI</p>\n<p>reCAPTCHA</p>\n<p>Recaptcha requires verification.</p>\n<p>protected by <strong>reCAPTCHA</strong></p>\n"}