Wat gebeurt er als je Kubernetes cluster geen internet kan bereiken? Niet “trage verbinding” — helemaal geen verbinding. Schepen op zee. Afgelegen mijnbouwlocaties. Fabrieksvloeren met air-gapped netwerken. Militaire deployments.

Dit is geen edge case. Het is een ontwerp-eis voor iedereen die soevereiniteit serieus neemt.

Waarom Dit Ertoe Doet: Voorbij het Technische

Kubernetes offline draaien dwingt je om een vraag onder ogen te zien die de meeste cloud-native guides negeren: waar ben je eigenlijk van afhankelijk?

Een standaard Kubernetes setup heeft overal onzichtbare dependencies:

  • Container registries (Docker Hub, gcr.io, quay.io)
  • Helm chart repositories
  • Certificate authorities
  • NTP servers
  • DNS resolvers
  • Package repositories
  • Telemetry endpoints

Dit zijn geen optionele extra’s. Het zijn dragende aannames ingebakken in hoe de meeste clusters werken. Je cluster “werkt” totdat een van deze verdwijnt.

Dit is belangrijk voor soevereiniteit. Als je je infrastructuur niet kunt draaien zonder externe afhankelijkheden, bezit je het niet echt. Je huurt capaciteit van systemen die je niet controleert.

Island Mode: De Architectuur

Ik ontwerp al mijn infrastructuur om “island mode capable” te zijn — in staat om volledig losgekoppeld van elk extern netwerk te functioneren. Zelfs als ik nooit echt offline draai, dwingt deze constraint betere architectuur af:

flowchart TD
    subgraph island["Island Mode Cluster"]
        subgraph internal["Interne Services"]
            Registry["Registry<br/>(Harbor)"]
            NTP["NTP<br/>(chrony)"]
            DNS["DNS<br/>(CoreDNS)"]
            CA["CA<br/>(cert-mgr)"]
            Helm["Helm<br/>(ChartMuseum)"]
            GitOps["GitOps<br/>(ArgoCD)"]
        end
        internal -->|"Alle dependencies lokaal opgelost"| workloads
        subgraph workloads["Workloads"]
            Apps["Apps draaien zonder externe calls"]
        end
    end
    island x--x|"Geen verbinding vereist"| Internet["Internet<br/>(optioneel)"]

Het doel: als je de netwerkkabel eruit trekt, blijft alles draaien. Updates stoppen, maar operaties gaan door.

Component 1: Lokale Container Registry

Je cluster heeft images nodig. In een verbonden omgeving pullt het on-demand van Docker Hub of gcr.io. Offline falen die requests.

Oplossing: Mirror alles lokaal.

Harbor is mijn keuze voor self-hosted registries. Het ondersteunt:

  • Pull-through caching (automatisch mirroren wanneer verbonden)
  • Replicatie van upstream registries
  • Vulnerability scanning
  • Access control
# Harbor replication policy - sync wanneer verbonden
apiVersion: goharbor.io/v1beta1
kind: HarborReplicationPolicy
metadata:
  name: docker-hub-mirror
spec:
  srcRegistry:
    url: https://registry-1.docker.io
  destRegistry:
    url: https://harbor.internal
  trigger:
    type: scheduled
    scheduledTrigger:
      cron: "0 2 * * *"  # Nachtelijk sync wanneer verbonden
  filters:
    - type: name
      value: "library/**"  # Alleen officiële images

Wanneer verbonden, synct Harbor images. Wanneer losgekoppeld, serveert het vanuit cache.

Kritiek: Pre-pull je dependencies.

Voordat je offline gaat, zorg dat alle images die je workloads nodig hebben in de lokale registry staan:

# Lijst alle images die momenteel draaien
kubectl get pods -A -o jsonpath="{.items[*].spec.containers[*].image}" | \
  tr ' ' '\n' | sort | uniq

# Zorg dat ze allemaal gespiegeld zijn naar lokale registry

Component 2: Lokale DNS

Kubernetes gebruikt DNS voor service discovery. CoreDNS handelt cluster-interne namen af, maar wat met externe namen?

Probleem: Pods die api.github.com of registry-1.docker.io proberen te resolven zullen falen zonder upstream DNS.

