# Tailscale Integration (/integrations/tailscale)



[Tailscale](https://tailscale.com/) is a zero-config VPN that connects your devices, services, and cloud networks using encrypted [WireGuard](https://www.wireguard.com/) tunnels.

By connecting StarSling Runners to your Tailscale network, your CI jobs get secure access to private services — databases, NFS volumes, internal APIs — without exposing them to the public internet or maintaining static IP allow lists.

## Prerequisites

Before you start, make sure you have:

* A [Tailscale account](https://login.tailscale.com) with admin access to the [ACL editor](https://login.tailscale.com/admin/acls/file)
* A GitHub repository with Actions enabled
* Tailscale 1.90.1+ on your tailnet for OIDC (any version for OAuth fallback)

By the end of this guide, you'll have configured:

* A Tailscale tag for your runners
* An OAuth client with OIDC federation
* A GitHub Actions workflow step that connects to your tailnet

## Connecting StarSling Runners to your tailnet

<Steps>
  <Step>
    ### Create a tag in your Tailnet ACLs

    [Tailscale tags](https://tailscale.com/kb/1068/tags) group non-user devices and let you manage access control policies based on device role. Create a tag for your StarSling Runners in the [admin console ACL editor](https://login.tailscale.com/admin/acls/file) by adding it under [`tagOwners`](https://tailscale.com/kb/1337/acl-syntax#tag-owners):

    ```json
    {
      "tagOwners": {
        "tag:starslingdev": ["autogroup:admin"]
      }
    }
    ```

    This tag will be assigned to every StarSling Runner that joins your tailnet. You'll use it in [ACL rules](#granting-access-to-private-services) to control what runners can access.
  </Step>

  <Step>
    ### Create a trust credential

    Go to the [Trust credentials](https://login.tailscale.com/admin/settings/trust-credentials) page in the Tailscale admin console and click **Credential**.

    1. Select **OpenID Connect**
    2. Set **Issuer** to **GitHub** — Tailscale auto-fills the issuer URL
    3. Set **Subject** to match your repository: `repo:<your-org>/<your-repo>:*`
    4. Click **Continue** to reach the **Scopes** page
    5. Expand the **Keys** section, enable **Auth Keys** (Read & Write), and add the tag `tag:starslingdev`
    6. Click **Generate credential**

    Save the **Client ID** and **Audience** values shown after generation — you'll need them in the next step.

    <Callout type="info">
      For the OAuth fallback path, create an **OAuth** credential instead of OpenID Connect. See the **OAuth Client Secret** tab in the next step.
    </Callout>
  </Step>

  <Step>
    ### Add the Tailscale step to your workflow

    <Tabs items={['OIDC (Recommended)', 'OAuth Client Secret']}>
      <Tab value="OIDC (Recommended)">
        OIDC uses short-lived tokens issued by GitHub — no long-lived secrets are stored. Requires Tailscale 1.90.1+.

        Add the following secrets to your GitHub repository (**Settings** > **Secrets and variables** > **Actions** > **Secrets**):

        | Secret               | Value                                 |
        | -------------------- | ------------------------------------- |
        | `TS_OAUTH_CLIENT_ID` | Client ID from the previous step      |
        | `TS_AUDIENCE`        | Audience value from the previous step |

        Then add the Tailscale connection step to your workflow:

        ```yaml title=".github/workflows/ci.yml"
        jobs:
          build:
            runs-on: starsling-ubuntu-24.04 # [!code highlight]
            permissions:
              id-token: write # Required for OIDC
              contents: read
            steps:
              - uses: actions/checkout@v6

              - name: Connect to Tailscale
                uses: tailscale/github-action@v4
                with:
                  oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
                  audience: ${{ secrets.TS_AUDIENCE }}
                  tags: tag:starslingdev

              # Your runner is now on the tailnet.
              # Access any private service allowed by your ACLs.
              - name: Run tests
                run: npm test
        ```
      </Tab>

      <Tab value="OAuth Client Secret">
        If your tailnet doesn't support OIDC, create an **OAuth** credential instead. Go to [Trust credentials](https://login.tailscale.com/admin/settings/trust-credentials), click **Credential**, select the **OAuth** tab, and configure with `auth_keys` (Read & Write) scope and `tag:starslingdev`. Save the **Client ID** and **Client Secret**.

        Add the following secrets to your GitHub repository (**Settings** > **Secrets and variables** > **Actions** > **Secrets**):

        | Secret               | Value               |
        | -------------------- | ------------------- |
        | `TS_OAUTH_CLIENT_ID` | OAuth Client ID     |
        | `TS_OAUTH_SECRET`    | OAuth Client Secret |

        Then add the Tailscale connection step to your workflow:

        ```yaml title=".github/workflows/ci.yml"
        jobs:
          build:
            runs-on: starsling-ubuntu-24.04 # [!code highlight]
            permissions:
              contents: read
            steps:
              - uses: actions/checkout@v6

              - name: Connect to Tailscale
                uses: tailscale/github-action@v4
                with:
                  oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
                  oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
                  tags: tag:starslingdev

              # Your runner is now on the tailnet.
              # Access any private service allowed by your ACLs.
              - name: Run tests
                run: npm test
        ```

        <Callout type="warn">
          The OAuth client secret is a long-lived credential stored in GitHub. Rotate it periodically — you can regenerate it in the Tailscale admin console and update the GitHub secret without downtime.
        </Callout>
      </Tab>
    </Tabs>

    Your StarSling Runner is now connected to your tailnet as an [ephemeral node](https://tailscale.com/kb/1111/ephemeral-nodes). When the job completes, the node is automatically removed.

    **Verify it works:** Add a step to your workflow to confirm connectivity:

    ```yaml
    - name: Verify Tailscale connection
      run: |
        tailscale status
        tailscale ping <your-tailscale-hostname>
    ```
  </Step>
</Steps>

## Granting access to private services

With runners connected, use [Tailscale ACLs](https://tailscale.com/kb/1337/acl-syntax) to control what they can reach. Runners are tagged with `tag:starslingdev`, which you reference in ACL rules.

### Access a specific service

Grant runners access to a private database by hostname and port:

```json
{
  "acls": [
    {
      "action": "accept",
      "src": ["tag:starslingdev"],
      "dst": ["database-hostname:5432"]
    }
  ]
}
```

### Access a subnet

Using [Tailscale subnet routers](https://tailscale.com/kb/1019/subnets), grant runners access to a VPC or on-premises network:

```json
{
  "acls": [
    {
      "action": "accept",
      "src": ["tag:starslingdev"],
      "dst": ["192.0.2.0/24:5432"]
    }
  ]
}
```

<Callout type="warn">
  Always restrict to specific ports (e.g. `:5432`). Avoid `:*` (all ports) in production — overly broad rules are the most common ACL mistake.
</Callout>

## Security and compliance

StarSling's Tailscale integration is designed for zero-trust CI environments:

* **No stored secrets (OIDC)** — each job receives a short-lived GitHub OIDC token that Tailscale validates directly. No long-lived credentials are stored in GitHub.
* **Ephemeral nodes** — runners are automatically removed from your tailnet when the job completes, leaving no persistent footprint.
* **Audit visibility** — connected runners appear as tagged nodes in the [Tailscale admin console](https://login.tailscale.com/admin/machines) and are recorded in your [tailnet audit logs](https://tailscale.com/kb/1011/log-streaming).
* **Scoped access** — tag-based ACLs enforce least-privilege per job. Runners can only reach services explicitly permitted by your ACL rules.

## Troubleshooting

### Runner can't reach private services

1. **Check the ACL.** Confirm your ACL rules allow `tag:starslingdev` to reach the target service and port. Verify the ACL has been applied in the [admin console](https://login.tailscale.com/admin/acls/file).
2. **Verify the host.** If using an IP, confirm it hasn't changed. If using a MagicDNS hostname, check for stale nodes — run `tailscale status` from the runner to see what's reachable.
3. **Look for stale nodes.** If `tailscale status` shows your target hostname as "offline" alongside a `-2` variant, delete the stale entries from the [Tailscale Admin Console](https://login.tailscale.com/admin/machines).

<Cards>
  <Card title="Tailscale: GitHub CI/CD Guide" href="https://tailscale.com/docs/solutions/connect-github-CICD-workflows-to-private-infrastructure-without-public-exposure" description="Tailscale's guide to connecting GitHub Actions to private infrastructure" />

  <Card title="NFS Mount Example" href="https://github.com/starslingdev/tailscale-mount-example" description="Mount private NFS volumes over Tailscale in GitHub Actions" />

  <Card title="Tailscale ACL Reference" href="https://tailscale.com/kb/1337/acl-syntax" description="Full ACL syntax documentation from Tailscale" />

  <Card title="Linux Runners" href="/runners/linux-runners" description="Runner specs and labels" />
</Cards>
