Files
github-action/guides/vpn-tailscale.md
T
Tirsvad 8eaba331ec Add the connect-tailscale composite action for Gitea (not yet verified on a runner)
actions/connect-tailscale joins a Tailscale network for a deployment job, with
a guide and offline tests. Whether tailscaled survives to the next step is
still open (MIL-012 task 4).

Refs #185
Task: MIL-012#4
2026-10-19 11:24:36 +08:00

5.5 KiB

Reaching a tailnet with Tailscale

The composite action actions/connect-tailscale joins a Tailscale network from a Gitea Actions runner, so the next steps of the same job (a deployment over SSH, a migration) can reach hosts on the tailnet. It does what the GitHub marketplace action tailscale/github-action does, written for Gitea Actions: that action is built for GitHub and has not been verified on Gitea. It is experimental: it lives on the branch wireguard-vpn-action until a trial shows it is wanted. For a plain tunnel without a third-party service, see WireGuard.

A node belongs to one job: every job that deploys joins on its own and logs out in a last step. It cannot be a reusable workflow, because a called workflow runs as a separate job.

Use it

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4.2.2

      - name: Connect Tailscale
        uses: TirSystem/github-action/actions/connect-tailscale@v2
        with:
          oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
          tags: tag:ci
          version: 1.80.2
          health-check-host: deploy-host

      - name: Deploy
        run: ssh deploy@deploy-host ./deploy.sh

      - name: Disconnect Tailscale
        if: always()
        uses: TirSystem/github-action/actions/connect-tailscale@v2
        with:
          action: down

If the short form does not resolve on your instance (Gitea looks for the action on the host set in DEFAULT_ACTIONS_URL), use the full address: https://git.tirsystem.com/TirSystem/github-action/actions/connect-tailscale@v2. To try the branch before it is released, replace @v2 with @wireguard-vpn-action.

Inputs

Input Default Meaning
action up up or down
authkey A Tailscale auth key, as a secret. Give this or oauth-secret
oauth-secret The secret of an OAuth client with the scope auth_keys, as a secret. Needs tags
tags ACL tags of the node, tag:ci or tag:ci,tag:deploy. Required with oauth-secret
hostname runner name Name of the node in the tailnet
version latest The version to install when the runner has no tailscale. Pin it
userspace-networking false true runs without a TUN device
args Extra flags for tailscale up, for example --accept-routes. Never a secret
timeout 60 Seconds to wait for the node
health-check-host A tailnet host that must answer a Tailscale ping

Set it up

  1. In the Tailscale admin console define a tag for CI (tag:ci) in the policy file, with the owners that may use it, and an access rule that lets tag:ci reach only the hosts and ports the deployment needs.
  2. Create either an OAuth client with the scope auth_keys (write) for that tag, or an auth key that is reusable only when needed, ephemeral, pre-authorized and tagged. The OAuth client is preferred: it does not expire the way a key does, and each run gets a fresh ephemeral key.
  3. Store the secret as a secret of the organization, with a name that does not start with GITEA_ or GITHUB_.
  4. Rotate the secret when a person with access leaves.

The node is ephemeral and its state is kept in memory, so it disappears from the tailnet when the job ends, even if the last step does not run.

The runner

  • The job runs as root or can use sudo. The action downloads the Tailscale binaries from pkgs.tailscale.com when tailscale is not on the runner. Pin version; a runner image that already contains Tailscale skips the download.

  • TUN mode (the default) needs the capability NET_ADMIN and the device /dev/net/tun in the runner container: in the act_runner configuration set under container: options: "--cap-add=NET_ADMIN --device=/dev/net/tun".

  • Userspace mode (userspace-networking: true) needs neither, but the tailnet is then reached only through a SOCKS5 proxy on localhost:1055:

    ssh -o ProxyCommand="nc -X 5 -x localhost:1055 %h %p" deploy@deploy-host
    

    Tools that cannot use a proxy do not work in this mode.

  • Use a dedicated runner with a label of its own (for example vpn) for jobs that join a tailnet.

  • The daemon is started in the background of the "Run the Tailscale step". That it stays alive for the following steps of the job has to be confirmed on the runner in use; the first trial must do so.

What the action does

  • masks the secrets in the log first;
  • passes the auth key to tailscale up in a file of mode 600 (--auth-key=file:), never on a command line, and deletes the file at once;
  • prints errors as ::error::[category] message, with the categories configuration, environment and connection;
  • on down logs the node out, stops tailscaled and deletes its work folder.

The file: form of --auth-key needs a recent Tailscale; with an old pinned version tailscale up reports an invalid key.

Troubleshooting

Message Cause
[configuration] give authkey or oauth-secret Neither was given, or both
[configuration] tags is required oauth-secret without tags
[environment] could not download The runner cannot reach pkgs.tailscale.com, or the version does not exist
[connection] tailscale up failed A wrong or expired key, a tag the client may not use, or a missing TUN device (try userspace-networking); the last lines of tailscaled follow
[connection] health-check-host ... does not answer The access rule does not let tag:ci reach the host