Mijn homelab cluster draait in een kast thuis. Ikzelf niet. Ik werk vanuit koffiebars, vanuit een klantkantoor, soms vanuit een hotel met WiFi die actief vijandig aanvoelt. En ik wil dan nog steeds bij mijn eigen machines kunnen terwijl ik daarbuiten zit.

Jarenlang was het standaardantwoord daarop dat je gaten in je eigen voordeur prikt. Forward een poort op de router, zet dynamic DNS op zodat het wisselende thuis-IP niet alles breekt, schrijf firewall regels, en hoop dan stilletjes dat niemand die het internet scant op de SSH daemon stuit die je net hebt blootgesteld. Het werkt, in de zin dat je bij je spullen kunt. Het betekent ook dat een deel van je privénetwerk nu de hele dag, voor altijd, vragen van vreemden beantwoordt.

Dus de vraag die ik eigenlijk beantwoord wilde hebben: hoe bereik ik mijn homelab van overal zonder iets open te zetten naar het internet? Dat gat dicht Tailscale voor mij.

Wat Tailscale eigenlijk is

Tailscale is een mesh VPN bovenop WireGuard. Elk apparaat dat je aanmeldt krijgt een stabiel adres in de 100.x.y.z range, en elk apparaat kan direct met elk ander praten. Er is geen centrale doos waar je verkeer doorheen moet, dus dit is niet het oude “VPN concentrator waar alles naar terugtunnelt” model.

flowchart TD
    subgraph ts["Tailscale Netwerk (100.x.y.z)"]
        Laptop["Laptop<br/>100.64.0.1"]
        Phone["Telefoon<br/>100.64.0.2"]
        Homelab["Homelab<br/>100.64.0.3"]
        VPS["VPS<br/>100.64.0.4"]
    end

    Laptop <--> Phone
    Laptop <--> Homelab
    Laptop <--> VPS
    Phone <--> Homelab
    Phone <--> VPS
    Homelab <--> VPS

Als het netwerk meewerkt verbinden twee apparaten rechtstreeks met elkaar. Als NAT lastig doet (en dat doet het vaak), valt het verkeer terug op een relay via Tailscale’s DERP servers. Daar regel je zelf niets van. Beide paden zijn end-to-end versleuteld, dus zelfs de relay kan niet lezen wat erdoorheen gaat.

De eerlijke trade-off, voordat we verder gaan: de coördinatielaag is van Tailscale, niet van jou. Zij draaien de control servers die de keys uitdelen en apparaten vertellen hoe ze elkaar vinden. Het eigenlijke verkeer blijft peer-to-peer en versleuteld, maar het opzetten van de verbinding leunt op een bedrijf dat je niet zelf host. Daar kom ik op terug, want voor een blog die op soevereiniteit draait doet dat ertoe, en er is een oplossing.

Waarom ik er toch naar grijp

Wat me overhaalde is wat er uit mijn router config verdwijnt. Niets wordt geforward. Niets staat bloot. Er is geen poort voor een scanner om te vinden, wat betekent dat er geen service is die ik gehard moet houden tegen het hele internet. Het attack surface dat ik vroeger onderhield bestaat gewoon niet meer.

Het prikt ook door netwerkopstellingen die anders een doodlopende weg zouden zijn. CGNAT van een ISP die je geen echt publiek IP geeft, double NAT, de dichtgetimmerde WiFi in een hotel: Tailscale komt er overal doorheen, omdat beide kanten naar buiten reiken in plaats van te wachten tot ze bereikt worden. Mijn homelab is er gewoon, op mijn tailnet, ongeacht welke brij aan routers ertussen zit.

Een paar kleinere voordelen stapelen daarbovenop. Elk apparaat houdt hetzelfde 100.x.y.z adres, dus ik jaag niet op dynamic DNS records die achterlopen op de realiteit. MagicDNS geeft elke machine een naam als homelab.tail-abc123.ts.net, zodat ik geen IPs meer hoef te onthouden. En ACLs laten me bepalen wie wat mag bereiken, wat betekent dat een gastapparaat of een geleende laptop niet automatisch de sleutels tot het cluster krijgt.

Aan de praat krijgen

Begin met de homelab node zelf.

# Ubuntu/Debian
curl -fsSL https://tailscale.com/install.sh | sh

