Go back to the list

October 06, 2023

3 min read

Running tailscale golink on kubernetes (Headscale edition)

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: