Running tailscale golink on kubernetes (Headscale edition)

October 06, 2023

As much as I love tailscale, rolling my own internal VPN with headscale has proven to be great fun and and good challenge. tailscale recently (read: a while ago, I'm just late to the party) introduced golink - an open source private URL shortener service for your tailnet. You can read more about this on the tailscale blog


For some context, I run my infrastructure on a self-managed Hetzner Cloud setup using their ARM boxes, with istio & authentik providing my ingress proxy and security needs. FluxCD is my GitOps provider of choice. I use headscale for my internal routing to my cluster, and also to tools running at home and in other locations. Keeping track of different links is challenging, plus I don't really use 1 browser between my devices (Arc on MacOS, Safari on iOS, etc.) so syncing bookmarks isn't so easy to just do. So, using tailscale's GoLink project, and my Headscale setup, I now run my own private go/ service, and here's how I did it.

Part 1 - Infrastructure/backing#

I'll be running golink on my k8s cluster as it's where headscale lives, and that's good enough for me. I'll be running this in my networking namespace however you can totally create a new namespace for this. Do this using the following:

kubectl create namespace networking

You don't need to configure istio's automatic injection for this namespace because of how headscale/tailscale works.

Part 2 - The setup#

Firstly, you'll need to create a PersistentVolumeClaim - an example of this is below:

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: golink-pvc
  namespace: networking
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
 

From there, you'll need to create a Deployment - replacing YOUR_AUTH_KEY with your Tailnet key, and YOUR_HEADSCALE_URL with your headscale controlplane URL:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: golink
  namespace: headscale
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: golink
      app.kubernetes.io/instance: main
  template:
    metadata:
      labels:
        app.kubernetes.io/name: golink
        app.kubernetes.io/instance: main
        sidecar.istio.io/inject: "false"
    spec:
      securityContext:
          fsGroup: 1000
      containers:
        - image: ghcr.io/tailscale/golink:main
          name: golink
          command:
            - "/golink"
          args:
            - "-sqlitedb"
            - "/home/nonroot/golink.db"
            - "-verbose"
            - "-control-url"
            - "YOUR_HEADSCALE_URL"
          env:
            - name: TS_AUTHKEY
              value: YOUR_AUTH_KEY
          volumeMounts:
            - name: data
              mountPath: /home/nonroot
          resources: {}
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: golink-pvc

It's important to mention that you can also use a secret for your environment variable (And I'd probably advice it) - this example just sets the key directly in the Deployment for ease

Lastly, we'll need to apply these to our Cluster - do this by running (Assuming you named your files golink-pvc.yaml & golink-deployment.yaml):

kubectl apply -f golink-pvc.yaml -f golink-deployment.yaml

If you're using Flux, commit the files to Git & Flux's source controller will do the rest

And... that's kinda it! Wait for the service to come online in your cluster and you can get started creating internal short-links!


Products/projects mentioned: