Skip to content

Nodes zu einem Space hinzufügen

Ein Space läuft auf Maschinen, die dir gehören. Eine hinzuzufügen sind drei Schritte: Ticket holen, Maschine vorbereiten, Join ausführen. Das dauert ein paar Minuten und funktioniert gleich, egal ob die Kiste eine Cloud-VM ist oder ein Server in deinem eigenen Rack.

Was eine Maschine braucht

  • Linux, 64-Bit, mit systemd. Jede moderne Distribution.
  • Mindestens 2 GB RAM, 4 GB für alles Ernsthafte. Zwei Kerne sind eine sinnvolle Untergrenze.
  • Keine Kubernetes-Pakete. Das Join-Skript installiert containerd und ein kubelet, das zur Version deiner Control Plane passt. Eine Maschine, die bereits Kubernetes-Zustand trägt, muss zuerst zurückgesetzt werden.
  • Ausgehenden Zugriff auf die Control Plane deines Space über TCP 6443 und 8088, dazu DNS, NTP und HTTPS für Image-Pulls. Nichts muss aus dem Internet erreichbar sein: Die Node wählt sich hinaus und hält die Verbindung offen.
  • Swap aus oder ein kubelet, das damit umgehen kann.
  • Ein fester Name. Der gewählte Name wird ins Join-Ticket eingebrannt und lässt sich später nur durch erneuten Join ändern.

Deine Node braucht keine öffentlich erreichbare Adresse

kubehz verbindet sich nie zu deiner Node. Alles (auch kubectl logs und kubectl exec) läuft zurück durch den Tunnel, den deine Node geöffnet hat. Deshalb bleibt der kubelet-Port in der Firewall unten geschlossen.

1. Ein Join-Ticket erzeugen

Öffne im Dashboard deinen Space und wähle Node hinzufügen. Gib der Maschine den Namen, den sie im Cluster tragen soll, und du bekommst ein Ticket.

Mit der CLI:

sh
lo kubehz join worker-1

Die CLI erzeugt das Ticket für den Space, der zum aktiven Cluster-Verzeichnis gehört. Sie braucht vier Dinge: eine cluster.lok8s.yaml mit kind: Kubehz und spec.kubehz.hosting: shared, ein abgeschlossenes lo provision für diesen Cluster (es legt den Space an) und ein in deiner Shell exportiertes KUBEHZ_TOKEN (ein API-Token, das du im Dashboard unter Zugriff → API-Tokens ausstellst). Fehlt davon etwas, nimm das Dashboard.

Drei Dinge zum Ticket, alle bewusst so:

  • Es wird einmal angezeigt. Nichts speichert es: weder unsere Datenbank noch das Dashboard. Verlierst du es, erzeuge ein neues; ein „zeig mir das Ticket noch mal" gibt es nicht.
  • Es gilt für eine Node, einmal, und ist an genau den gewählten Namen gebunden. Ein Ticket für worker-1 kann worker-2 nicht aufnehmen.
  • Es läuft nach etwa zehn Minuten ab. Erzeuge es, wenn du an der Maschine bist, nicht am Abend davor.

2. Die Maschine vorbereiten

Die Node-Firewall

Der Perimeter der Node ist ein Dienst an dir, keine Kontrolle über dich: du hast root und kannst ihn ändern oder entfernen. Die eigentlichen Kontrollen der Plattform liegen clusterseitig. Dieses Rezept hält die Angriffsfläche der Maschine klein und lässt das Cluster-Fabric intakt.

Speichere es als /etc/nftables.d/kubehz-node.nft und wende es mit nft -f /etc/nftables.d/kubehz-node.nft an. Ein erneuter Lauf ersetzt die vorherige Tabelle, also beliebig wiederholbar, und auch über SSH sicher, weil bestehende Verbindungen zuerst akzeptiert werden.

bash
table inet kubehz_node {}
flush table inet kubehz_node