# Start Tailscale
sudo tailscale up

# Check status
tailscale status

tailscale up print een authenticatie link. Open die, log in, en de node sluit zich aan op je tailnet. Zet daarna de client op wat je met je meeneemt:

  • Je laptop
  • Je telefoon
  • Wat er verder ook bij moet kunnen

Elk apparaat authenticeert tegen hetzelfde Tailscale account, en zodra dat gebeurd is zien ze elkaar. Om te checken of het werkt, benader je de homelab vanaf je laptop:

# Vanaf laptop
ping homelab.tail-abc123.ts.net

# SSH naar homelab
ssh user@homelab.tail-abc123.ts.net

# Toegang tot services
curl http://homelab.tail-abc123.ts.net:3000  # Grafana

Geen port forwarding, geen firewall regels om te schrijven. De eerste keer dat je vanuit de trein in een machine thuis SSH’t, zonder iets opengezet te hebben, voelt het lichtelijk illegaal.

De apparaten bereiken die geen Tailscale kunnen draaien

Genoeg dingen op een thuisnetwerk zullen nooit een Tailscale client draaien. De NAS firmware staat het niet toe, de printer is een printer, de smart plug is amper een computer. Een subnet router lost dat op door één aangemelde node om te toveren tot een doorgang naar de rest van het LAN.

# Op homelab node
sudo tailscale up --advertise-routes=192.168.1.0/24

# Accepteer de route in Tailscale admin console
# Of gebruik --accept-routes op clients

Nadat je de geadverteerde route accepteert, kan je laptop 192.168.1.50 bereiken (de NAS, de printer, wat daar ook leeft) door via de homelab node te routeren.

flowchart LR
    subgraph remote["Remote (Koffiebar)"]
        Laptop["Laptop<br/>100.64.0.1"]
    end

    subgraph home["Thuisnetwerk"]
        Homelab["Homelab<br/>100.64.0.3<br/>192.168.1.10"]
        NAS["NAS<br/>192.168.1.50"]
        Printer["Printer<br/>192.168.1.60"]
    end

    Laptop -->|Tailscale| Homelab
    Homelab -->|Lokaal| NAS
    Homelab -->|Lokaal| Printer

Al je verkeer naar huis sturen

Er is een verwante truc voor wanneer je het netwerk om je heen helemaal niet vertrouwt. Maak van een thuis-node een exit node en je laptop kan alles erdoorheen routeren, zodat je verkeer vanaf je thuis-IP naar buiten gaat in plaats van vanaf dat van de koffiebar.

# Op homelab node
sudo tailscale up --advertise-exit-node

# Accepteer in admin console

# Op laptop (wanneer nodig)
sudo tailscale up --exit-node=homelab

Ik zet dit aan als ik op WiFi zit die ik niet beheer, wat me uit het zicht haalt van wat dat netwerk ook logt. Het is ook handig om geo-restricted dingen te bereiken alsof je thuis bent, en om je thuis Pi-hole te gebruiken waar je ook bent.

Cluster services op het tailnet zetten

Hier wordt het echt prettig voor een Kubernetes homelab. De Tailscale Kubernetes Operator kan services direct op je tailnet publiceren, zodat ze op naam bereikbaar zijn zonder een ingress die naar het internet wijst.

# Installeer operator
helm repo add tailscale https://pkgs.tailscale.com/helmcharts
helm repo update

helm install tailscale-operator tailscale/tailscale-operator \
  --namespace tailscale \
  --create-namespace \
  --set oauth.clientId="<client-id>" \
  --set oauth.clientSecret="<client-secret>"

Maak eerst de OAuth client in de Tailscale admin console, en zet die credentials dan in de install. Met de operator draaiend is een service blootstellen één annotatie:

apiVersion: v1
kind: Service
metadata:
  name: grafana
  annotations:
    tailscale.com/expose: "true"
spec:
  selector:
    app: grafana
  ports:
    - port: 3000

Grafana is nu bereikbaar op grafana.tail-abc123.ts.net:3000 vanaf elk apparaat op het tailnet, en nergens anders. Wil je Tailscale liever als volwaardige ingress controller behandelen, dan kan dat ook:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: grafana
spec:
  ingressClassName: tailscale
  rules:
    - host: grafana
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: grafana
                port:
                  number: 3000

