Built-in connectors

Last validated:
Built-in Tailnet connectors are currently in alpha.

Aperture includes two connectors whose tools ship with the product instead of coming from an upstream MCP server: Tailnet and Tailscale SSH. Both act on your tailnet through Tailscale itself, so they need no external MCP server or third-party credential.

Use these connectors for the following jobs:

  • Join a new application, service, or machine to the user's tailnet.
  • Discover the tailnet machines that have Tailscale SSH enabled.
  • Run a shell command on one of those machines.

An admin must set up each connector before it appears. Aperture shows the guided setup only to admins, on My Aperture > Connectors. Access is deny-by-default, so each connector also needs a matching connectors grant.

Tailscale SSH setup takes about a minute. Tailnet setup takes about ten minutes because you create a Tailscale OAuth app first.

Prerequisites

Before you begin, you need the following:

  • The admin role in Aperture, to set up a connector and edit grants.
  • For the tailnet connector, permission to create a Tailscale OAuth app in your tailnet, which requires the Owner or Admin role in that tailnet, plus a Tailscale API access token.
  • For the Tailscale SSH connector, target machines with Tailscale SSH enabled. Your tailnet policy must let the Aperture node connect to them.
  • An Aperture gateway that runs with its own Tailscale node. Both Tailscale SSH tools depend on it.

How built-in connectors differ from other connectors

A built-in connector appears on the Connectors page in the Built-in category. Both connectors also appear as tool categories in Aperture's built-in chat.

Two characteristics set a built-in connector apart:

  • Aperture supplies the tools, so it does not poll an upstream server and the tool list stays fixed while the connector runs.
  • The Built-in category and the system label are separate concepts. The shipped default configuration grants label:system, which reaches only the built-in aperture connector. Tailnet and Tailscale SSH do not receive that label, and you cannot give it to them: Aperture strips the reserved system label from every connectors.servers entry and reports a warning that blocks the save. Grant these connectors by ID instead. Refer to the reserved system label.

Where to define grants

The grant examples in this topic show a single connectors capability entry. Add each one to a grant whose src names the users or groups that need the connector, in one of the following two places:

  • Add the grant in the Aperture gateway configuration.

    Open the Aperture dashboard and go to Administration > Grants.

    Use the visual grant editor, or edit the same grants array as JSON on Administration > Configuration.
  • Add the grant in the tailnet policy file. Add it to the grants array of the tailnet the Aperture gateway belongs to.

The following example shows a complete Aperture configuration grant that gives the ai-users group every tool from the tailnet connector:

{
  "grants": [
    {
      "src": ["group:ai-users"],
      "app": {
        "tailscale.com/cap/aperture": [
          { "role": "user" },
          { "connectors": ["Tailnet/**"] }
        ]
      }
    }
  ]
}

The grant examples on this page use Aperture configuration syntax, where the dst field is not required because the destination is the Aperture device itself. If you define grants in the tailnet policy file instead, you must include a dst key specifying the Aperture device (for example, "dst": ["tag:aperture"]). Omitting dst in a tailnet policy file grant causes the grant to silently have no effect. For a full comparison and conversion steps, refer to Aperture grants vs. tailnet policy file grants.