Oplossing: Configureer CoreDNS om bekende externe namen intern af te handelen:

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        errors
        health

        # Cluster-interne namen
        kubernetes cluster.local in-addr.arpa ip6.arpa {
          pods insecure
          fallthrough in-addr.arpa ip6.arpa
        }

        # Bekende externe services - resolve naar intern
        hosts /etc/coredns/custom.hosts {
          fallthrough
        }

        # Forward onbekend naar lokale resolver (of fail gracefully)
        forward . 10.0.0.1 {
          policy sequential
          health_check 5s
        }

        cache 30
        loop
        reload
        loadbalance
    }
  custom.hosts: |
    10.0.10.5  harbor.internal registry.internal
    10.0.10.6  git.internal gitlab.internal
    10.0.10.7  ntp.internal time.internal

Component 3: Lokale Tijdsynchronisatie

Tijdsynchronisatie is belangrijker dan je denkt. TLS certificaten valideren tegen tijd. Logs hebben accurate timestamps nodig. Distributed systems hebben clock agreement nodig.

Zonder internet NTP servers heb je een lokale tijdsbron nodig:

# Chrony als lokale NTP server
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: chrony
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: chrony
  template:
    spec:
      hostNetwork: true
      containers:
      - name: chrony
        image: harbor.internal/infra/chrony:4.3
        volumeMounts:
        - name: chrony-conf
          mountPath: /etc/chrony
      volumes:
      - name: chrony-conf
        configMap:
          name: chrony-config
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: chrony-config
data:
  chrony.conf: |
    # Wanneer verbonden, gebruik publieke NTP
    server time.cloudflare.com iburst
    server pool.ntp.org iburst

    # Wanneer losgekoppeld, wees de tijdsbron
    local stratum 10
    allow 10.0.0.0/8

    # Drift file voor accuratesse wanneer losgekoppeld
    driftfile /var/lib/chrony/drift

De sleutel: local stratum 10 laat de server als tijdsbron fungeren zelfs zonder upstream. Nodes syncen met elkaar, behouden relatieve consistentie.

Component 4: Lokale Certificate Authority

TLS overal betekent certificaten overal. In verbonden omgevingen praat cert-manager met Let’s Encrypt. Offline is dat onmogelijk.

Oplossing: Draai je eigen CA.

# Self-signed root CA
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: internal-ca
spec:
  ca:
    secretName: internal-ca-root

---
# Maak het root certificaat
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: internal-ca-root
  namespace: cert-manager
spec:
  isCA: true
  commonName: Internal Root CA
  secretName: internal-ca-root
  duration: 87600h  # 10 jaar
  privateKey:
    algorithm: ECDSA
    size: 256
  issuerRef:
    name: selfsigned
    kind: ClusterIssuer

Distribueer de root CA naar alle nodes en clients. Nu geeft cert-manager certificaten uit vanuit je interne CA zonder externe calls.

Component 5: GitOps Zonder Internet

ArgoCD synct normaal van GitHub of GitLab. Offline heb je een lokale Git server nodig.

Optie 1: Embedded GitLab

Draai GitLab in-cluster. Zwaar, maar full-featured.

Optie 2: Gitea

Lichtgewicht Git server, perfect voor edge:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gitea
spec:
  template:
    spec:
      containers:
      - name: gitea
        image: harbor.internal/gitea/gitea:1.21
        env:
        - name: GITEA__server__OFFLINE_MODE
          value: "true"

ArgoCD wijst naar git.internal in plaats van github.com. Wanneer verbonden, push je wijzigingen. ArgoCD synct lokaal of je nu verbonden bent of niet.

Component 6: Helm Charts Offline

Helm charts pullen normaal van remote repositories. Offline heb je lokale chart storage nodig.

ChartMuseum biedt een Helm repository API:

# Push charts naar lokale museum wanneer verbonden
helm push mychart-1.0.0.tgz http://chartmuseum.internal

# ArgoCD gebruikt lokale charts
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
spec:
  source:
    repoURL: http://chartmuseum.internal
    chart: mychart
    targetRevision: 1.0.0

De Pre-Deployment Checklist

Voordat je looskoppelt, verifieer:

#!/bin/bash
# offline-readiness-check.sh

echo "=== Checking Offline Readiness ==="