Dat geeft je grafana.tail-abc123.ts.net met HTTPS dat voor je geregeld wordt.

Bepalen wie wat mag bereiken

Apparaten aanmelden is de makkelijke helft. De helft die het denken waard is, is toegang. Standaard kan alles op je tailnet met alles praten, en dat is prima totdat je een apparaat toevoegt dat je minder vertrouwt. ACLs laten je het strakker zetten.

{
  "acls": [
    // Admin kan alles benaderen
    {
      "action": "accept",
      "src": ["group:admin"],
      "dst": ["*:*"]
    },
    // Developers kunnen cluster services benaderen
    {
      "action": "accept",
      "src": ["group:dev"],
      "dst": ["tag:k8s:80,443,6443"]
    },
    // Iedereen kan monitoring benaderen
    {
      "action": "accept",
      "src": ["*"],
      "dst": ["tag:monitoring:3000,9090"]
    }
  ],
  "groups": {
    "group:admin": ["user@example.com"],
    "group:dev": ["dev1@example.com", "dev2@example.com"]
  },
  "tagOwners": {
    "tag:k8s": ["group:admin"],
    "tag:monitoring": ["group:admin"]
  }
}

Je tagt apparaten in de admin console, en de ACL bepaalt wat elke tag mag bereiken. Een dev-box krijgt de cluster API en de web-poorten, de monitoring stack is leesbaar voor iedereen op het tailnet, en admin krijgt de rest. Hetzelfde principe dat network policies het schrijven waard maakt geldt hier: een kleinere blast radius als er wél iets misgaat.

Namen en certificaten

MagicDNS geeft elk apparaat al een device-name.tail-abc123.ts.net naam. In de admin console kun je het aanzetten en de willekeurige tailnet-string vervangen door iets dat je echt onthoudt.

Voor TLS geeft Tailscale echte certificaten uit voor tailnet domeinen:

tailscale cert grafana.tail-abc123.ts.net

Dat is geldige HTTPS voor een interne service zonder Let’s Encrypt of cert-manager erbij te slepen. Wil je je eigen domein ervoor, wijs dan een CNAME naar de tailnet-naam en vertel Tailscale het te accepteren:

# In je DNS
grafana.internal.example.com CNAME grafana.tail-abc123.ts.net

Wanneer je wél iets publiek nodig hebt

Af en toe wil je één ding bereikbaar hebben vanaf het open internet: een webhook endpoint, een link die je iemand een uur lang geeft, een snelle demo. Tailscale Funnel geeft je daar een publieke URL voor zonder aan je router te komen.

# Stel lokale poort 3000 publiek bloot
tailscale funnel 3000

Je krijgt iets terug als https://homelab.tail-abc123.ts.net/. Eén waarschuwing die je serieus moet nemen: Funnel zit buiten je ACLs om. Wat je funnelt is echt publiek, dus behandel het alsof je toch een poort hebt geforward, en funnel alleen dingen waarvan je het prima vindt dat de wereld eraan zit.

De control plane in eigen hand

Terug naar die trade-off van het begin. Wat aan me knaagt bij het afhankelijk zijn van Tailscale’s coördinatieservers is hetzelfde dat aan me knaagt bij elke black box: als hij plat ligt, of als het bedrijf van koers verandert, verandert mijn toegang mee. Voor een homelab gebouwd op het idee van bezitten wat je draait is het uithanden geven van de keys-en-discovery laag een echte concessie.

Headscale is daar het antwoord op. Het is een open-source herimplementatie van de Tailscale control server die je zelf host, en de gewone Tailscale clients praten er probleemloos mee.

# headscale deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: headscale
spec:
  replicas: 1
  selector:
    matchLabels:
      app: headscale
  template:
    spec:
      containers:
        - name: headscale
          image: headscale/headscale:latest
          args:
            - serve
          ports:
            - containerPort: 8080
          volumeMounts:
            - name: config
              mountPath: /etc/headscale
            - name: data
              mountPath: /var/lib/headscale

Zelfde WireGuard eronder, zelfde client-ervaring, behalve dat de coördinatie nu draait op infrastructuur die jij beheert. Je neemt het zelf draaien en updaten ervan op je, dat is de prijs. Wat je terugkrijgt is een mesh waar niemand anders de stekker uit kan trekken.