The dashboard can write one grant for you. When a connector reports No access to an admin, Grant me access on the Connectors page adds a <connector-id>/** grant for that admin account's login name to the configuration grants array. Tailscale SSH setup performs the same step automatically. Any grant for another user or group is a manual edit in one of the two locations above.

The first segment of a connectors pattern is the connector ID, the key under connectors.servers, not the provider value. Dashboard setup uses the provider name as the key, so the IDs are Tailnet and TailnetSSH. If you configure an instance by hand under a different key, use that key in your grants. The key must not contain an underscore, because Aperture recovers the connector ID by splitting a prefixed tool name on the first underscore.

The provider value is fixed and case-sensitive: exactly Tailnet or TailnetSSH. Aperture rejects any other value as an unknown provider.

Tailnet connector

The tailnet connector joins a new node to the user's tailnet and returns a single-use auth key for that join.

The connector provides one tool:

ToolClient-facing nameWhat it does
provision_nodeTailnet_provision_nodeStarts a one-time browser authorization, then returns a single-use Tailscale auth key after the user approves.

Use this connector for workflows where an agent brings a new application or service into the tailnet. The agent holds no long-lived shared credential.

Step 1: Set up the connector

The dashboard guides you through creating the Tailscale OAuth app that provision_node requires, then writes the connector configuration for you. Doing this by hand is error-prone. The OAuth app must allow exactly the scope and node attributes that the connector requests.

To set up the connector:

  1. In the Aperture dashboard, open My Aperture > Connectors.
  2. Select the tailnet connector.
  3. Generate a Tailscale API access token with admin scope from the Keys page of the Tailscale admin console.
  4. Run the two commands the panel provides, which are reproduced below.
  5. Paste the clientSecret from the response.
  6. Select Add connector.

The first command prompts for the token and exports it:

printf "Paste your Tailscale API access token: " \
  && read -rs TS_API_TOKEN \
  && export TS_API_TOKEN \
  && echo

The second command creates the OAuth app in the same shell session:

curl -X POST "https://api.tailscale.com/api/v2/tailnet/-/oauth-apps" \
  -u "$TS_API_TOKEN:" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "aperture-tailnet",
    "redirectUris": [
      "https://<aperture-hostname>/aperture/auth/Tailnet/callback"
    ],
    "scopes": [
      "auth_keys:create:once"
    ],
    "allowedNodeAttributes": [
      "custom:createdByAperture",
      "custom:createdByAI"
    ]
  }' | python3 -m json.tool

The panel fills in <aperture-hostname> for you. The callback path is /aperture/auth/<connector-id>/callback, so a connector configured under a key other than Tailnet needs that key in the URL instead. The panel also provides a PowerShell version of both commands for Windows.

The two node attributes mark provisioned devices as Aperture-created and AI-created. You can reference them in device posture conditions and grants to gate access based on how a device joined. Refer to Device provisioning with OAuth apps for the full OAuth app reference.

The client secret is required. Aperture reads the client ID out of it, so Aperture rejects a secret in any form other than tskey-app-<id>-<secret>.

Step 2: Grant access

Setup does not grant access. Add a connectors grant for the users who need the tool, in one of the locations described in Where to define grants. To grant the provisioning tool by itself:

{ "connectors": ["Tailnet/tools/provision_node"] }

To grant every tool from the connector:

{ "connectors": ["Tailnet/**"] }

Refer to Grant access to MCP tools for the full pattern syntax.

Authorization flow

This connector does not use the Connect button. Authorization starts from the tool and runs one time per provisioned node:

  1. The agent calls Tailnet_provision_node with no arguments.
  2. Aperture returns an authorization link.
  3. The user opens the link and approves the request.
  4. The agent calls the tool again.
  5. Aperture returns the single-use auth key.

Two timers bound the flow. The user must approve the authorization link within 15 minutes, and the agent must collect the key within 10 minutes after approval. Aperture discards the pending flow or the uncollected key after those windows, and the agent starts over with a new call.

Aperture authorizes each call rather than each user, so the connector reports Ready on the Connectors page once it is configured and granted. Ready means the connector is usable, not that any node is authorized: every provisioned node still needs its own approval. Aperture stores no credential to connect or disconnect.

Tailnet configuration reference

Dashboard setup writes the following configuration to connectors.servers, which you can review on Administration > Configuration. Use it to audit an existing entry. The Tailnet key is the connector ID your grants must reference.

{
  "connectors": {
    "servers": {
      "Tailnet": {
        "provider": "Tailnet",
        "protocol": "mcp",
        "url": "https://api.tailscale.com",
        "labels": ["restricted"],
        "auth": {
          "type": "oauth2_authorization_code",
          "client_id": "<oauth-client-id>",
          "client_secret": "<oauth-client-secret>",
          "auth_url": "https://login.tailscale.com/a/oauth_authorize",
          "token_url": "https://api.tailscale.com/api/v2/oauth/token",
          "scopes": ["auth_keys:create:once"],
          "auth_params": {
            "node_attributes": "{\"custom:createdByAperture\":true,\"custom:createdByAI\":true}"
          }
        }
      }
    }
  }
}

auth.type must be oauth2_authorization_code. Any other auth type produces the load-time error connectors.servers.<id>: built-in provider "Tailnet" requires auth type "oauth2_authorization_code".

The node_attributes value must stay within the OAuth app's allowedNodeAttributes allowlist. Aperture sends it on the token exchange, so an attribute the app does not allow fails the exchange rather than the save.

Dashboard setup labels both connectors restricted. The label records intent and enforces nothing on its own. Restriction comes from narrow grants.

Tailscale SSH connector

The Tailscale SSH connector discovers SSH-enabled machines in the tailnet and runs one shell command on a chosen machine.

The connector provides two tools:

ToolClient-facing nameWhat it does
list_machinesTailnetSSH_list_machinesLists the tailnet machines that have Tailscale SSH enabled, with status, OS, and tags.
run_commandTailnetSSH_run_commandRuns one shell command on one listed machine over Tailscale SSH.

This connector holds no credential of its own. It connects as the Aperture node, so your tailnet's Tailscale SSH policy determines what it reaches.

Step 1: Set up the connector

To set up the connector:

  1. In the Aperture dashboard, open My Aperture > Connectors.
  2. Select the Tailscale SSH connector.
  3. Select Set up.

This connector has no configuration fields. Setup writes the connector configuration, then adds a TailnetSSH/** grant for the admin who performs it. Aperture appends that grant to the configuration grant policy whose only src entry is that admin account's login name, or adds a policy for them if none exists.

Step 2: Grant access

Setup grants the connector only to the admin who ran it. Add a connectors grant for every other user who needs the tools, in one of the locations described in Where to define grants. Use one of the following patterns:

PatternGrants
"TailnetSSH/tools/list_machines"Machine discovery alone
"TailnetSSH/tools/run_command"Command execution alone
"TailnetSSH/**"The whole connector

For example:

{ "connectors": ["TailnetSSH/tools/list_machines"] }

This connector runs commands on reachable tailnet machines, so grant it narrowly. A user with a broad "**" grant reaches it regardless of the connector's label.

Call the connector tools

Call the tools in the following order:

  1. Call TailnetSSH_list_machines to discover eligible targets.
  2. Take a machine name from the Machine column of the result.
  3. Call TailnetSSH_run_command with machine and command.

run_command also accepts two optional fields:

FieldDefaultDescription
userrootThe remote user to connect as. Your Tailscale SSH policy determines whether this user has permission.
timeout300Command timeout in seconds. The maximum is 600. Aperture clamps longer values. On timeout, Aperture stops the command and returns the output it produced.

Aperture resolves the machine value against the network map. A full MagicDNS name or an exact hostname wins over a short name. If a short name matches more than one machine, the tool asks for the full name instead. Matching ignores case.

A machine on the list has Tailscale SSH enabled. That does not mean the Aperture node can run commands on it. Your tailnet's Tailscale SSH policy decides access when run_command connects, so a listed machine can still refuse.

Behavior and limits

The following behaviors apply to every run_command call:

  • Each call opens a fresh SSH session. Working directory, environment variables, and shell state do not carry over, so combine steps into one command, such as cd /srv && ./deploy.sh.
  • A job that runs in the background ends when the call returns, unless the command detaches it, such as nohup cmd >/tmp/out 2>&1 &.
  • Aperture gives the connection 30 seconds to establish, separate from the command timeout.
  • Aperture returns stdout and stderr together with the exit code.
  • The tool result reports a non-zero exit code instead of failing the call.
  • Aperture truncates output at about 1 MiB. Filter large output on the remote machine.
  • Aperture verifies the machine's SSH host key against the tailnet network map on every call, so there is no trust-on-first-use window and no host-key approval prompt.
  • The machine list reflects the tailnet at the moment of the call. Aperture does not cache it.
  • Machine names, OS values, and tags come from other tailnet members. Aperture marks them as data rather than instructions when it hands them to the model.
  • Each call writes one audit log line with the connector ID, calling user, machine, remote user, full command, and exit code. Aperture does not log command output.

Tailscale SSH configuration reference

Setup writes the following configuration. This connector has no auth block, because it connects as the Aperture node.

{
  "connectors": {
    "servers": {
      "TailnetSSH": {
        "provider": "TailnetSSH",
        "protocol": "mcp",
        "url": "https://api.tailscale.com",
        "labels": ["restricted"]
      }
    }
  }
}

Aperture does not dial the url value. The tools read the tailnet network map and connect over the gateway's own Tailscale node.

Verify a built-in connector

To confirm a built-in connector works:

  1. Open the My Aperture > Connectors page.
  2. Confirm the connector appears in the Built-in category with status Ready and lists the expected tools.
  3. Confirm the users who need it have a matching connectors grant.
  4. Call a tool from a client through Aperture's MCP endpoint, then compare the result with the following.

A successful TailnetSSH_list_machines call returns a count summary, notes about authorization and data handling, and a table:

Tailnet machines with Tailscale SSH enabled: 2 total, 1 online, 1 offline.

| Machine | Status | OS | Tags |
| --- | --- | --- | --- |
| build-1.tail0abc.ts.net | offline (last seen 2026-08-12) | linux | tag:ci |
| web-1.tail0abc.ts.net | online | linux | - |

A tailnet with no SSH-enabled machines returns No tailnet machines have Tailscale SSH enabled. instead.

A successful TailnetSSH_run_command call returns a header line followed by the combined output, or (no output) when the command produced none:

Ran on web-1.tail0abc.ts.net (user root), exit code 0:

Linux web-1 6.8.0-40-generic x86_64