Skip to content

Nodes toevoegen aan een Space

Een Space draait op machines die van jou zijn. Er een toevoegen is drie stappen: haal een ticket, maak de machine klaar, voer de join uit. Het kost een paar minuten en werkt hetzelfde of de doos nu een cloud-VM is of een server in je eigen rack.

Wat een machine nodig heeft

  • 64-bits Linux, met systemd. Elke moderne distributie.
  • Minimaal 2 GB RAM, 4 GB voor iets serieus. Twee cores is een verstandige ondergrens.
  • Geen Kubernetes-pakketten. Het join-script installeert containerd en een kubelet die past bij de versie van je control plane. Een machine die al Kubernetes-state draagt, moet eerst worden gereset.
  • Uitgaande toegang tot het control plane van je Space op TCP 6443 en 8088, plus DNS, NTP en HTTPS voor het ophalen van images. Er hoeft niets vanaf internet bereikbaar te zijn: de node belt naar buiten en houdt de verbinding open.
  • Swap uit, of een kubelet die ermee overweg kan.
  • Een vaste naam. De gekozen naam wordt in het join-ticket vastgelegd en is daarna niet te wijzigen zonder opnieuw te joinen.

Je node heeft geen publiek bereikbaar adres nodig

kubehz verbindt nooit naar je node. Alles (ook kubectl logs en kubectl exec) komt terug door de tunnel die je node heeft geopend. Daarom blijft de kubelet-poort in de firewall hieronder dicht.

1. Maak een join-ticket

Open in de dashboard je Space en kies Node toevoegen. Geef de machine de naam die hij in het cluster gaat dragen, en je krijgt een ticket.

Met de CLI:

sh
lo kubehz join worker-1

De CLI maakt het ticket aan voor de Space die bij de actieve clustermap hoort. Hij heeft vier dingen nodig: een cluster.lok8s.yaml met kind: Kubehz en spec.kubehz.hosting: shared, een afgeronde lo provision voor dat cluster (die maakt de Space aan), en KUBEHZ_TOKEN geëxporteerd in je shell (een API-token dat je in de dashboard aanmaakt onder Toegang → API-tokens). Ontbreken die, gebruik dan de dashboard.

Drie dingen over het ticket, allemaal met opzet:

  • Het wordt één keer getoond. Niets bewaart het: onze database niet en de dashboard niet. Ben je het kwijt, maak dan een nieuw; “laat het nog eens zien” bestaat niet.
  • Het geldt voor één node, één keer, en is gebonden aan precies de naam die je koos. Een ticket voor worker-1 kan worker-2 niet inschrijven.
  • Het verloopt na ongeveer tien minuten. Maak het aan als je bij de machine bent, niet de avond ervoor.

2. Maak de machine klaar

De node-firewall

De perimeter van de node is een dienst aan jou, geen controle over jou: je hebt root en kunt hem aanpassen of verwijderen. De echte controles van het platform zitten aan de clusterkant. Dit recept houdt het aanvalsoppervlak van de machine klein en laat het clusterweefsel intact.

Sla op als /etc/nftables.d/kubehz-node.nft en pas toe met nft -f /etc/nftables.d/kubehz-node.nft. Opnieuw uitvoeren vervangt de vorige tabel, dus herhalen is veilig, en ook veilig via SSH, omdat bestaande verbindingen als eerste worden geaccepteerd.

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

    # Cluster fabric interfaces. Traffic on cilium_wg0 has already proved
    # itself to WireGuard; dropping post-decryption flows here is how you
    # break kubectl logs/exec without ever seeing why.
    iifname "cilium_wg0" accept
    iifname "cilium_vxlan" accept
    iifname "cilium_host" accept
    iifname "cilium_net" accept
    iifname "lxc*" accept

    # ICMP carries path-MTU discovery. Pod traffic is double-encapsulated, so
    # the usable MTU is smaller than the wire's — block these and you get
    # silent stalls instead of errors.
    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: every node-to-node path rides this. The peers are other
    # machines in the cluster with arbitrary addresses, so the source cannot
    # be pinned — WireGuard's own keys are the authentication.
    udp dport 51871 accept

    # VXLAN (pod traffic, normally inside WireGuard).
    udp dport 8472 accept

    # Your SSH. Pin it to your own addresses if you can:
    #   tcp dport 22 ip saddr { 203.0.113.0/24 } accept
    tcp dport 22 accept

    # NodePorts: closed. Uncomment only if you deliberately want to expose
    # them on this machine's public address.
    # tcp dport 30000-32767 accept
    # udp dport 30000-32767 accept

    counter
  }

  # Cilium's datapath owns pod forwarding — the perimeter must not fight the
  # CNI. Pod-level rules belong in your NetworkPolicies.
  chain forward { type filter hook forward priority filter; policy accept; }
  chain output  { type filter hook output  priority filter; policy accept; }
}

Twee poorten staan bewust niet open:

  • 10250 (kubelet). kubectl logs en exec komen binnen via de tunnel die je node opende, dus de kubelet-API hoeft het netwerk nooit aan te kijken. Openzetten voegt risico toe en levert niets op.
  • NodePorts. Hierboven uitgecommentarieerd; zet ze alleen aan als je het meent.