# 1. Alle images lokaal beschikbaar
echo "Checking images..."
for image in $(kubectl get pods -A -o jsonpath="{..image}" | tr ' ' '\n' | sort -u); do
  if ! curl -s "https://harbor.internal/v2/${image}/manifests/latest" > /dev/null; then
    echo "MISSING: $image"
  fi
done

# 2. DNS resolvet intern
echo "Checking DNS..."
for name in harbor.internal git.internal ntp.internal; do
  if ! nslookup $name > /dev/null 2>&1; then
    echo "DNS FAIL: $name"
  fi
done

# 3. Tijd is gesynchroniseerd
echo "Checking NTP..."
chronyc tracking | grep -q "Leap status.*Normal" || echo "TIME SYNC ISSUE"

# 4. Certificaten zijn geldig
echo "Checking certificates..."
kubectl get certificates -A -o jsonpath='{range .items[*]}{.metadata.name}: {.status.conditions[0].type}{"\n"}{end}'

# 5. GitOps is synced
echo "Checking ArgoCD..."
argocd app list | grep -v "Synced.*Healthy" && echo "APPS NOT SYNCED"

echo "=== Check Complete ==="

Real-World Scenario’s

Scenario 1: Schip op Zee

Een containerschip draait Kubernetes voor cargo management. Satellietconnectiviteit is duur en onbetrouwbaar.

Architectuur:

  • K3s voor lage resource footprint
  • Harbor met volledige mirror van benodigde images
  • Lokale GitOps met Gitea
  • Sync updates wanneer in de haven

De catch: Updates gebeuren in batches tijdens havenaanloop. Het systeem moet weken stabiel draaien tussen syncs.

Scenario 2: Fabrieksvloer

Een productiefabriek draait Kubernetes voor automatisering. Beveiligingsbeleid verbiedt internetconnectiviteit.

Architectuur:

  • Volledige Kubernetes met air-gapped installatie
  • USB-gebaseerde updates (gesigned en geverifieerd)
  • Apart management netwerk voor zeldzame updates

De catch: Updates vereisen fysieke aanwezigheid. Elke wijziging moet bulletproof zijn.

Scenario 3: Mijn Homelab

Ik draai mijn infrastructuur om island-mode capable te zijn. Niet omdat ik disconnectie verwacht, maar omdat het goed ontwerp afdwingt.

Voordelen:

  • Geen externe dependencies = geen externe failure modes
  • Snellere pulls vanuit lokale registry
  • Werkt tijdens ISP storingen
  • Werkelijk soevereine infrastructuur

De Trade-offs

Offline Kubernetes is niet gratis:

AspectVerbondenOffline
UpdatesContinuBatched
Vulnerability infoReal-timeVertraagd
Externe integratiesMakkelijkOnmogelijk
Operationele lastLagerHoger
SoevereiniteitGedeeltelijkCompleet

De vraag is niet “wat is beter” — het is “wat heb je nodig?”

K3s: Gebouwd voor de Edge

K3s verdient speciale vermelding. Het is ontworpen voor precies deze use case:

  • Enkele binary, ~100MB
  • Embedded SQLite (geen externe etcd nodig)
  • Werkt op ARM (Raspberry Pi, edge devices)
  • Minimale dependencies
# Air-gapped K3s installatie
curl -sfL https://get.k3s.io > install.sh

# Op air-gapped machine
INSTALL_K3S_SKIP_DOWNLOAD=true \
INSTALL_K3S_EXEC="--private-registry /etc/rancher/k3s/registries.yaml" \
./install.sh

Met een lokale registry geconfigureerd, draait K3s volledig offline.

Mijn Aanbeveling

  1. Ontwerp voor offline, draai verbonden. Zelfs als je nooit loskoppelt, is de architectuur beter.

  2. Mirror alles. Container images, Helm charts, Git repos. Als het extern is, mirror het.

  3. Test disconnectie. Trek daadwerkelijk de netwerkkabel eruit en kijk wat breekt. Je zult verrast zijn.

  4. Automatiseer de sync. Wanneer verbonden, update mirrors automatisch. Vertrouw niet op handmatige processen.

  5. Documenteer dependencies. Weet precies wat je cluster van de buitenwereld nodig heeft.


Island mode gaat niet over paranoia. Het gaat over het begrijpen van je dependencies. Een systeem dat je offline kunt draaien is een systeem dat je werkelijk begrijpt.