5.9 kB · 191 lines
Markdown
at main

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 #

nix build .#gitops-pusher          # binary
nix build .#gitops-pusher-image    # image
podman load -i result              # registers localhost/gitops-pusher:<version>

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 #

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:

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:

.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:

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:<namespace>/<project>:ref_type:branch:ref:main
    • Scope: policy_file (Read and Write) Record the client ID and audience.
  2. Add id_tokens to .gitops-pusher:

    id_tokens:
      TS_ID_TOKEN:
        aud: api.tailscale.com/<client-id>
    
  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.