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
This commit is contained in:
2026-10-19 11:24:36 +08:00
parent fce3cf8c6b
commit 8eaba331ec
4 changed files with 517 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
# 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](vpn-wireguard.md).
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
```yaml
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`:
```bash
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 |