OpenBao HA auf k3s — 3 stacked Nodes auf Ubuntu 24.04
Summary: End-to-End-Anleitung von drei nackten Ubuntu-24.04-Servern bis zum funktionsfähigen OpenBao-HA-Cluster auf k3s. Topologie: drei stacked Nodes, jeder gleichzeitig Control-Plane (mit embedded etcd) und Worker. Inkl. kube-vip für die API-VIP, k3s’ eingebautem local-path-Provisioner als StorageClass, cert-manager für die TLS-PKI und der OpenBao-Helm-Installation mit Raft, Service-Registration und Pod-Anti-Affinity. Jeder Befehl ausgeschrieben mit „was und warum”.
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. k3s- und kube-vip-Schritte folgen den jeweiligen Upstream-Docs (docs.k3s.io, kube-vip.io) — diese liegen nicht im raw/-Ordner.
Last updated: 2026-05-20
0 — Warum k3s und für wen ist diese Anleitung
k3s ist die Light-Variante von Rancher/SUSE: ein einziges Go-Binary unter 70 MB, mit containerd, Flannel, kube-proxy, local-path-provisioner, Traefik und CoreDNS eingebaut. Im Vergleich:
| Aspekt | kubeadm | k3s | RKE2 |
|---|---|---|---|
| Footprint | mittel | klein | mittel |
| Container-Runtime | manuell | embedded | embedded |
| CNI default | keiner | Flannel | Canal/Cilium |
| Storage default | keiner | local-path | keiner |
| Ingress default | keiner | Traefik | nginx |
| etcd default | extern | sqlite (HA: embedded etcd) | embedded etcd |
| Zielgruppe | DIY/lernen | Edge, kleine Cluster, Lab | Enterprise, regulated |
| Anleitung | k8s-ha-from-scratch | diese Seite | rke2-ha-setup |
Diese Seite ist die richtige Wahl, wenn:
- Ein kleines, kompaktes HA-Cluster auf 3 VMs reicht.
- Ressourcen knapp sind (z. B. 4 GB RAM pro Node, Edge-Hardware).
- Eine schnelle Bring-Up-Zeit wichtiger ist als Enterprise-Hardening.
Wenn Pod-Trennung in dedizierte Master/Worker-Nodes gewünscht ist (Compliance, Lasttrennung): rke2-ha-setup nehmen.
1 — Architektur
Drei Ubuntu-24.04-Server, jeder zugleich k3s-Server (Control-Plane + embedded etcd) und Workload-Host. Eine virtuelle IP via kube-vip sorgt dafür, dass der API-Endpoint trotz HA einen einzigen Namen hat.
┌──────────────────────┐
│ VIP 10.0.1.10 │
│ (kube-vip, │
│ port 6443) │
└──────────┬───────────┘
│
┌──────────────────────┼──────────────────────┐
│ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ node-1 │ │ node-2 │ │ node-3 │
│ 10.0.1.11 │ │ 10.0.1.12 │ │ 10.0.1.13 │
│ │ │ │ │ │
│ k3s server │ │ k3s server │ │ k3s server │
│ etcd │◀───────│ etcd │───────▶│ etcd │
│ apiserver │ raft │ apiserver │ raft │ apiserver │
│ kubelet │ │ kubelet │ │ kubelet │
│ flannel │ │ flannel │ │ flannel │
│ kube-vip │ │ kube-vip │ │ kube-vip │
│ openbao-0 │ │ openbao-1 │ │ openbao-2 │
└─────────────┘ └─────────────┘ └─────────────┘
Warum genau drei Nodes (analog zu allen anderen HA-Setups): etcd und OpenBao-Raft brauchen Quorum-Mehrheit. Mit drei Nodes überlebt der Cluster den Ausfall genau eines Nodes. Hintergrund: raft § Quorum.
Hardware-Empfehlung:
| Rolle | vCPU | RAM | Disk | Hinweis |
|---|---|---|---|---|
| Node (×3) | 2 | 4 GB | 50 GB SSD | k3s + OpenBao + Reserve. Bei stärkerer Workload: 4 vCPU / 8 GB. |
Netzwerk: alle drei Nodes im selben L2 (sonst funktioniert kube-vip im ARP-Mode nicht). Inter-Node-Ports: 6443 (apiserver), 2379–2380 (etcd), 10250 (kubelet), 8472/UDP (Flannel VXLAN), 51820/UDP optional (wenn Flannel mit WireGuard).
2 — Voraussetzungen vor dem ersten Befehl
| Variable | Beispiel | Bedeutung |
|---|---|---|
VIP | 10.0.1.10 | Freie VIP im selben Subnet wie die Nodes |
VIP_DNS | k3s-api.example.com | DNS-Name auf VIP (empfohlen — sonst nur IP in TLS-SANs) |
NODE_IP | 10.0.1.11/12/13 | LAN-IPs der drei Nodes |
K3S_TOKEN | Zufalls-String | Shared Secret für Cluster-Join |
K3S_VERSION | v1.32.2+k3s1 | Konkrete Version pinnen |
Token einmal generieren und in Passwortmanager ablegen:
openssl rand -hex 32
/etc/hosts auf allen drei Nodes:
sudo tee -a /etc/hosts <<EOF
10.0.1.10 k3s-api.example.com
10.0.1.11 node-1
10.0.1.12 node-2
10.0.1.13 node-3
EOF
3 — Basis-Prep auf allen drei Nodes
k3s bringt fast alles mit — der Pre-Flight ist deutlich kürzer als bei kubeadm.
3.1 — System aktualisieren
sudo apt-get update
sudo apt-get -y upgrade
sudo apt-get -y install curl ca-certificates iptables vim
3.2 — Swap deaktivieren
sudo swapoff -a
sudo sed -i.bak '/ swap / s/^/#/' /etc/fstab
Warum: kubelet weigert sich per Default bei aktivem Swap zu starten. Gleicher Grund wie bei kubeadm und RKE2.
3.3 — ufw / Firewall
sudo systemctl disable --now ufw || true
sudo apt-get -y remove ufw || true
Warum: k3s setzt seine iptables-Regeln selbst; ufw würde sie überschreiben oder filtern. Wenn ufw zwingend bleiben muss (Compliance), alle Ports aus § 1 explizit freigeben.
3.4 — Hostnames
# auf node-1:
sudo hostnamectl set-hostname node-1
# analog für node-2 und node-3
3.5 — IPv4-Forwarding (sicherheitshalber)
k3s setzt das normalerweise selbst, aber explizit schadet nicht:
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/k3s.conf
sudo sysctl --system
4 — kube-vip für die API-VIP vorbereiten
Wir legen das kube-vip-Manifest in das Auto-Apply-Verzeichnis, das k3s direkt nach dem Start einliest. Auf allen drei Nodes:
sudo mkdir -p /var/lib/rancher/k3s/server/manifests
VIP=10.0.1.10
INTERFACE=$(ip route | awk '/default/ {print $5; exit}')
KUBE_VIP_VERSION=v0.8.2
sudo tee /var/lib/rancher/k3s/server/manifests/kube-vip.yaml >/dev/null <<EOF
apiVersion: v1
kind: ServiceAccount
metadata:
name: kube-vip
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: system:kube-vip-role
rules:
- apiGroups: [""]
resources: ["services", "services/status", "nodes", "endpoints"]
verbs: ["list", "get", "watch", "update"]
- apiGroups: ["coordination.k8s.io"]
resources: ["leases"]
verbs: ["list", "get", "watch", "update", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: system:kube-vip-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:kube-vip-role
subjects:
- kind: ServiceAccount
name: kube-vip
namespace: kube-system
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: kube-vip-ds
namespace: kube-system
spec:
selector:
matchLabels:
name: kube-vip-ds
template:
metadata:
labels:
name: kube-vip-ds
spec:
hostNetwork: true
serviceAccountName: kube-vip
tolerations:
- effect: NoSchedule
operator: Exists
- effect: NoExecute
operator: Exists
nodeSelector:
node-role.kubernetes.io/control-plane: "true"
containers:
- name: kube-vip
image: ghcr.io/kube-vip/kube-vip:${KUBE_VIP_VERSION}
imagePullPolicy: IfNotPresent
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
args: ["manager"]
env:
- name: vip_arp
value: "true"
- name: port
value: "6443"
- name: vip_cidr
value: "32"
- name: cp_enable
value: "true"
- name: cp_namespace
value: kube-system
- name: vip_leaderelection
value: "true"
- name: vip_leaseduration
value: "5"
- name: vip_renewdeadline
value: "3"
- name: vip_retryperiod
value: "1"
- name: address
value: "${VIP}"
- name: vip_interface
value: "${INTERFACE}"
EOF
Was hier passiert:
/var/lib/rancher/k3s/server/manifests/ist k3s’ Auto-Apply-Verzeichnis. Jede YAML-Datei darin wird beim Start gegen die API geapplied — saubere Möglichkeit, „Bootstrap-Workloads” Teil der Installation zu machen.vip_arp: "true": kube-vip announct die VIP per Gratuitous-ARP. Funktioniert ohne BGP/Router-Config, aber nur innerhalb desselben L2-Segments.vip_leaderelection: "true": nur einer der drei kube-vip-Instanzen hält die VIP zu jeder Zeit; bei Ausfall springt sie in 5–10 Sekunden weiter.nodeSelector: control-plane: in k3s sind alle drei Server gleichzeitig Control-Plane — alle drei sind kube-vip-Kandidaten.
Alternative ohne VIP (nicht empfohlen, aber funktioniert für PoC): direkt die IP von node-1 als API-Endpoint nehmen. Bei Ausfall von node-1 ist dann die K8s-API offline, obwohl der Cluster eigentlich weiterläuft. Für Production daher kube-vip nehmen.
5 — k3s auf node-1 (Cluster-Init)
Auf node-1 — und nur dort — die config.yaml mit cluster-init: true:
sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/config.yaml >/dev/null <<EOF
write-kubeconfig-mode: "0640"
token: ${K3S_TOKEN}
tls-san:
- k3s-api.example.com
- 10.0.1.10
- node-1
- node-2
- node-3
cluster-init: true
disable:
- traefik
EOF
Begründungen der Optionen:
token: das in Schritt 2 erzeugte Shared Secret. Muss auf allen drei Nodes identisch sein.tls-san: zusätzliche DNS-Namen und IPs im API-Server-Cert. Ohne diese Liste meckert jeder kubectl-Call über die VIP mitx509: certificate is valid for …, not k3s-api.example.com.cluster-init: true: macht diesen Node zum etcd-Bootstrap-Seed. Nur auf einem einzigen Node setzen — sonst wird ein zweiter, paralleler Cluster gebootet. Implizit aktiviert das den embedded-etcd-Backend (statt sqlite); für HA zwingend.disable: traefik: spart Ressourcen, weil wir keinen Ingress brauchen (Clients gehen direkt auf den OpenBao-Service). Wer Traefik will, weglassen.
Installieren:
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.32.2+k3s1 sh -
get.k3s.io schreibt automatisch eine Unit k3s.service und startet sie. Status prüfen:
sudo systemctl status k3s
sudo journalctl -u k3s -f
Sobald „k3s is up and running” oder ähnlich erscheint, mit Ctrl-C raus. Dann kubectl bedienen können:
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
sed -i "s/127.0.0.1/k3s-api.example.com/" ~/.kube/config
# k3s bringt kubectl als symlink mit
kubectl get nodes
Erwartet: node-1 mit Status Ready und Rollen control-plane,etcd,master.
6 — node-2 und node-3 joinen
Auf node-2 (und analog node-3):
sudo mkdir -p /etc/rancher/k3s
sudo tee /etc/rancher/k3s/config.yaml >/dev/null <<EOF
write-kubeconfig-mode: "0640"
token: ${K3S_TOKEN}
server: https://k3s-api.example.com:6443
tls-san:
- k3s-api.example.com
- 10.0.1.10
- node-1
- node-2
- node-3
disable:
- traefik
EOF
curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION=v1.32.2+k3s1 sh -
Unterschiede zu node-1:
- Kein
cluster-init: true— sonst startet ein zweiter Cluster. server: https://k3s-api.example.com:6443— über die VIP joinen, nicht über die IP von node-1. Wenn node-1 später ausfällt, schadet das dem Join-Vorgang neuer Nodes nicht.
Nach 1–2 Minuten auf node-1:
kubectl get nodes
Erwartet: drei Nodes, alle Ready, alle mit Rollen control-plane,etcd,master.
7 — Cluster-Verifikation
kubectl get nodes -o wide
kubectl get pods -A
kubectl describe node node-1 | grep -A3 Taints
Erwartet bei describe node:
Taints: <none>
k3s tainted Server-Nodes nicht — sie laufen sofort auch als Worker. Das ist genau, was wir bei drei stacked Nodes wollen.
VIP-Failover testen (empfohlen):
# auf einer Workstation:
ping -c 5 k3s-api.example.com
# danach: k3s auf dem aktuell VIP-haltenden Node stoppen
sudo systemctl stop k3s
# nach ~5–10 s muss die VIP auf einen anderen Node gewandert sein
ping -c 5 k3s-api.example.com
# k3s wieder starten
sudo systemctl start k3s
8 — StorageClass prüfen
k3s bringt local-path als Default-StorageClass mit — kein Helm-Install nötig:
kubectl get storageclass
Erwartet: local-path (default).
Caveat: local-path ist Node-lokal. Fällt der Node weg, ist auch das Volume weg. Für 3-Node-OpenBao mit Raft ist das OK — Raft repliziert die Daten ohnehin auf allen drei Pods, jede Pod-PVC liegt auf einem anderen Node (durchgesetzt durch Pod-Anti-Affinity in § 11). Bei Verlust eines Nodes übernimmt einer der überlebenden zwei. Für andere Workloads ohne eigene Replikation wäre Longhorn die bessere Wahl — siehe Production-Hardening in § 15.
9 — Helm und cert-manager installieren
9.1 — Helm
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
9.2 — cert-manager
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
kubectl -n cert-manager get pods
Erwartet: drei Pods Running (cert-manager, cainjector, webhook).
10 — TLS-PKI für OpenBao via cert-manager
OpenBao spricht TLS auf dem Client-Port 8200 und mTLS auf dem Raft-Inter-Node-Port 8201. Beide decken wir mit einem Server-Cert ab, das alle nötigen DNS-Namen als SANs trägt (source: raw/docs/platform/k8s/helm/examples/ha-tls.md, raw/docs/platform/k8s/helm/examples/standalone-tls.md).
Namespace anlegen:
kubectl create namespace openbao
10.1 — Selbst-signierte Root-CA
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
secretName: openbao-ca-secret
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
name: selfsigned-bootstrap
kind: Issuer
EOF
Warum zwei Issuer: Der selfSigned-Issuer bootstrapt die CA, kann selbst aber keine regulären Server-Certs ausstellen. Den eigentlichen Server-Cert signiert ein zweiter Issuer vom Typ ca, der den frischen privaten Schlüssel der CA verwendet.
10.2 — CA-Issuer
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
10.3 — Server-Cert mit allen SANs
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
renewBefore: 720h
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:
openbao*— der reguläre ClusterIP-Service.openbao-internal*und*.openbao-internal*— der Headless-Service plus die per-Pod-DNS-Namen (openbao-0.openbao-internal.openbao.svc.cluster.local), die Raft beim Join braucht.openbao-active/openbao-standby— die Selector-Services aus kubernetes-service-registration.127.0.0.1— damitbaolokal im Pod viahttps://127.0.0.1:8200funktioniert (Liveness-Probes etc.).
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, tls.key und ca.crt.
11 — OpenBao Helm-Values
helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update
(source: raw/docs/platform/k8s/helm/terraform.md)
values-ha.yaml schreiben:
cat > values-ha.yaml <<'EOF'
global:
tlsDisable: false
server:
image:
repository: "openbao/openbao"
tag: "2.0.1"
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" {}
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
# 1 Pod pro Node — bei drei Nodes und drei Replikas: 1:1
affinity: |
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: {{ template "openbao.name" . }}
app.kubernetes.io/instance: "{{ .Release.Name }}"
component: server
topologyKey: kubernetes.io/hostname
dataStorage:
enabled: true
size: 10Gi
storageClass: local-path
extraEnvironmentVars:
BAO_CACERT: /openbao/userconfig/openbao-server-tls/ca.crt
serviceAccount:
create: true
ui:
enabled: true
serviceType: ClusterIP
EOF
Die wichtigen Punkte:
ha.replicas: 3— drei Pods, einer pro Node.- Drei explizite
retry_join-Blöcke mitleader_tls_servername="openbao": jeder Pod versucht beim Start alle drei möglichen Leader. Der TLS-Servername-Override zwingt den TLS-Code, gegen den CNopenbaozu verifizieren, nicht gegen den per-Pod-DNS ausleader_api_addr(sonst x509-Mismatch, source:raw/docs/platform/k8s/helm/examples/ha-tls.md). service_registration "kubernetes" {}: Pod-Labels werden gepflegt, die Selector-Servicesopenbao-active/openbao-standbybekommen Endpoints. Voraussetzungen erfüllt das Chart selbst. Hintergrund: kubernetes-service-registration. Falls das Setup hängt: service-registration-reactivation.podAntiAffinity: required: höchstens ein OpenBao-Pod pro Node. Mit drei Replikas auf drei Nodes: 1:1. Ein vierter Pod (wenn jemandreplicas: 4setzt) bleibt für immerPending— bewusst, weil 4 Voter keine sinnvolle Raft-Größe sind (raft § Quorum).storageClass: local-path: nutzt k3s’ eingebauten Provisioner. Volume liegt auf dem jeweiligen Node, OpenBao-Raft repliziert die Daten zwischen den Pods.
12 — Installieren
helm install openbao openbao/openbao \
--namespace openbao \
--values values-ha.yaml
kubectl -n openbao get pods -o wide -w
Erwartet: drei Pods openbao-0, openbao-1, openbao-2, einer pro Node. Status anfangs Running 0/1 (laufen, aber sealed → nicht ready). Ctrl-C raus, sobald alle drei laufen.
13 — Cluster initialisieren und unsealen
kubectl -n openbao exec -ti openbao-0 -- bao operator init \
-key-shares=5 \
-key-threshold=3 \
-tls-skip-verify
Output sofort wegspeichern: 5 Unseal-Keys + 1 Initial Root Token. Verlust = Cluster für immer tot.
Pod-für-Pod unsealen (drei Keys pro Pod):
for POD in openbao-0 openbao-1 openbao-2; do
for KEY in <UNSEAL_KEY_1> <UNSEAL_KEY_2> <UNSEAL_KEY_3>; do
kubectl -n openbao exec -ti ${POD} -- bao operator unseal ${KEY}
done
done
Warum jeder Pod separat: Unseal-Status wird nicht über Raft repliziert — by design, damit ein kompromittierter Knoten nicht automatisch über das Storage-Replikat geunsealed wird. Mehr in seal-unseal.
Mit den retry_join-Blöcken verbinden sich openbao-1 und openbao-2 automatisch mit openbao-0, sobald sie unsealed sind. Kein manueller bao operator raft join.
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
Service-Registration-Labels:
kubectl -n openbao get pods -L openbao-active,openbao-sealed,openbao-version -o wide
Erwartet: genau ein Pod hat openbao-active=true, alle drei openbao-sealed=false.
Active-Service-Endpoint:
kubectl -n openbao get endpoints openbao-active
Erwartet: eine Pod-IP — die des Leaders.
Externer Smoketest:
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
Erwartet: Sealed: false, HA Mode: active.
Failover-Test:
LEADER_POD=$(kubectl -n openbao get pods -l openbao-active=true -o jsonpath='{.items[0].metadata.name}')
kubectl -n openbao delete pod ${LEADER_POD}
sleep 15
kubectl -n openbao get pods -L openbao-active
kubectl -n openbao get endpoints openbao-active
Erwartet: ein anderer Pod ist jetzt openbao-active=true, das openbao-active-Endpoint zeigt auf diesen.
15 — Production-Hardening
- Auto-Unseal statt manuellem Shamir: Cloud-KMS oder Transit-Seal eines zweiten OpenBao-Clusters. Sonst bleibt jeder Pod nach Restart sealed. Siehe seal-unseal.
- Audit-Devices: mindestens zwei mit verschiedenen Sinks. Ein blockierendes einzelnes Audit-Device stoppt OpenBao komplett. Siehe audit.
- Snapshot-CronJob: Raft-Snapshots automatisch nach extern wegschreiben. Siehe backups.
- NetworkPolicies: in k3s ist Flannel ohne NetworkPolicy-Support — entweder Calico-NetworkPolicies separat installieren oder die Default-CNI auf Cilium tauschen (k3s-Option
--flannel-backend=none --disable-network-policy+ Cilium-Helm-Install). Wichtig: Egress zur K8s-API erlauben, sonst hängt service_registration — siehe service-registration-reactivation § 2. - etcd-Backup: k3s schreibt etcd-Snapshots automatisch nach
/var/lib/rancher/k3s/server/db/snapshots/(alle 12 h default). Diese auf extern wegspiegeln. Siehe k3s-Upstream-Doku. - Persistenz upgraden: für Workloads ohne eigene Replikation Longhorn nachinstallieren. OpenBao selbst kommt mit
local-pathaus, weil Raft die Replikation erledigt. - kube-vip BGP-Mode: bei Multi-Subnet-Setup ARP durch BGP ersetzen.
- Resource-Requests/Limits für OpenBao setzen:
server.resources.requests.{cpu,memory}— sonst kann ein Memory-Spike den ganzen Node ausknocken (k3s-Server, Workload und kube-vip teilen sich denselben Node).
16 — Stolperfallen
cluster-init: trueauf mehreren Nodes: Fataler Fehler. Zwei parallele Cluster, beide denken sie sind allein, etcd findet sich nie. Lösung: alle k3s-Installationen deinstallieren (k3s-uninstall.sh),cluster-init: truenur auf einem Node, neu beginnen.- node-2 startet, bleibt aber
NotReady: meist falscher Token in/etc/rancher/k3s/config.yamloder VIP noch nicht stabil.sudo journalctl -u k3s | tail -50zeigt den eigentlichen Fehler. - kube-vip-Pod startet nicht mit „interface not found”:
INTERFACEin Schritt 4 zeigt auf das falsche Interface.ip aauf dem Node prüfen. - OpenBao-Pods
Pendingmit „node(s) had volume node affinity conflict”: beilocal-pathist das Volume an einen spezifischen Node gebunden. Wenn ein Pod gerade auf einen anderen Node verschoben werden soll (z. B. nach Node-Restart), aber das alte PVC noch an den toten Node gepinnt ist, hängt der Pod. Lösung: PVC löschen, Pod neu schedulen, neue PVC entsteht. Funktioniert, weil Raft die Daten ohnehin auf den anderen zwei Pods hat. x509: certificate signed by unknown authoritybeim Raft-Join:leader_tls_servernamefehlt oder passt nicht zumcommonNameaus Schritt 10.3. CN muss exaktopenbaosein.- k3s-Upgrade via
apt: k3s wird nicht über apt aktualisiert — es ist gar nicht über apt installiert. Stattdessen erneutcurl get.k3s.io | INSTALL_K3S_VERSION=… shmit der höheren Version auf jedem Node einzeln. - Traefik kommt zurück nach Helm-Update: wenn
disable: traefikvergessen wird, deployt k3s den Traefik-Manifest aus/var/lib/rancher/k3s/server/manifests/traefik.yamlautomatisch wieder. Ggf. zusätzlich die Datei löschen. - VAULT-/BAO-Adressvariablen vermischen:
BAO_ADDRist OpenBao-nativ. Wer aus dem Vault-Ökosystem kommt: nichtVAULT_ADDRsetzen — viele OpenBao-Releases akzeptieren beides, aber die offizielle Variable istBAO_*. Siehe commands-cli.
Related pages
- k8s-ha-setup — die kompakte Helm-Variante ohne Cluster-Bring-up
- k8s-ha-from-scratch — End-to-End mit
kubeadmstatt k3s (mehr DIY, mehr Hardening-Spielraum) - rke2-ha-setup — End-to-End mit RKE2, 3 Master + 3 Worker (Enterprise-Variante)
- kubernetes-platform — Helm-Chart, Injector, VSO, CSI im Überblick
- kubernetes-service-registration — Pod-Labels, RBAC, Active-Service
- service-registration-reactivation — wenn die Stanza wegen TLS-Hang ausgeschaltet werden musste
- high-availability — Leader-/Standby-Mechanik, Request-Forwarding
- raft — Konsens, Quorum, Recovery
- seal-unseal — Shamir vs. Auto-Unseal
- backups — Snapshot-Strategie
- upgrading — HA-Upgrade-Reihenfolge
- deployment-vm-vs-k8s — Entscheidungshilfe K8s vs. VM-HA