table inet kubehz_node {
  chain input {
    type filter hook input priority filter; policy drop;

    ct state established,related accept
    ct state invalid drop
    iif "lo" accept

    # Fabric-Interfaces des Clusters. Verkehr auf cilium_wg0 hat sich
    # bereits gegenüber WireGuard ausgewiesen; entschlüsselte Flows hier zu
    # verwerfen ist der Weg, kubectl logs/exec zu zerstören, ohne je zu
    # sehen warum.
    iifname "cilium_wg0" accept
    iifname "cilium_vxlan" accept
    iifname "cilium_host" accept
    iifname "cilium_net" accept
    iifname "lxc*" accept

    # ICMP trägt die Path-MTU-Erkennung. Pod-Verkehr ist doppelt gekapselt,
    # die nutzbare MTU also kleiner als die der Leitung — blockiere das, und
    # du bekommst stille Hänger statt Fehlermeldungen.
    icmp type {
      echo-request, echo-reply, destination-unreachable,
      time-exceeded, parameter-problem
    } accept
    icmpv6 type {
      echo-request, echo-reply, destination-unreachable,
      time-exceeded, parameter-problem, packet-too-big,
      nd-neighbor-solicit, nd-neighbor-advert,
      nd-router-solicit, nd-router-advert
    } accept

    # WireGuard: jeder Node-zu-Node-Pfad läuft hierüber. Die Gegenstellen
    # sind andere Maschinen im Cluster mit beliebigen Adressen — die Quelle
    # lässt sich nicht festnageln, die WireGuard-Schlüssel sind die
    # Authentifizierung.
    udp dport 51871 accept

    # VXLAN (Pod-Verkehr, normalerweise innerhalb von WireGuard).
    udp dport 8472 accept

    # Dein SSH. Nagle es auf deine eigenen Adressen fest, wenn möglich:
    #   tcp dport 22 ip saddr { 203.0.113.0/24 } accept
    tcp dport 22 accept

    # NodePorts: geschlossen. Nur einkommentieren, wenn du sie bewusst auf
    # der öffentlichen Adresse dieser Maschine anbieten willst.
    # tcp dport 30000-32767 accept
    # udp dport 30000-32767 accept

    counter
  }

  # Ciliums Datapath besitzt das Pod-Forwarding — der Perimeter darf sich
  # nicht mit dem CNI anlegen. Pod-Regeln gehören in deine NetworkPolicies.
  chain forward { type filter hook forward priority filter; policy accept; }
  chain output  { type filter hook output  priority filter; policy accept; }
}

Zwei Ports sind bewusst nicht offen:

  • 10250 (kubelet). kubectl logs und kubectl exec kommen durch den Tunnel, den deine Node geöffnet hat, sodass die kubelet-API dem Netz nie gegenüberstehen muss. Sie zu öffnen bringt Risiko und keinen Nutzen.
  • NodePorts. Oben auskommentiert; öffne sie nur, wenn du es so meinst.

Sitzt deine Maschine hinter einer Cloud-Firewall (Hetzner Cloud Firewalls, einer Security Group, einer Appliance), bilde dieselbe Form dort ab. Zwei Schichten, die übereinstimmen, sind in Ordnung; eine Cloud-Firewall, die dem Host still widerspricht, ist der Grund, warum eine Node beitritt und eine Stunde später unerreichbar ist.

Weg: eine Hetzner-Cloud-VM

Nichts Besonderes. Eine cx22 oder größer mit einem Standard-Image; das Join-Skript bringt containerd und kubelet selbst mit. Bei einer Hetzner Cloud Firewall eingehend UDP 51871 und 8472 sowie dein SSH erlauben; ausgehend kann offen bleiben.

Weg: Bare Metal oder eine Maschine im eigenen Rack

