OpenBao HA auf Kubernetes — End-to-End auf Ubuntu 24.04
Summary: Komplette Anleitung von drei nackten Ubuntu-24.04-Servern bis zu einer funktionsfähigen 3-Node-OpenBao-HA-Installation mit Raft, TLS und Service-Registration. Jeder Befehl ist ausgeschrieben; jeder Schritt erklärt was gemacht wird und warum. Endet bei einem produktionsnahen Setup, das noch durch Auto-Unseal, Audit und Snapshots gehärtet werden kann.
Sources: raw/docs/platform/k8s/helm/examples/ha-with-raft.md, raw/docs/platform/k8s/helm/examples/ha-tls.md, raw/docs/platform/k8s/helm/examples/standalone-tls.md, raw/docs/platform/k8s/helm/configuration.md, raw/docs/platform/k8s/helm/run.md, raw/docs/configuration/storage/raft.md, raw/docs/configuration/service-registration/kubernetes.md. K8s- und Ubuntu-spezifische Schritte folgen den jeweiligen Upstream-Docs (kubernetes.io, cilium.io, cert-manager.io) — diese liegen nicht im raw/-Ordner.
Last updated: 2026-05-20
0 — Architektur und Ziel
Drei Ubuntu-24.04-Server, jeder zugleich Kubernetes-Control-Plane- und Worker-Node (Stacked-etcd-HA). Auf jedem Node läuft genau ein OpenBao-Pod, alle drei bilden einen Raft-Cluster.
┌─────────────────────────────────────────────────────────────────┐
│ 3 × Ubuntu 24.04 │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ node-1 │ │ node-2 │ │ node-3 │ │
│ │ │ │ │ │ │ │
│ │ kubelet │ │ kubelet │ │ kubelet │ │
│ │ containerd │ │ containerd │ │ containerd │ │
│ │ kube-apisrv │ │ kube-apisrv │ │ kube-apisrv │ │
│ │ etcd │ │ etcd │ │ etcd │ │
│ │ Cilium agent│ │ Cilium agent│ │ Cilium agent│ │
│ │ openbao-0 │ │ openbao-1 │ │ openbao-2 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └──── Raft (8201) ───┴──── Raft (8201) ───┘ │
└─────────────────────────────────────────────────────────────────┘
Warum genau drei Nodes? Raft braucht für Quorum eine Mehrheit der Voter. Mit drei Knoten überlebt der Cluster den Ausfall eines Knotens — ein Knoten allein wäre Standalone, zwei Knoten wären „Split-Brain-anfällig”, weil 1/2 kein Quorum bildet. Hintergrund: raft § Quorum. Fünf Knoten wäre die nächste sinnvolle Stufe; vier sind nie sinnvoll, weil sie nur einen Ausfall verkraften wie drei, aber doppelt so viele Schreib-Replikationen brauchen.
Hardware-Empfehlung pro Node:
- 2 vCPU, 4 GB RAM, 30 GB Disk minimum für den PoC.
- 4 vCPU, 8 GB RAM, 100 GB SSD für Production (etcd + OpenBao + Workload-Reserve).
- Netzwerk: alle drei Nodes erreichen sich auf TCP 6443 (kube-apiserver), 2379–2380 (etcd), 10250 (kubelet), 8200/8201 (OpenBao) sowie UDP 8472 oder TCP 4240 (Cilium-VXLAN bzw. Health).
1 — Voraussetzungen vor dem ersten Befehl
Auf allen drei Nodes:
- Frisches Ubuntu 24.04 LTS Server.
- Root- oder sudo-Zugriff.
- Statische IPs.
- Eindeutige Hostnames (
node-1,node-2,node-3im Folgenden). - Eine Methode, dass alle Nodes sich namentlich erreichen — DNS oder
/etc/hosts.
Was du dir vor dem ersten sudo notieren solltest:
| Variable | Beispielwert | Erklärung |
|---|---|---|
NODE1_IP, NODE2_IP, NODE3_IP | 10.0.1.11, 10.0.1.12, 10.0.1.13 | LAN-IPs der drei Nodes |
CONTROL_PLANE_ENDPOINT | k8s-api.example.com:6443 oder 10.0.1.11:6443 | DNS-Name oder IP unter dem alle Nodes die K8s-API erreichen. Wenn DNS verfügbar: lieber DNS, weil sonst der Hostname mit der IP eines spezifischen Nodes verknüpft ist — ein Cluster auf nur einer einzigen IP ist nicht HA. |
POD_CIDR | 10.244.0.0/16 | CIDR für Pod-Netz, darf sich nicht mit dem Host-Netz überschneiden |
SVC_CIDR | 10.96.0.0/12 | CIDR für Service-IPs (kubeadm-Default) |
2 — Basis-Prep auf allen drei Nodes
Diese Schritte auf node-1, node-2 und node-3 in dieser Reihenfolge ausführen. Sie bereiten den Kernel und das Filesystem so vor, dass kubelet startet — fehlt einer der Schritte, scheitert später kubeadm init mit einer kryptischen Fehlermeldung.
2.1 — System aktualisieren
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get -y install ca-certificates curl gnupg apt-transport-https lsb-release
Warum: Kernel-Updates können Module umbenennen; Apt-Tools brauchen wir gleich für die Kubernetes-Repos.
2.2 — Swap deaktivieren
sudo swapoff -a
sudo sed -i.bak '/ swap / s/^/#/' /etc/fstab
Warum: kubelet weigert sich per Default zu starten, wenn Swap aktiv ist — der Scheduler kann sonst nicht zuverlässig RAM-Reservierungen einhalten, weil Pods in den Swap rutschen können. Es gibt einen --fail-swap-on=false-Flag, aber das ist in Produktion nicht empfehlenswert.
2.3 — Kernel-Module laden
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf
overlay
br_netfilter
EOF
sudo modprobe overlay
sudo modprobe br_netfilter
Warum: overlay ist der Backing-Filesystem-Driver für containerd, br_netfilter verbindet iptables mit dem Linux-Bridge-Subsystem — beide werden für Pod-zu-Pod-Traffic und für kube-proxy/CNI gebraucht. Die modules-load.d-Datei sorgt dafür, dass sie auch nach Reboots geladen sind.
2.4 — sysctl-Parameter setzen
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system
Warum: Damit Pakete zwischen Pods auf demselben Host und zwischen Hosts geroutet werden, muss IP-Forwarding an sein. Die Bridge-Hooks sorgen dafür, dass Pakete, die über eine Linux-Bridge laufen, auch durch iptables/nftables und damit durch kube-proxy gesehen werden.
2.5 — Hostname und /etc/hosts
Auf node-1:
sudo hostnamectl set-hostname node-1
Analog node-2 auf node-2, node-3 auf node-3.
Auf allen drei Nodes:
sudo tee -a /etc/hosts <<EOF
10.0.1.11 node-1
10.0.1.12 node-2
10.0.1.13 node-3
EOF
(IPs durch eure echten ersetzen.)
Warum: kubeadm und etcd nutzen Hostnames für die TLS-Zertifikate ihrer Cluster-Member. Ohne stabile Namens-zu-IP-Auflösung führt jeder Restart zu Cert-Mismatches.
3 — Container Runtime: containerd
Kubernetes spricht nicht direkt mit Docker — der Container-Runtime-Layer ist seit K8s 1.24 standardmäßig containerd (oder CRI-O). Wir nehmen containerd, weil es auf Ubuntu im offiziellen Repo liegt und am wenigsten Wartung braucht.
3.1 — Installation
Auf allen Nodes:
sudo apt-get update
sudo apt-get -y install containerd
3.2 — Default-Config schreiben
sudo mkdir -p /etc/containerd
sudo containerd config default | sudo tee /etc/containerd/config.toml >/dev/null
Warum: Frisches containerd hat keine config.toml. Die default-Config ist die Basis, in die wir gleich eine einzige Änderung eintragen.
3.3 — Systemd-Cgroup-Driver aktivieren
sudo sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml
sudo systemctl restart containerd
sudo systemctl enable containerd
Warum: Ubuntu 24.04 nutzt systemd als cgroup-Manager. Kubernetes erwartet, dass die Container-Runtime denselben cgroup-Manager nutzt wie das Init-System. Steht hier false (cgroupfs-Driver), gerät der Cluster in „inconsistent cgroup driver”-Fehler beim ersten Pod-Start.
4 — Kubernetes-Komponenten installieren
Auf allen Nodes — wir nehmen die offizielle Community-Quelle pkgs.k8s.io, gepinnt auf eine Minor-Version.
K8S_MINOR=v1.32
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/Release.key \
| sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/${K8S_MINOR}/deb/ /" \
| sudo tee /etc/apt/sources.list.d/kubernetes.list
sudo apt-get update
sudo apt-get -y install kubelet kubeadm kubectl
sudo apt-mark hold kubelet kubeadm kubectl
Warum apt-mark hold: unattended-upgrades würde sonst eine Minor-Version mitziehen, sobald sie verfügbar ist. K8s-Upgrades müssen aber versioniert und gestaffelt über kubeadm upgrade plan laufen, nicht über Apt — sonst geht der Cluster mitten in der Nacht hops.
5 — Cluster-Bootstrap auf node-1
Nur auf node-1:
sudo kubeadm init \
--pod-network-cidr=10.244.0.0/16 \
--control-plane-endpoint=node-1:6443 \
--upload-certs
Was passiert hier:
kubeadmlegt etcd, kube-apiserver, kube-controller-manager und kube-scheduler als statische Pods unter/etc/kubernetes/manifests/an —kubeletstartet sie nach 10–20 Sekunden.- Die TLS-PKI für die Control-Plane wird generiert und unter
/etc/kubernetes/pki/abgelegt. --control-plane-endpointlegt fest, unter welchem DNS-Namen die HA-Control-Plane erreichbar ist — wenn ihr später einen Load Balancer vor die drei API-Server stellt (empfohlen), tragt hier dessen DNS-Namen ein, nicht die IP von node-1. Mit nur einer Eintragung gibt es bei Ausfall von node-1 keine API-Verfügbarkeit.--upload-certspackt die PKI in ein verschlüsseltes Secret (kubeadm-certs) im Cluster, das genau 2 Stunden gültig ist. Mit dem mit ausgegebenen--certificate-keykönnen node-2 und node-3 als zusätzliche Control-Plane joinen ohne manuellen Cert-Transport.
Am Ende gibt kubeadm init zwei wichtige Befehle aus — diese Ausgabe wegspeichern:
You can now join any number of the control-plane node by running:
kubeadm join node-1:6443 --token … --discovery-token-ca-cert-hash sha256:… \
--control-plane --certificate-key …
Then you can join any number of worker nodes by running:
kubeadm join node-1:6443 --token … --discovery-token-ca-cert-hash sha256:…
5.1 — kubectl für den admin-User einrichten
Auf node-1 (oder eurer Workstation, wenn ihr /etc/kubernetes/admin.conf dorthin kopiert):
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes
kubectl get nodes zeigt jetzt node-1 mit Status NotReady — das ist erwartet, weil noch kein CNI installiert ist (siehe Schritt 7).
6 — node-2 und node-3 als Control-Plane joinen
Auf node-2:
sudo kubeadm join node-1:6443 \
--token <token-aus-init-output> \
--discovery-token-ca-cert-hash sha256:<hash-aus-init-output> \
--control-plane \
--certificate-key <cert-key-aus-init-output>
Dasselbe analog auf node-3.
Wenn der --certificate-key älter als 2 Stunden ist, generiert ihr auf node-1 einen neuen:
sudo kubeadm init phase upload-certs --upload-certs
Anschließend auf node-1:
kubectl get nodes
Erwartet: drei Nodes, alle NotReady mit Rolle control-plane.
6.1 — Taints entfernen, damit Workloads auf Control-Plane-Nodes laufen
In einem reinen Drei-Node-Setup sind alle Knoten gleichzeitig Worker. Per Default verbietet ein Taint das Schedulen normaler Pods auf Control-Plane-Nodes — der muss runter:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-
Warum: ohne diesen Befehl bleibt der OpenBao-StatefulSet hängen, weil kein Worker-Knoten zum Schedulen existiert. In größeren Clustern (≥5 Nodes) lässt man den Taint stehen und stellt die OpenBao-Pods bewusst nur auf dedizierte Worker.
7 — CNI: Cilium installieren
Ohne CNI bleibt jeder Pod isoliert. Wir nehmen Cilium, weil es eBPF-basiert ist, NetworkPolicies sauber kann (relevant für service-registration-reactivation später) und mit kubeadm gut zusammenspielt.
7.1 — Helm installieren
Falls Helm noch nicht da ist (auf node-1 oder eurer Workstation):
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg >/dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" \
| sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get -y install helm
7.2 — Cilium installieren
helm repo add cilium https://helm.cilium.io/
helm repo update
helm install cilium cilium/cilium \
--namespace kube-system \
--set kubeProxyReplacement=true \
--set k8sServiceHost=node-1 \
--set k8sServicePort=6443 \
--set ipam.mode=kubernetes \
--set ipam.operator.clusterPoolIPv4PodCIDRList='{10.244.0.0/16}'
Was hier passiert:
kubeProxyReplacement=trueersetztkube-proxydurch Ciliums eBPF-Lösung — weniger iptables-Last und sauberere NetworkPolicy-Semantik. Wer das nicht will, lässt den Schalter auffalse; dann musskube-proxyals DaemonSet weiterlaufen.ipam.mode=kubernetesliest Pod-CIDRs aus den Node-Spec-Feldern (spec.podCIDR), diekubeadm initmit dem Flag aus Schritt 5 gefüllt hat.
Verifikation:
kubectl -n kube-system get pods -l k8s-app=cilium
kubectl get nodes
Erwartet: drei Cilium-Pods im Status Running, alle drei Nodes auf Ready.
8 — Storage-Provisioner: local-path-provisioner
OpenBao mit Raft braucht persistente Storage pro Pod (server.dataStorage). Auf einem Standalone-Cluster ohne externes Storage-System ist local-path-provisioner von Rancher die einfachste Lösung — er macht aus einem Verzeichnis auf dem Node ein dynamisches PV.
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml
kubectl annotate storageclass local-path storageclass.kubernetes.io/is-default-class=true
Verifikation:
kubectl get storageclass
Erwartet: local-path (default).
Caveat: Bei local-path lebt das Volume auf dem Node — fällt der Node weg, ist auch das Volume weg. Für PoC und Lab in Ordnung, weil Raft die Daten ohnehin auf den anderen zwei Pods replikiert. Für ernsthaften Betrieb stattdessen Longhorn oder ein externes Storage-Backend nehmen. Hintergrund-Diskussion: storage und backups.
9 — cert-manager installieren
cert-manager managt X.509-Zertifikate als K8s-Ressourcen — wir erzeugen damit gleich eine eigene CA und ein Server-Cert für OpenBao.
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.15.3 \
--set crds.enabled=true
Verifikation:
kubectl -n cert-manager get pods
Erwartet: drei Pods (cert-manager, cert-manager-cainjector, cert-manager-webhook), alle Running.
10 — TLS-PKI für OpenBao mit cert-manager
OpenBao hat zwei TLS-Pfade:
- Client-API (Port 8200) — alle Clients sprechen TLS.
- Inter-Node-Raft (Port 8201) — die drei Pods sprechen mTLS untereinander, sonst scheitert
bao operator raft joinan einem CN-Mismatch (source:raw/docs/platform/k8s/helm/examples/ha-tls.md).
Beide Pfade decken wir mit einem Server-Cert ab, das alle nötigen DNS-Namen als SANs trägt.
Namespace anlegen:
kubectl create namespace openbao
10.1 — Selbst-signierte Root-CA erzeugen
cat <<'EOF' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: selfsigned-bootstrap
namespace: openbao
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: openbao-ca
namespace: openbao
spec:
isCA: true
commonName: openbao-ca
duration: 87600h # 10 Jahre
secretName: openbao-ca-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-bootstrap
kind: Issuer
EOF
Was hier passiert:
- Der Bootstrap-Issuer ist ein “selfSigned”-Issuer — der signiert die nächste Stufe (die CA) mit dem privaten Schlüssel des Subjekts selbst.
- Die CA-Certificate-Ressource erzeugt ein CA-Schlüsselpaar; cert-manager legt das Ergebnis als Secret
openbao-ca-secretab. Dieses Secret enthälttls.crt(= CA-Cert),tls.key(= CA-Schlüssel) undca.crt.
10.2 — Issuer für Server-Zertifikate
Aus dem CA-Secret bauen wir einen Issuer vom Typ ca — der signiert die eigentlichen Server-Certs für OpenBao:
cat <<'EOF' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: openbao-ca-issuer
namespace: openbao
spec:
ca:
secretName: openbao-ca-secret
EOF
Warum zwei Issuer? Der selfSigned-Issuer kann keine separaten Server-Certs ausstellen — er ist dafür gedacht, eine CA zu bootstrappen. Die signierten Server-Certs kommen vom ca-Issuer, der den privaten Schlüssel der CA hat.
10.3 — Server-Certificate für OpenBao
cat <<'EOF' | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: openbao-server-tls
namespace: openbao
spec:
secretName: openbao-server-tls
duration: 8760h # 1 Jahr
renewBefore: 720h # 30 Tage vor Ablauf neu ausstellen
commonName: openbao
dnsNames:
- openbao
- openbao.openbao
- openbao.openbao.svc
- openbao.openbao.svc.cluster.local
- openbao-internal
- openbao-internal.openbao
- openbao-internal.openbao.svc
- openbao-internal.openbao.svc.cluster.local
- "*.openbao-internal"
- "*.openbao-internal.openbao"
- "*.openbao-internal.openbao.svc"
- "*.openbao-internal.openbao.svc.cluster.local"
- openbao-active
- openbao-active.openbao.svc.cluster.local
- openbao-standby
- openbao-standby.openbao.svc.cluster.local
ipAddresses:
- 127.0.0.1
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: openbao-ca-issuer
kind: Issuer
EOF
Was die SAN-Liste abdeckt (source: raw/docs/platform/k8s/helm/examples/standalone-tls.md für die SAN-Konvention):
openbao*— der reguläre ClusterIP-Service.openbao-internal*— der Headless-Service, über den die Pods sich gegenseitig per DNS sehen (openbao-0.openbao-internal.openbao.svc.cluster.local). Das Wildcard*.openbao-internal*deckt diese Pod-DNS-Namen ab.openbao-active/openbao-standby— die Selector-Services aus der Service Registration.127.0.0.1— damitbaoaus dem Pod heraus viahttps://127.0.0.1:8200funktioniert (z. B. für Liveness-Probes).
cert-manager rotiert das Cert automatisch 30 Tage vor Ablauf (renewBefore). Verifikation:
kubectl -n openbao get certificate openbao-server-tls
kubectl -n openbao describe certificate openbao-server-tls | grep -A2 Status
Erwartet: Ready: True.
Das Secret openbao-server-tls enthält jetzt:
tls.crt— Server-Cert.tls.key— privater Schlüssel.ca.crt— die Root-CA aus Schritt 10.1.
Genau diese drei Dateien hängt der Helm-Chart gleich als Volume in die OpenBao-Pods.
11 — OpenBao Helm-Repo und Values
helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update
helm search repo openbao/openbao
(source: raw/docs/platform/k8s/helm/terraform.md)
Jetzt die values-ha.yaml schreiben. Diese Datei ist das Herzstück — alle Stellschrauben für HA, TLS und Service-Registration leben hier:
cat > values-ha.yaml <<'EOF'
global:
tlsDisable: false
server:
image:
repository: "openbao/openbao"
tag: "2.0.1"
# 3-Pod-Setup mit Pod-Anti-Affinity:
# je ein OpenBao-Pod pro Node, sonst gehen bei Node-Ausfall
# potentiell zwei Pods gleichzeitig down.
ha:
enabled: true
replicas: 3
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/openbao/userconfig/openbao-server-tls/tls.crt"
tls_key_file = "/openbao/userconfig/openbao-server-tls/tls.key"
tls_client_ca_file = "/openbao/userconfig/openbao-server-tls/ca.crt"
}
storage "raft" {
path = "/openbao/data"
retry_join {
leader_api_addr = "https://openbao-0.openbao-internal:8200"
leader_tls_servername = "openbao"
leader_ca_cert_file = "/openbao/userconfig/openbao-server-tls/ca.crt"
leader_client_cert_file = "/openbao/userconfig/openbao-server-tls/tls.crt"
leader_client_key_file = "/openbao/userconfig/openbao-server-tls/tls.key"
}
retry_join {
leader_api_addr = "https://openbao-1.openbao-internal:8200"
leader_tls_servername = "openbao"
leader_ca_cert_file = "/openbao/userconfig/openbao-server-tls/ca.crt"
leader_client_cert_file = "/openbao/userconfig/openbao-server-tls/tls.crt"
leader_client_key_file = "/openbao/userconfig/openbao-server-tls/tls.key"
}
retry_join {
leader_api_addr = "https://openbao-2.openbao-internal:8200"
leader_tls_servername = "openbao"
leader_ca_cert_file = "/openbao/userconfig/openbao-server-tls/ca.crt"
leader_client_cert_file = "/openbao/userconfig/openbao-server-tls/tls.crt"
leader_client_key_file = "/openbao/userconfig/openbao-server-tls/tls.key"
}
}
service_registration "kubernetes" {}
# Cert-Secret als Volume mounten (cert-manager hat es gerade angelegt)
volumes:
- name: userconfig-openbao-server-tls
secret:
secretName: openbao-server-tls
volumeMounts:
- mountPath: /openbao/userconfig/openbao-server-tls
name: userconfig-openbao-server-tls
readOnly: true
# Pod-Anti-Affinity: höchstens ein OpenBao-Pod pro Node
affinity: |
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: {{ template "openbao.name" . }}
app.kubernetes.io/instance: "{{ .Release.Name }}"
component: server
topologyKey: kubernetes.io/hostname
# Persistenz für Raft
dataStorage:
enabled: true
size: 10Gi
storageClass: local-path
# bao binary findet die CA aus dem gemounteten Secret
extraEnvironmentVars:
BAO_CACERT: /openbao/userconfig/openbao-server-tls/ca.crt
# ServiceAccount mit Patch-Rechten auf Pods (für service_registration)
serviceAccount:
create: true
ui:
enabled: true
serviceType: ClusterIP
EOF
Die wichtigsten Punkte zur Begründung:
- Drei explizite
retry_join-Blöcke: Jeder Pod versucht beim Start alle drei möglichen Leader anzusprechen — solange einer erreichbar ist, joint der Pod. Das Setup mit drei Blöcken folgt Solution 1 aus der TLS-Doku (source:raw/docs/platform/k8s/helm/examples/ha-tls.md); siehe k8s-ha-setup § Production hardening. leader_tls_servername = "openbao"zwingt den TLS-Client-Code dazu, gegen den CN des Certs (openbao) zu verifizieren statt gegen den DNS-Namen aus demleader_api_addr(z. B.openbao-1.openbao-internal) — sonst x509-Mismatch, wie in der ha-tls-Quelle beschrieben.service_registration "kubernetes" {}ist aktiv: Pods werden mitopenbao-active/openbao-sealed/openbao-versiongelabelt; die Selector-Services bekommen Endpoints. Voraussetzungen (RBAC, Downward API) erfüllt das Chart selbst. Hintergrund: kubernetes-service-registration. Falls das Setup später hängt, siehe service-registration-reactivation.podAntiAffinityrequired…: schedult nie zwei OpenBao-Pods auf denselben Node. Ohne diese Regel könnten bei drei Replikas und drei Nodes zwei Pods auf node-1 landen — Quorum-Verlust bei einem einzigen Node-Ausfall.server.image.tag = "2.0.1": explizit pinnen, nichtlatest. Upgrade siehe upgrading.
12 — Installieren
helm install openbao openbao/openbao \
--namespace openbao \
--values values-ha.yaml
Sofort danach beobachten:
kubectl -n openbao get pods -w
Erwartet: drei Pods openbao-0, openbao-1, openbao-2. Status anfangs Running aber 0/1 Ready — das ist erwartet: die Pods laufen, sind aber sealed und damit nicht „bereit”. Erst nach bao operator init + unseal werden sie ready.
13 — Cluster initialisieren und unsealen
bao operator init darf nur einmal in der Lebenszeit eines Clusters ausgeführt werden (source: seal-unseal). Wir machen das auf openbao-0:
kubectl -n openbao exec -ti openbao-0 -- bao operator init \
-key-shares=5 \
-key-threshold=3 \
-tls-skip-verify
Was die Optionen tun:
-key-shares=5 -key-threshold=3— Shamir teilt den Master-Key in 5 Anteile auf; 3 davon genügen zum Unseal. Default ist 5/3, aber explizit zu setzen ist sauberer.-tls-skip-verify— erlaubt den lokalen TLS-Call ohne die CA mitzugeben. Alternativ kann manBAO_CACERTsetzen (haben wir oben gemacht) und das Flag weglassen.
Die Ausgabe enthält fünf Unseal-Keys (Base64) und einen Initial Root Token — sofort wegspeichern (Password-Manager oder verschlüsselte Datei), sonst ist der Cluster für immer verloren.
13.1 — Pods unsealen
Drei der fünf Unseal-Keys nacheinander durch bao operator unseal schicken — auf jedem Pod einzeln:
# openbao-0
kubectl -n openbao exec -ti openbao-0 -- bao operator unseal <UNSEAL_KEY_1>
kubectl -n openbao exec -ti openbao-0 -- bao operator unseal <UNSEAL_KEY_2>
kubectl -n openbao exec -ti openbao-0 -- bao operator unseal <UNSEAL_KEY_3>
# openbao-1
kubectl -n openbao exec -ti openbao-1 -- bao operator unseal <UNSEAL_KEY_1>
kubectl -n openbao exec -ti openbao-1 -- bao operator unseal <UNSEAL_KEY_2>
kubectl -n openbao exec -ti openbao-1 -- bao operator unseal <UNSEAL_KEY_3>
# openbao-2
kubectl -n openbao exec -ti openbao-2 -- bao operator unseal <UNSEAL_KEY_1>
kubectl -n openbao exec -ti openbao-2 -- bao operator unseal <UNSEAL_KEY_2>
kubectl -n openbao exec -ti openbao-2 -- bao operator unseal <UNSEAL_KEY_3>
Warum jeder Pod separat: Der Unseal-Status wird nicht über Raft repliziert — er ist ein Pod-lokaler Zustand. Das ist by design, damit ein kompromittierter Knoten nicht durch das Storage-Replikat automatisch unsealed wird. Mehr dazu in seal-unseal.
Mit retry_join aus values-ha.yaml verbinden sich openbao-1 und openbao-2 nach dem Unseal automatisch mit openbao-0 — kein manueller bao operator raft join nötig (das wäre nur bei Setup ohne retry_join nötig, wie in der Minimalanleitung k8s-ha-setup beschrieben).
14 — Verifikation
kubectl -n openbao exec -ti openbao-0 -- bao login <ROOT_TOKEN>
kubectl -n openbao exec -ti openbao-0 -- bao operator raft list-peers
Erwartet (source: raw/docs/platform/k8s/helm/examples/ha-with-raft.md):
Node Address State Voter
---- ------- ----- -----
openbao-0 openbao-0.openbao-internal:8201 leader true
openbao-1 openbao-1.openbao-internal:8201 follower true
openbao-2 openbao-2.openbao-internal:8201 follower true
Sind alle drei Pods Voter mit State leader/follower, ist Raft synchron.
Status-Labels von Service Registration prüfen:
kubectl -n openbao get pods -L openbao-active,openbao-sealed,openbao-version
Erwartet: genau ein Pod hat openbao-active=true, alle haben openbao-sealed=false, eine gesetzte Versions-Spalte. Hintergrund und Failover-Test: kubernetes-service-registration § 9.
Active-Service-Endpoint prüfen:
kubectl -n openbao get endpoints openbao-active
Erwartet: eine Pod-IP — die des aktuellen Leaders.
Von einer Workstation aus mit dem CA-Cert:
kubectl -n openbao get secret openbao-server-tls -o jsonpath='{.data.ca\.crt}' | base64 -d > openbao-ca.crt
kubectl -n openbao port-forward svc/openbao-active 8200:8200 &
BAO_ADDR=https://127.0.0.1:8200 BAO_CACERT=./openbao-ca.crt bao status
Sealed: false, HA Mode: active — der Cluster lebt.
15 — Production-Hardening (Kurzliste)
Das Setup oben ist funktionsfähig, aber noch nicht produktionsreif. Nächste Schritte, jeweils mit eigener Wiki-Seite:
- Auto-Unseal statt manuellem Shamir-Unseal beim Pod-Restart. Cloud-KMS (AWS/GCP/Azure) oder Transit-Seal eines zweiten OpenBao-Clusters. Siehe seal-unseal.
- Audit-Devices aktivieren — mindestens zwei mit verschiedenen Sinks; ein einzelnes blockierendes Audit Device legt den Cluster lahm. Siehe audit.
- Snapshots als CronJob automatisieren. Siehe backups.
- NetworkPolicies für Egress/Ingress zwischen Pods. Cilium kann das nativ. Wichtig: Egress zur K8s-API erlauben, sonst hängt service_registration — siehe service-registration-reactivation § 2.
- Persistenz auf echtes Storage umziehen —
local-pathist Node-lokal. Longhorn oder externes CSI. Siehe storage. - HA für die Control-Plane mit echtem LB. Der
--control-plane-endpoint=node-1:6443aus Schritt 5 ist Single-Point-of-Failure für die K8s-API; in echt einen kube-vip / HAProxy / Cloud-LB davorstellen. - VAULT_ADDR / BAO_ADDR der Clients konsequent auf
openbao-active:8200zeigen, nicht auf den Round-Robin-Service — Hintergrund: kubernetes-service-registration § 7.
16 — Typische Stolperfallen
kubeadm inithängt bei „[apiclient] Created API client”: meist swap nicht aus, oder cgroup-Driver-Mismatch zwischen kubelet und containerd. Beide aus Schritt 2.2 und 3.3 prüfen.- Pods bleiben
0/1 Readynach Unseal: Active-/Standby-Service-Endpoints prüfen. Wenn leer, ist meist service_registration deaktiviert oder die NetworkPolicy blockt den K8s-API-Egress — siehe service-registration-reactivation. x509: certificate signed by unknown authoritybeimbao operator raft join:leader_tls_servernamefehlt oder passt nicht zum CN aus Schritt 10.3. Der CN im Server-Cert istopenbao; genau diesen String braucht jederretry_join-Block.openbao-0startet,openbao-1undopenbao-2aber nicht: meist Anti-Affinity zu strikt und zu wenige Nodes. Vor demhelm installprüfen, dass alle drei NodesReadysind und gleichwertig sind (kein Taint, der die anderen ausschließt).- CA-Cert läuft nach 10 Jahren ab — und niemand denkt rechtzeitig dran:
renewBeforeder CA selbst auf z. B.8000hsetzen, sodass cert-manager rechtzeitig blökt. Oder die CA halt manuell rotieren, was sich durch alle Server-Certs durchzieht. - Initial Root Token verloren: mit dem
bao operator generate-root-Verfahren neu erzeugbar, solange ihr noch das Unseal-Key-Threshold habt. Siehe tokens.
Related pages
- k8s-ha-setup — die kompaktere Variante, ohne Cluster-Bring-up (setzt einen K8s-Cluster voraus)
- rke2-ha-setup — End-to-End-Variante mit RKE2 statt
kubeadm, 3 Master + 3 Worker, kube-vip VIP, Longhorn-Storage - k3s-ha-setup — End-to-End-Variante mit k3s, 3 stacked Nodes, leichtgewichtig (Flannel + local-path eingebaut)
- kubernetes-platform — Helm-Chart, Injector, VSO, CSI im Überblick
- kubernetes-service-registration — Pod-Labels, RBAC, Active-Service
- service-registration-reactivation — wenn der Workaround „Stanza auskommentiert” rückgängig gemacht wird
- high-availability — Leader-/Standby-Mechanik, Request Forwarding
- raft — Raft-Konsens, Quorum, Recovery
- seal-unseal — Shamir vs. Auto-Unseal, Recovery Keys
- backups — Snapshot-Strategie für Raft
- upgrading — HA-Reihenfolge beim Upgrade
- deployment-vm-vs-k8s — Entscheidungshilfe Kubernetes vs. VM-HA