Staat je machine achter een cloud-firewall (Hetzner Cloud Firewalls, een security group, een appliance), leg daar dezelfde vorm neer. Twee lagen die het eens zijn is prima; een cloud-firewall die stilletjes met de host van mening verschilt is de reden dat een node joint en een uur later onbereikbaar is.

Route: een Hetzner Cloud-VM

Niets bijzonders. Een cx22 of groter met een standaard image; het join-script brengt zijn eigen containerd en kubelet mee. Koppel je een Hetzner Cloud Firewall, sta dan inkomend UDP 51871 en 8472 en je SSH toe; uitgaand mag open blijven.

Route: bare metal, of een machine in je eigen rack

Ook niets bijzonders, met twee dingen om te controleren:

  • NAT is prima, symmetrische NAT niet altijd. De node belt naar buiten, dus een gewone thuis- of kantoor-NAT werkt. WireGuard tussen jouw node en andere heeft UDP 51871 door de NAT in beide richtingen nodig. De meeste kunnen dat, sommige carrier-grade NAT’s niet.
  • MTU. Is je verbinding al verkleind (PPPoE, een tunnel), dan wordt pod-verkeer daar bovenop nog twee keer ingepakt. Laat ICMP open (het recept hierboven doet dat) en path-MTU-discovery regelt de rest.

3. Voer de join uit

Bij je ticket hoort een script. De dashboard toont het direct onder het ticket, als kopieerveld; lo kubehz join schrijft het naar $TMPDIR/kubehz-join-<node>.sh (alleen voor de eigenaar leesbaar) en print de scp/ssh-regel; de API geeft het terug als het veld script van het ticket (POST /api/spaces/{id}/join-token). Lees het en voer het daarna als root uit op de machine; het bevat het ticket, dus verwijder het zodra de node is gejoind. Het installeert containerd en een kubelet die past bij de versie van je control plane, leest de CA van het cluster van het control plane zelf en verifieert die tegen je ticket (een machine kan alleen joinen bij het control plane dat zijn ticket heeft aangemaakt, dus er is geen CA-bestand te downloaden en niets met de hand te vergelijken), schrijft een bootstrap-configuratie die naar het control plane van je Space wijst (join.endpoint op de Space in de API) en start de kubelet. Op een machine met systemd-resolved wijst het script de kubelet naar /run/systemd/resolve/resolv.conf, zodat pods de echte upstream-resolvers krijgen in plaats van de stub op 127.0.0.53. De node:

  1. toont het ticket en krijgt zijn eigen clientcertificaat,
  2. registreert zich onder de naam die het ticket vastlegde,
  3. krijgt label en taint voor jouw tenant: dat houdt de pods van andere klanten van je machine af, en het wordt door het platform gezet, niet door de node,
  4. krijgt de CNI en wordt Ready.

De eerste join duurt een paar minuten, grotendeels images ophalen.

Daarna

  • De node verschijnt met zijn status op de pagina van je Space. kubectl get nodes met je eigen kubeconfig toont je nodes.
  • Een node verwijderen heet Afmelden in de dashboard. Het trekt de credentials in en haalt hem uit het cluster; je workloads verhuizen naar je overige nodes. De machine blijft draaien: hem uitzetten, en daarmee de kosten stoppen, is aan jou.
  • Opnieuw joinen onder dezelfde naam werkt: afmelden, machine repareren, een nieuw ticket voor dezelfde naam maken en opnieuw joinen.

Als een node niet joint

SymptoomGebruikelijke oorzaak
Het join-commando meldt een ongeldig ticketHet is verlopen (tien minuten), was al gebruikt, of is voor een andere nodenaam gemaakt. Maak een nieuw.
kubelet blijft proberen en registreert nooitUitgaand 6443 geblokkeerd, of de hostnaam van het control plane resolvet niet op de machine. Controleer met curl -v vanaf de node.
Pods kunnen geen namen resolven, de node zelf welDe kubelet leest een stub-resolver. Het script regelt systemd-resolved; bij een andere lokale resolver wijs je resolvConf in de kubelet-configuratie naar een bestand met echte upstream-servers.
De node registreert maar blijft NotReadyDe CNI start nog, of de weefselpoorten (UDP 51871/8472) zijn inkomend geblokkeerd. Controleer de firewall op beide lagen.
kubectl logs/exec lopen in een timeout, de rest werktDe tunnel staat, maar retourverkeer wordt gefilterd, meestal een regel die de interface cilium_wg0 weggooit.
Pods blijven Pending op een Ready-nodeDe pod tolereert je tenant-taint niet. Deploy in de namespace van je Space, waar tolerations voor je gezet worden, in plaats van in een namespace die je anders hebt aangemaakt.

Documentstatus

AspectDetail
Statuslive: join-flow zoals uitgeleverd (ticket → voorbereiden → join); firewall-recept gevalideerd met nft
Laatst gecontroleerd2026-09-05