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=falseshall be passed; GitLab does not parse GitHub workflow commands.version-cache.jsonstores 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
mkdirandtouchbeforescript: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:
-
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.
- Issuer:
-
Add
id_tokensto.gitops-pusher:id_tokens: TS_ID_TOKEN: aud: api.tailscale.com/<client-id> -
Set
TS_OAUTH_ID(client ID) as a protected CI/CD variable. -
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 #
- Set
tailscaleVersioninflake.nix. - Set
tailscaleSrc.hashandvendorHashtolib.fakeHash. - Run
nix build .#gitops-pushertwice; record each reportedgot:hash. - 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.