Ebenfalls nichts Besonderes, mit zwei Punkten zum Prüfen:

  • NAT ist in Ordnung, symmetrisches NAT nicht immer. Die Node wählt sich hinaus, also funktioniert ein normales Heim- oder Büro-NAT. WireGuard zwischen deiner Node und anderen braucht UDP 51871 in beide Richtungen durch das NAT. Die meisten können das, manche Carrier-Grade-NATs nicht.
  • MTU. Ist deine Leitung schon reduziert (PPPoE, ein Tunnel), wird Pod-Verkehr darüber hinaus zweimal gekapselt. Lass ICMP offen (das Rezept oben tut das), dann regelt die Path-MTU-Erkennung den Rest.

3. Den Join ausführen

Folge dem Befehl, der mit deinem Ticket angezeigt wird. Er schreibt eine Bootstrap-Konfiguration, richtet kubelet auf die Control Plane deines Space aus und startet es neu. Auf einer Maschine mit systemd-resolved richtet das Skript das kubelet auf /run/systemd/resolve/resolv.conf aus, damit Pods die echten Upstream-Resolver bekommen statt des 127.0.0.53-Stubs. Die Node:

  1. legt das Ticket vor und erhält ihr eigenes Client-Zertifikat,
  2. registriert sich unter dem im Ticket festgelegten Namen,
  3. wird für deinen Mandanten gelabelt und getaintet: Das hält die Pods anderer von deiner Maschine fern, und es wird von der Plattform gesetzt, nicht von der Node,
  4. bekommt das CNI und wird Ready.

Der erste Join dauert ein paar Minuten, das meiste davon Image-Pulls.

Danach

  • Die Node erscheint mit ihrem Status auf der Seite deines Space. kubectl get nodes mit deiner eigenen Kubeconfig zeigt deine Nodes.
  • Eine Node entfernen heißt im Dashboard Abmelden. Das entzieht ihr die Zugangsdaten und nimmt sie aus dem Cluster; deine Workloads verteilen sich auf die verbleibenden Nodes. Die Maschine läuft weiter: Sie herunterzufahren und damit ihre Kosten zu beenden, ist deine Sache.
  • Erneut unter demselben Namen beitreten funktioniert: abmelden, Maschine reparieren, neues Ticket für denselben Namen erzeugen, wieder beitreten.

Wenn eine Node nicht beitritt

SymptomÜbliche Ursache
Der Join-Befehl meldet ein ungültiges TicketEs ist abgelaufen (zehn Minuten), war schon benutzt oder wurde für einen anderen Node-Namen erzeugt. Erzeuge ein neues.
kubelet versucht es immer wieder und registriert sich nieAusgehend 6443 blockiert, oder der Hostname der Control Plane löst auf der Maschine nicht auf. Prüfe es mit curl -v von der Node aus.
Pods lösen keine Namen auf, die Node selbst schonDas kubelet liest einen Stub-Resolver. Das Skript behandelt systemd-resolved; bei einem anderen lokalen Resolver richte resolvConf in der kubelet-Konfiguration auf eine Datei mit echten Upstream-Servern aus.
Die Node registriert sich, bleibt aber NotReadyDas CNI startet noch, oder die Fabric-Ports (UDP 51871/8472) sind eingehend blockiert. Prüfe die Firewall auf beiden Schichten.
kubectl logs/exec laufen in Timeouts, alles andere gehtDer Tunnel steht, aber Rückverkehr wird gefiltert, meist eine Regel, die das Interface cilium_wg0 verwirft.
Pods bleiben auf einer Ready-Node PendingDer Pod toleriert deinen Mandanten-Taint nicht. Deploye in den Namespace deines Space, wo Tolerations für dich gesetzt werden, statt in einen anderweitig angelegten Namespace.

Doku-Status

AspektDetail
ZustandLive: Join-Ablauf wie ausgeliefert (Ticket → Vorbereiten → Join); Firewall-Rezept gegen nft validiert
Zuletzt geprüft2026-09-05