Inhaken op de rest van je tools

Een paar integraties verdienen zich dagelijks terug. Tailscale kan SSH auth afhandelen, zodat je geen keys meer hoeft te jongleren voor hosts die al op je tailnet zitten:

# Enable Tailscale SSH op server
tailscale up --ssh

# Verbind (geen keys nodig!)
ssh user@homelab.tail-abc123.ts.net

Tailscale authenticeert de verbinding op basis van je tailnet-identiteit, dus er is geen key om rond te kopiëren. Cluster-toegang werkt op dezelfde manier zodra de API server op het tailnet zit:

# ~/.kube/config
apiVersion: v1
clusters:
  - cluster:
      server: https://k3s.tail-abc123.ts.net:6443
    name: homelab
contexts:
  - context:
      cluster: homelab
      user: admin
    name: homelab
current-context: homelab

kubectl vanuit een hotelkamer raakt het cluster alsof je ernaast zat. En om op de machine zelf te bewerken is één regel SSH config genoeg voor VS Code Remote om te verbinden:

// .ssh/config
Host homelab
    HostName homelab.tail-abc123.ts.net
    User youruser

Hoe mijn setup er echt uitziet

Niets exotisch. Eén node doet het zware werk en mijn handvol apparaten verbindt ermee.

flowchart TD
    subgraph tailnet["Mijn Tailnet"]
        subgraph home["Thuis"]
            K3s["K3s Cluster<br/>Subnet Router<br/>Exit Node"]
            NAS["Synology NAS"]
        end

        subgraph devices["Apparaten"]
            Laptop["Laptop"]
            Phone["Telefoon"]
            Tablet["Tablet"]
        end
    end

    Laptop --> K3s
    Phone --> K3s
    Tablet --> K3s
    K3s --> NAS

De K3s node fungeert als subnet router voor 192.168.1.0/24, zodat de NAS en al het andere op het LAN bereikbaar zijn. Hij is ook mijn exit node voor de dagen dat ik op WiFi zit die ik liever niet vertrouw. De Tailscale Operator publiceert Grafana, ArgoCD en GitLab op het tailnet, en een ACL houdt het geheel beperkt tot mijn eigen apparaten.

De opbrengst is alledaags in de beste zin. Ik check monitoring van overal. Ik heb een ArgoCD sync vanaf mijn telefoon in de trein getrapt. En de router heeft nog nooit één geforwarde poort gehad om dat allemaal te laten werken.

Wanneer het zich misdraagt

Een paar dingen waar ik daadwerkelijk over gestruikeld ben.

“Tailscale is geblokkeerd.” Sommige bedrijfsnetwerken blokkeren het ronduit. Je kunt het naar een andere DERP poort verplaatsen, of het over TCP/443 draaien zodat het op gewoon HTTPS verkeer lijkt en ongemerkt langsglipt.

“De verbinding is traag.” Meestal betekent dat het directe pad mislukt is en je via een DERP relay gaat. Check of een firewall het UDP verkeer opvreet dat Tailscale voor directe verbindingen gebruikt, en of je NAT type er één is dat relay afdwingt.

“Ik kan het subnet niet bereiken.” Subnet routes moeten geaccepteerd worden, niet alleen geadverteerd. Adverteer op de router node met --advertise-routes, accepteer de route in de admin console, en zet --accept-routes aan op de clients die het nodig hebben.

Waarom dit beklijft

De verschuiving die Tailscale maakt is klein om te beschrijven en groot in de praktijk. In plaats van je netwerk open te zetten en die opening vervolgens te verdedigen, zet je niets open, en vereist het bereiken van wat dan ook dat je je eerst op de mesh authenticeert. Encryptie is geen instelling die je moet onthouden aan te zetten, het is de standaard, en toegang leeft op één plek waarover je kunt nadenken.

Wat ik er echt uit haal is een homelab die privé blijft terwijl hij zich, vanaf mijn kant, gedraagt alsof ik nooit van huis ben gegaan. Met Headscale erbij krijg ik dat zonder op andermans servers te leunen om de verbindingen tot stand te brengen. De meest veilige poort is inderdaad degene die nooit open was.