# Tailscale

Connect Verify browsers to your tailnet and test private previews or apps running on localhost.

## Test private apps with Tailscale

Send Verify browser agents to an internal preview or an app running on your own computer through Tailscale. With Tailscale, the app can stay private: each browser sandbox joins your tailnet with a dedicated tag and the access you grant it. You need a Tailscale administrator to configure the policy and OAuth client, and a Composal organization administrator to save the connection.

## Test an app running on localhost

A remote browser's `localhost` refers to its own sandbox. To test your computer's app, expose it through your tailnet and use that reachable address as the Verify environment URL.

1. Install and sign in to Tailscale on the computer running your app. Start the app, for example at `http://127.0.0.1:3000`.
2. On that computer, run:

   ```sh
   tailscale serve 3000
   ```

   Follow any prompt to enable HTTPS. Copy the HTTPS tailnet URL printed by the command, such as `https://dev-laptop.example-tailnet.ts.net`, and keep the command running. [Tailscale Serve](https://tailscale.com/docs/features/tailscale-serve) proxies the local port within your tailnet; its access policy still applies.
3. Configure the restricted connection below. Allow the Verify source tag to reach this computer on **TCP 443**, the Serve endpoint's port. For a user-owned laptop, use its exact Tailscale IP as the policy destination rather than retagging your personal device. Adapt the policy's accept and deny tests to that address.
4. Save the HTTPS tailnet URL as your Verify environment URL and select the Tailscale connection. Ask your connected agent to prepare one short journey against that environment, review its actions and expected outcome, then launch it. Keep your computer awake, Tailscale connected, and the app and Serve running until the run finishes.

If your app already listens on its Tailscale interface, you can instead use `http://<tailscale-ip>:<app-port>` and allow that exact port. A service bound only to `127.0.0.1` needs a proxy such as Serve. Configure your development server's allowed hosts, redirects, API URLs, and WebSocket endpoints for the tailnet address: a redirect or request to `localhost` sends the remote browser back to its own sandbox.

## Connect Tailscale

An organization administrator can give Verify browser agents access to a preview on your Tailscale network. This is a network connection for the selected Verify environment; it does not sign an agent into your application. Use a separate test account in the project's **Credentials** when the preview requires a login.

Verify supports Tailscale **OAuth clients** using the client credentials flow. It does not support the **OAuth Apps** browser consent and callback flow. Tailscale's current OAuth Apps device flow requires fresh consent for each new device and creates user-owned devices; Verify uses a dedicated tag so browser sandboxes can enroll automatically with restricted access. See [Tailscale OAuth Apps device provisioning](https://tailscale.com/docs/features/oauth-apps/device-provisioning).

1. In your Tailscale policy, create a dedicated source tag such as `tag:composal-verify` and a destination tag for the preview service. Grant the source **only** the destination and ports the browser needs. For an HTTPS preview on a tagged Tailscale device, a starting policy is:

   ```json
   {
     "tagOwners": {
       "tag:composal-verify": ["autogroup:admin"],
       "tag:preview-web": ["autogroup:admin"],
       "tag:production-db": ["autogroup:admin"]
     },
     "grants": [
       {
         "src": ["tag:composal-verify"],
         "dst": ["tag:preview-web"],
         "ip": ["tcp:443"]
       }
     ],
     "tests": [
       {
         "src": "tag:composal-verify",
         "accept": ["tag:preview-web:443"],
         "deny": ["tag:preview-web:22", "tag:production-db:5432"]
       }
     ]
   }
   ```

   Merge this into your existing policy rather than replacing it. Change the destination tag and port to match your preview, and use a real sensitive destination in the deny test. If the preview is behind a subnet router, grant the exact private address and port instead. **Review every existing grant and ACL that also matches `tag:composal-verify`: permissions are additive, so a broad rule can give the browser access beyond this example.** Tailscale's default allow-all policy must be replaced with the narrow policy you intend. Save the policy only after its access tests pass. See [Tailscale grants](https://tailscale.com/docs/reference/syntax/grants) and [policy tests](https://tailscale.com/kb/1337/policy-syntax).
2. In the [Tailscale admin console](https://console.tailscale.com/admin/settings/trust-credentials), open **Settings → Trust credentials** and create an **OAuth client**. Give it a recognizable name, such as `Composal Verify`. Choose custom scopes, enable **Auth Keys → Write** (its required read access is included), and restrict it to `tag:composal-verify`. Do not create an all-scopes client. After generating the credential, the **Credential created** dialog displays **Client ID** and **Client secret**, each with a copy button. Keep this dialog open until you have saved both values in Verify: the full secret is shown only once. Do not put it in chat, a Scenario, or a screenshot. If you close the dialog before copying the secret, create a replacement OAuth client with the same restricted scope and tag. Verify uses this credential to mint a one-use, ephemeral, preapproved auth key for each browser sandbox; the OAuth secret stays in Composal's encrypted storage. See [Tailscale OAuth clients](https://tailscale.com/docs/features/oauth-clients).
3. In Composal, open your project and follow **Environments → selected environment → Network Access → Private network → Add Tailscale connection**. Enter a name, the Tailscale OAuth client ID and secret, and the exact device tag from your restricted policy. Choose **Save connection**, then select it if necessary. Confirm the card names your connection as **Selected** and shows it as **active**. Only organization administrators can change these settings; the saved secret is never displayed again. You can now close Tailscale's one-time credential dialog.
4. Run one short journey against the preview URL. Check that the agent reaches the page, signs in with its selected test account if needed, and that the ephemeral device leaves the Tailscale Machines list after the sandbox ends. If enrollment, DNS, or the page load fails, check the tag grant, preview hostname, port, subnet route, and device-approval policy separately.

### Where to configure Tailscale in Composal

Follow these callouts after preparing your Tailscale policy and OAuth client above.

<figure>
  <div style={{ position: 'relative' }}>
    <img src="/marketing/docs/verify-vpn-environments.jpg" alt="Project environment list. Callout 1 points to Environments in the sidebar; callout 2 points to the environment to open." style={{ display: 'block', width: '100%', margin: 0 }} />
    <svg viewBox="0 0 1280 490" aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
      <path d="M 210 330 L 133 330" stroke="#111827" strokeWidth="4" />
      <circle cx="210" cy="330" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="210" y="331" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">1</text>
      <path d="M 1175 284 L 1228 284" stroke="#111827" strokeWidth="4" />
      <circle cx="1175" cy="284" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="1175" y="285" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">2</text>
    </svg>
  </div>
  <figcaption>1. Open Environments in your project. 2. Choose the environment that should use the VPN. The screenshots use a synthetic test project; choose your own project and environment.</figcaption>
</figure>

<figure>
  <div style={{ position: 'relative' }}>
    <img src="/marketing/docs/verify-vpn-network-access.jpg" alt="Network Access card. Callout 3 points to Network Access; callout 4 points to Add Tailscale connection." style={{ display: 'block', width: '100%', margin: 0 }} />
    <svg viewBox="0 0 975 245" aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
      <path d="M 210 24 L 140 24" stroke="#111827" strokeWidth="4" />
      <circle cx="210" cy="24" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="210" y="25" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">3</text>
      <path d="M 330 163 L 289 163" stroke="#111827" strokeWidth="4" />
      <circle cx="330" cy="163" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="330" y="164" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">4</text>
    </svg>
  </div>
  <figcaption>3. Scroll to Network Access. 4. Choose Add Tailscale connection. The existing test connection shown here is disabled; it is not a connection to reuse.</figcaption>
</figure>

<figure>
  <div style={{ position: 'relative' }}>
    <img src="/marketing/docs/verify-tailscale-form.jpg" alt="Empty Tailscale form. Callouts 5 through 9 point to Connection name, OAuth client ID, OAuth client secret, Device tag and Save connection." style={{ display: 'block', width: '100%', margin: 0 }} />
    <svg viewBox="0 0 932 409" aria-hidden="true" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
      <path d="M 870 55 L 730 55" stroke="#111827" strokeWidth="4" />
      <circle cx="870" cy="55" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="870" y="56" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">5</text>
      <path d="M 870 130 L 730 130" stroke="#111827" strokeWidth="4" />
      <circle cx="870" cy="130" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="870" y="131" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">6</text>
      <path d="M 870 203 L 730 203" stroke="#111827" strokeWidth="4" />
      <circle cx="870" cy="203" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="870" y="204" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">7</text>
      <path d="M 870 298 L 730 298" stroke="#111827" strokeWidth="4" />
      <circle cx="870" cy="298" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="870" y="299" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">8</text>
      <path d="M 220 375 L 144 375" stroke="#111827" strokeWidth="4" />
      <circle cx="220" cy="375" r="21" fill="#F1F93B" stroke="#111827" strokeWidth="3" />
      <text x="220" y="376" textAnchor="middle" dominantBaseline="middle" fill="#111827" fontSize="24" fontWeight="700" fontFamily="sans-serif">9</text>
    </svg>
  </div>
  <figcaption>5. Name the connection. 6. Paste the OAuth client ID. 7. Paste the OAuth client secret. 8. Enter the exact source tag allowed by your Tailscale policy and OAuth client. 9. Choose Save connection. Credential fields are deliberately empty in this screenshot.</figcaption>
</figure>

After saving, select your connection and confirm it is **active**. Then run the short journey described above to check enrollment and app access. For a standalone environment, open **Verify → Environments** and use the same **Private network** card.

Verify creates an isolated Tailscale device per browser sandbox. The connection grants network reachability only; saved cookies from your own cloud agents are not copied into Verify. Browser sign-in through the cloud credential capture flow currently accepts public login URLs only. For a private login URL, configure a test account that the Verify agent can use directly. Private-network support currently applies to browser runs from a selected environment; the owner-operated cloud login browser is separate.

Disabling a connection in Verify stops new enrollments. It does not immediately remove a device that is already running. For immediate revocation, remove the grant for `tag:composal-verify` or delete the active Verify device from the Tailscale Machines page.

Continue with [scenario and MCP reference](/docs/verification) to define journeys and connect your agent.


For an AWS-native connection, see [AWS Client VPN](/docs/verify/aws-client-vpn).
