# tailscale-gitops-pusher ## 0.0 LLM The documentation is still very LLM-y, I (the HITL) apologise! ## 1.0 Purpose This repository builds a minimal OCI image that contains `gitops-pusher`, the Tailscale GitOps tool. The image replaces the upstream GitLab CI template, which downloads `golang:latest` (~800 MB) and compiles `gitops-pusher` during each job (~48 s per job). ## 2.0 Design Table 2-1 lists the image components. Table 2-1. Image components | Component | Size | Purpose | | --- | --- | --- | | `gitops-pusher` binary (static, stripped) | 11 MB | `apply`, `test`, `checksum` commands | | CA certificate bundle | <1 MB | TLS trust for api.tailscale.com | | Minimal static busybox | 0.3 MB | Shell and utilities required by GitLab Runner | The image is 12.3 MB uncompressed and 5.1 MB compressed (registry pull). The image has no entrypoint and `CMD /bin/sh` (the busybox:latest contract). `PATH` is `/bin`; the working directory is `/data`. GitLab Runner executes `script:` lines through the image shell. The Go binary is stripped of references to `tzdata`, `iana-etc`, and `mailcap`. The tool does not use service names, MIME types, or time zone data. ## 3.0 Build ```sh nix build .#gitops-pusher # binary nix build .#gitops-pusher-image # image podman load -i result # registers localhost/gitops-pusher: ``` The flake pins nixpkgs to `nixos-26.05`. The Tailscale release is pinned by `tailscaleVersion` in `flake.nix`. `nix develop` provides `podman`, `skopeo`, `crane`, `dive`, `glab`, `jq`, and `nixfmt`. ## 4.0 Operation ### 4.1 Policy deployment ```sh podman run --rm \ -v "$PWD/policy.hujson:/data/policy.hujson:ro" \ -e TS_API_KEY \ -e TS_TAILNET \ localhost/gitops-pusher:1.104.5 gitops-pusher apply ``` gSubcommands: `apply` (push the policy), `test` (validate the policy), `checksum` (compare control and local hashes). ### 4.2 GitLab CI The Tangled pipeline publishes the image to Docker Hub: ```sh docker pull jwkohnen/tailscale-gitops-pusher:1.104.5 ``` The pinned tag is `1.104.5`; `nix` is the floating tag. Define a base job; the GitOps jobs shall extend it: ```yaml .gitops-pusher: image: jwkohnen/tailscale-gitops-pusher:1.104.5 cache: paths: [./version-cache.json] test: extends: .gitops-pusher stage: test script: - gitops-pusher --policy-file ./policy.hujson --github-syntax=false test apply: extends: .gitops-pusher stage: apply script: - gitops-pusher --policy-file ./policy.hujson --github-syntax=false apply ``` Requirements: - Script lines shall use POSIX syntax (busybox ash). - `--github-syntax=false` shall be passed; GitLab does not parse GitHub workflow commands. - `version-cache.json` stores the previous policy ETag. The tool uses it to detect policy edits made in the admin console. - A shell-only image shall not be used; GitLab Runner executes `mkdir` and `touch` before `script:` lines. ### 4.3 Tangled CI `main` is the branch hosted on Tangled; `flake` is kept as a legacy alias. Tangled runs `.tangled/workflows/ci.yml` on pushes and pull requests to `main`. The workflow runs on a NixOS microVM and executes: ```sh nix flake check --no-build path:. nix build --no-link --print-out-paths 'path:.#gitops-pusher' nix build --no-link --print-out-paths 'path:.#gitops-pusher-image' ``` The knot hosts SHA-256 repositories. The workflow sets `GIT_DEFAULT_HASH=sha256` for the spindle clone and evaluates the checkout as a `path:` flake because Nix 2.34's libgit2 cannot open SHA-256 repositories. Select the spindle in the repository's Settings → Pipelines. It publishes `jwkohnen/tailscale-gitops-pusher:1.104.5` and `:nix` on pushes to `main` and manual runs. Add `DOCKERHUB_TOKEN` under Settings → Pipelines → Secrets; without it, the publish step is skipped. Pull request runs never publish. ## 5.0 Authentication `TS_TAILNET` is required in all modes. The tool rejects runs with more than one mode configured. Table 5-1. Authentication modes | Mode | Variables | Notes | | --- | --- | --- | | API key | `TS_API_KEY` | Expires after 90 days. | | OAuth client | `TS_OAUTH_ID`, `TS_OAUTH_SECRET` | | | Federated identity | `TS_OAUTH_ID`, `TS_ID_TOKEN` | Section 5.1 | ### 5.1 Federated identity Federated identity (workload identity federation) exchanges a GitLab OIDC token for a short-lived, `policy_file`-scoped Tailscale API token. No secret is stored in GitLab; no rotation is required. The client ID and audience are not secrets. Configuration: 1. Create a trust credential in the Tailscale admin console (Trust credentials, Credential, OpenID Connect): - Issuer: `https://gitlab.com` - Subject: `project_path:/:ref_type:branch:ref:main` - Scope: `policy_file` (Read and Write) Record the client ID and audience. 2. Add `id_tokens` to `.gitops-pusher`: ```yaml id_tokens: TS_ID_TOKEN: aud: api.tailscale.com/ ``` 3. Set `TS_OAUTH_ID` (client ID) as a protected CI/CD variable. 4. Remove `TS_API_KEY`. Token exchange errors appear on the trust credential page in the admin console. A `ref:main` subject does not authenticate merge request pipelines on other branches. ## 6.0 Version updates 1. Set `tailscaleVersion` in `flake.nix`. 2. Set `tailscaleSrc.hash` and `vendorHash` to `lib.fakeHash`. 3. Run `nix build .#gitops-pusher` twice; record each reported `got:` hash. 4. Push to `main`; Tangled CI rebuilds and publishes the image. ## 7.0 Verification - `nix flake check` - The image loads in podman and runs `gitops-pusher --help`. - TLS check: with a dummy API key, the tool returns HTTP 401 from api.tailscale.com. ## 8.0 License This repository is licensed under BSD-3-Clause (see `LICENSE`). The packaged `gitops-pusher` binary is Copyright Tailscale Inc & contributors and is distributed under the same license.