secure k8 s

Raft — von Anfänger bis Profi

Summary: Ein didaktischer Durchgang durch Raft, das Konsens-Verfahren hinter OpenBaos Integrated Storage. Sechs Levels: das Problem, die Mechanik, die OpenBao-Integration, Betriebs-Tuning, Recovery, Antipatterns.

Sources: raw/docs/internals/integrated-storage.md, raw/docs/concepts/integrated-storage/index.md, raw/docs/concepts/integrated-storage/autopilot.md, raw/docs/configuration/storage/raft.md, raw/docs/commands/operator/raft.md, raw/docs/internals/telemetry/metrics/raft.md.

Last updated: 2026-05-19


Level 1 — Anfänger: das Problem

OpenBao speichert Secrets. Secrets dürfen weder verschwinden (Verfügbarkeit) noch sich widersprechen (Konsistenz). Daraus folgt direkt:

  • Ein einziger Server reicht nicht. Stirbt die Maschine, ist OpenBao weg. Single Point of Failure.
  • Mehrere unabhängige Server reichen auch nicht. Wenn alle gleichzeitig schreiben dürfen, können dieselben Secrets in zwei Versionen existieren — welche ist richtig?

Die Lösung muss zwei Eigenschaften zusammenbringen: mehrere Server und eine einzige Wahrheit. Genau das leistet ein Konsens-Algorithmus: Eine Gruppe von Knoten einigt sich darauf, in welcher Reihenfolge welche Änderungen passiert sind — auch wenn einzelne Knoten ausfallen oder das Netzwerk wackelt.

Raft ist ein solcher Konsens-Algorithmus. Er ist verwandt mit Paxos, aber bewusst auf Verständlichkeit hin entworfen (source: raw/docs/internals/integrated-storage.md). Das Originalpapier heißt nicht ohne Grund “Raft: In search of an Understandable Consensus Algorithm” (Ongaro & Ousterhout, 2014).

Im CAP-Sinne

Raft ist CP: bei einer Netzpartition entscheidet sich der Cluster für Konsistenz und akzeptiert eingeschränkte Verfügbarkeit. Wenn keine Mehrheit erreichbar ist, akzeptiert OpenBao keine Schreibvorgänge mehr — lieber kein Schreibvorgang als ein widersprüchlicher Schreibvorgang.

Die Grundidee in einem Satz

Aus einer Gruppe gleichberechtigter Server wird per Wahl ein Leader bestimmt. Nur der Leader nimmt Änderungen entgegen, repliziert sie an die anderen, und gilt eine Änderung erst dann als gültig, wenn sie auf einer Mehrheit der Server liegt.

Das ist der ganze Trick. Der Rest ist Detail.

Level 2 — Grundkenntnisse: wie Raft funktioniert

Drei Knotenzustände

Jeder Raft-Knoten ist immer in genau einem von drei Zuständen (source: raw/docs/internals/integrated-storage.md):

  • Follower — der Default. Nimmt Log-Einträge vom Leader an, antwortet auf dessen Heartbeats, stimmt bei Wahlen ab.
  • Candidate — Übergangszustand. Wenn ein Follower zu lange keinen Heartbeat sieht, erklärt er sich selbst zum Kandidaten und bittet die anderen um ihre Stimme.
  • Leader — der aktive Knoten. Nimmt Schreibanfragen an, schreibt sie in sein Log, repliziert sie an die Follower und entscheidet, wann sie committed sind.

In OpenBao ist der Raft-Leader gleichzeitig der active node, an dem Schreibvorgänge stattfinden; die Follower sind die standby nodes (source: raw/docs/internals/integrated-storage.md). Siehe high-availability.

Terms — die fortlaufende Wahlperiode

Jede Wahl bekommt eine streng monoton steigende Nummer, den Term. Terms sind das wichtigste Tie-Breaking-Werkzeug: bei jeder Nachricht zwischen Knoten wird der Term mitgeschickt. Ein Knoten mit einem niedrigeren Term akzeptiert sofort einen höheren — und tritt, falls er Leader war, zurück. Damit kann es nie zwei legitime Leader in derselben Wahlperiode geben.

Leader-Wahl

  1. Ein Follower bekommt für eine zufällige Zeit (Election-Timeout) keinen Heartbeat.
  2. Er erhöht den Term um eins, wird zum Candidate, stimmt für sich selbst und schickt RequestVote-RPCs an alle Peers.
  3. Bekommt er eine Mehrheit der Stimmen — auch seine eigene zählt — wird er Leader.
  4. Bekommt niemand eine Mehrheit (Stimmensplit), läuft der Timer ab, alle erhöhen den Term und versuchen es wieder. Die zufälligen Timeouts machen Patt-Situationen unwahrscheinlich.

Log-Replikation

Schreibvorgänge sind in Raft Log-Einträge. Der Leader hängt jeden neuen Eintrag an sein lokales Log, schickt ihn per AppendEntries-RPC an alle Follower und beobachtet die Bestätigungen.

Ein Eintrag heißt:

  • Committed, sobald er auf einer Mehrheit des Peer-Sets durabel auf Disk steht (source: raw/docs/internals/integrated-storage.md). Erst jetzt ist er sicher.
  • Applied, sobald er auf der State Machine (FSM) angewendet wurde — bei OpenBao bedeutet das: in der BoltDB-Datei eingetragen.

OpenBao-Schreibvorgänge blockieren, bis sowohl committed als auch applied sind. Das ist die Grundlage des “Linearizable”-Verhaltens.

Quorum — die Mehrheit zählt

Bei n Mitgliedern braucht das Quorum mindestens (n+1)/2 Knoten (source: raw/docs/internals/integrated-storage.md). Daraus folgt die berühmte Tabelle:

ServerQuorumFailure Tolerance
110
220
321
431
532
642
743

(Quelle: raw/docs/internals/integrated-storage.md)

Zwei Beobachtungen, die jeder Raft-Betreiber verinnerlicht haben muss:

  • Gerade Zahlen sind nie besser. 4 Knoten tolerieren genauso viele Ausfälle wie 3, aber sie machen Splitbrains wahrscheinlicher. Deshalb läuft Raft fast immer mit ungeraden Voter-Anzahlen.
  • Verfügbarkeit ist nicht gleich Skalierbarkeit. Mehr Knoten ⇒ mehr Ausfälle tolerierbar, aber nicht mehr Durchsatz, weil jeder Schreibvorgang weiterhin von einer Mehrheit bestätigt werden muss (und high-availability erläutert, dass die Standbys traditionell keine Reads bedient haben — das ändert sich erst ab v2.5.0).

Heartbeats und Failure Detection

Der Leader schickt regelmäßige leere AppendEntries als Heartbeats. Solange Follower diese sehen, bleiben sie Follower. Bleiben sie aus, beginnen die Election-Timer zu laufen — und der nächste Term beginnt.

Level 3 — Fortgeschritten: Raft in OpenBao

OpenBao implementiert Raft als Integrated Storage: die Persistenzschicht des Vaults ist gleichzeitig der Konsens-Cluster. Keine externe Datenbank, keine separates Consul — alles in einem Binary (source: raw/docs/internals/integrated-storage.md).

Anatomie

  • Log: geschrieben in BoltDB (raft.db in path).
  • FSM: ebenfalls BoltDB. Praktischer Nebeneffekt: Snapshots sind sehr leichtgewichtig, weil der FSM-State schon persistent ist; der Snapshot-Prozess muss “nur” das Raft-Log kürzen (source: raw/docs/internals/integrated-storage.md).
  • mTLS-Cluster-Port: Inter-Node-Traffic läuft über den Cluster-Port (Default 8201) und ist mit einem intern generierten Zertifikat geschützt, das regelmäßig rotiert wird (source: raw/docs/concepts/integrated-storage/index.md).
  • Barrier: Daten landen verschlüsselt in BoltDB. Der Storage-Layer sieht nur Chiffrate (siehe seal-unseal, internals).

Minimal-Konfiguration

storage "raft" {
  path    = "/var/raft/"
  node_id = "node1"
}
cluster_addr = "https://node1.openbao.local:8201"

(source: raw/docs/configuration/storage/raft.md)

Wichtig: cluster_addr ist bei Raft Pflicht — der Cluster muss wissen, wo seine Knoten miteinander sprechen. Eine separate ha_storage-Stanza ist neben raft nicht zulässig (source: raw/docs/configuration/storage/raft.md).

Der Join-Prozess

Wenn ein neuer Knoten dem Cluster beitritt, gibt es ein Henne-Ei-Problem: die Cluster-TLS-Zertifikate liegen im (noch nicht replizierten) Storage. Lösung — ein einmaliges Challenge/Answer über den API-Port statt über den Cluster-Port (source: raw/docs/concepts/integrated-storage/index.md):

  1. Der neue Knoten (node2) schickt eine Challenge-Anfrage an einen existierenden Knoten (node1).
  2. node1 antwortet mit einer verschlüsselten UUID + Seal-Konfiguration.
  3. node2 entschlüsselt die UUID — mit Auto-Unseal automatisch, mit Shamir-Seal nach manueller Eingabe der Unseal-Keys.
  4. node2 schickt die UUID zurück.
  5. node1 ist überzeugt, dass node2 die Seal-Konfiguration teilt, und schickt das Bootstrap-Paket: Cluster-TLS-Zertifikat + Key.
  6. node2 empfängt einen Raft-Snapshot über den Cluster-Port und ist nun voll Mitglied.

Konsequenz: Alle Knoten eines Clusters müssen denselben Seal verwenden (source: raw/docs/internals/integrated-storage.md). Auto-Unseal mit demselben KMS oder dieselben Shamir-Keys — etwas anderes geht nicht.

Join per CLI oder per Config

CLI:

$ bao operator raft join https://node1.openbao.local:8200

Config-basiert mit retry_join — robuster, weil das Binary beim Start automatisch versucht beizutreten (source: raw/docs/concepts/integrated-storage/index.md, raw/docs/configuration/storage/raft.md):

storage "raft" {
  path    = "/var/raft/"
  node_id = "node3"

  retry_join {
    leader_api_addr = "https://node1.openbao.local:8200"
  }
  retry_join {
    leader_api_addr = "https://node2.openbao.local:8200"
  }
}

Cloud-Auto-Join über go-discover ist ebenfalls möglich — eine retry_join-Stanza mit auto_join = "provider=aws region=eu-west-1 tag_key=openbao tag_value=..." lässt OpenBao Knoten anhand von Cloud-Tags entdecken (source: raw/docs/configuration/storage/raft.md).

Voter vs Non-Voter

Per Default sind beitretende Knoten Voter. Mit -non-voter (CLI) oder retry_join_as_non_voter = true (Config) wird ein Knoten zum Non-Voter: er bekommt den vollständigen Replikationsstrom, zählt aber nicht zum Quorum. Anwendungsfall: lese-skalieren oder einen Reserveknoten warmhalten, ohne die Quorum-Mathematik zu ändern (source: raw/docs/concepts/integrated-storage/index.md, raw/docs/commands/operator/raft.md).

Beförderung/Degradierung zur Laufzeit:

$ bao operator raft promote node1
$ bao operator raft demote  node1

(source: raw/docs/commands/operator/raft.md)

Autopilot — was du nicht selbst machen willst

Autopilot ist eine integrierte Steuerschicht (source: raw/docs/concepts/integrated-storage/autopilot.md) mit drei Funktionen:

  • Server Stabilization. Neu beigetretene Voter werden zunächst als Non-Voter aufgenommen, beobachtet (server_stabilization_time, Default 10 s) und erst nach stabilem Verhalten zum Voter befördert. Verhindert, dass ein wackeliger neuer Knoten Quorum gefährdet.
  • Dead Server Cleanup. Knoten, die länger als dead_server_last_contact_threshold (Default 24 h) keinen Leader-Kontakt hatten, werden automatisch aus der Konfiguration entfernt — sofern aktiv (cleanup_dead_servers = true) und min_quorum gesetzt ist (default aus).
  • State API. Detaillierter Health-View des Clusters in einem Aufruf — bao operator raft autopilot state.

Wichtige Defaults im Überblick (source: raw/docs/concepts/integrated-storage/autopilot.md):

ParameterDefaultBedeutung
cleanup_dead_serversfalsetote Knoten automatisch entfernen
dead_server_last_contact_threshold24hnach dieser Zeit gilt ein Knoten als tot
min_quorum(unset)Untergrenze für Pruning
max_trailing_logs1000wie weit ein Follower hinten sein darf
last_contact_threshold10swann ein Knoten als ungesund gilt
server_stabilization_time10swie lange ein neuer Knoten beobachtet wird

Beispiel autopilot state

Healthy:                      true
Failure Tolerance:            1
Leader:                       raft1
Voters:
   raft1
   raft2
   raft3
Non Voters:
   raft4

(source: raw/docs/commands/operator/raft.md)

Failure Tolerance ist die zentrale Kennzahl: wie viele Knoten dürften jetzt noch ausfallen, ohne das Quorum zu verlieren.

Level 4 — Profi: Betriebs-Tuning

Cluster-Sizing

Empfehlung der OpenBao-Reference-Architecture: 5 Knoten (Failure Tolerance 2) (source: raw/docs/internals/integrated-storage.md). Drei Knoten sind das absolute Minimum; ein einzelner Server in Produktion ist “highly discouraged — data loss is inevitable in a failure scenario” (source: raw/docs/internals/integrated-storage.md).

Skalieren ohne Quorum-Verlust

Die richtige Methode, einen 5-Knoten-Cluster zu erneuern, ist paarweise (source: raw/docs/internals/integrated-storage.md):

  1. Zwei neue Knoten dazustellen → 7 Knoten, Failure Tolerance 3.
  2. Warten, bis die neuen Knoten in Sync sind (gleicher Raft-Index wie der Leader).
  3. Zwei alte Knoten entfernen → wieder 5 Knoten.
  4. Wiederholen, bis alle Alten ersetzt sind.

So bleibt jederzeit eine Failure Tolerance ≥ 2 erhalten und die Voter-Anzahl ist immer ungerade.

Gefahr: mehrere Knoten gleichzeitig hinzufügen, die noch nicht in Sync sind — sie zählen für Quorum, können aber nicht abstimmen, und der Cluster verliert Quorum (source: raw/docs/internals/integrated-storage.md). Vor jedem weiteren Hinzufügen: bao status prüfen.

Raft-Timing-Tuning

performance_multiplier (source: raw/docs/configuration/storage/raft.md):

  • Default in OpenBao: 5 (für sparsame Maschinen).
  • Empfehlung Produktion: 1 — engste Timings, schnellste Leader-Detection, höhere CPU- und Netz-Anforderungen.
  • Max: 10 (sehr entspannt; nur für extrem ressourcen-arme Setups).

Niedriger ⇒ schneller, aber empfindlicher; höher ⇒ träger, aber genügsamer.

Snapshots und Log-Kompaktierung

Raft-interne Snapshots laufen automatisch (source: raw/docs/configuration/storage/raft.md):

  • snapshot_interval (Default 120 s) — wie oft Raft prüft, ob ein Snapshot fällig ist; gestaffelt zwischen Intervall und 2× Intervall, damit nicht alle Knoten gleichzeitig snapshotten.
  • snapshot_threshold (Default 8192) — minimale Anzahl von Commits zwischen Snapshots.
  • trailing_logs (Default 10 000) — Log-Einträge, die nach einem Snapshot stehen bleiben. Wenn Follower nach Reconnect nicht aufholen können (Log schon truncated), kann dieser Wert erhöht werden — oder besser: Write-Throughput senken.

Davon zu unterscheiden: operator-getriggerte Snapshots als Backup-Primitive (source: raw/docs/commands/operator/raft.md):

$ bao operator raft snapshot save  raft.snap
$ bao operator raft snapshot restore raft.snap

Auf Kubernetes wird das typischerweise als CronJob umgesetzt (siehe k8s-ha-setup). Ohne ein nutzbares Snapshot-Backup ist kein produktiver Betrieb möglich.

Entry-Größen-Limits

  • max_entry_size (Default 1 048 576 Byte = 1 MiB) — größter erlaubter Raft-Log-Eintrag. Größere Puts schlagen fehl.
  • max_transaction_size (Default 8 388 608 Byte = 8 MiB) — Obergrenze für Transaktionen; einzelne Operationen darin müssen weiterhin unter max_entry_size bleiben (source: raw/docs/configuration/storage/raft.md).

Wer sehr große Secrets speichert (PKI-Bundles, große Wrap-Tokens), muss diese Werte kennen.

Telemetrie, die ein Profi überwacht

Aus dem Raft-Metrikkatalog (source: raw/docs/internals/telemetry/metrics/raft.md) sind besonders relevant:

  • vault.raft.commitTime — wie lange ein Commit dauert. Steigt mit Disk-Latenz.
  • vault.raft.replication.appendEntries.rpc — Replikations-Latenz zu jedem Follower.
  • vault.raft.leader.lastContact — Zeit seit letztem Heartbeat eines Followers.
  • vault.raft_storage.follower.applied_index_delta — wie weit ein Follower hinter dem Leader liegt (Raft-Index-Delta).
  • vault.raft_storage.follower.last_heartbeat_ms — Latenz des letzten Heartbeats.
  • vault.raft_storage.stats.fsm_pending — Backlog im FSM-Apply.
  • vault.raft_storage.bolt.* — BoltDB-interne Metriken (Freelist, Spill, Rebalance) — wichtig bei wachsenden raft.db-Dateien.
  • vault.autopilot.healthy und vault.autopilot.failure_tolerance — der bequemste Single-Number-Health-Check.

Ein einfaches SLO: vault.autopilot.failure_tolerance >= 1 immer, >= 2 als gelb/grün-Schwelle bei 5 Knoten.

Operator-Cheat-Sheet

$ bao operator raft list-peers
$ bao operator raft join          <leader-api-addr>
$ bao operator raft remove-peer   <node-id>
$ bao operator raft promote       <node-id>
$ bao operator raft demote        <node-id>
$ bao operator raft snapshot save raft.snap
$ bao operator raft snapshot restore raft.snap
$ bao operator raft autopilot state
$ bao operator raft autopilot get-config
$ bao operator raft autopilot set-config -cleanup-dead-servers=true -min-quorum=3

(source: raw/docs/commands/operator/raft.md)

Level 5 — Recovery: wenn es ernst wird

Fall A — Quorum erhalten

Ein Knoten von dreien stirbt. Der Cluster läuft weiter (Failure Tolerance 1 → 0), aber er ist verwundbar.

Optionen (source: raw/docs/concepts/integrated-storage/index.md):

  1. Knoten reanimieren, gleicher Hostname/Adresse, Vault startet, Raft holt nach. Beste Option.
  2. Neuen Knoten provisionieren, bao operator raft join, mit Autopilot durchlaufen lassen.
  3. Alten Knoten explizit entfernen: bao operator raft remove-peer <id> — wichtig vor Schritt 2, falls der Knoten dauerhaft weg ist.

Achtung: Nach einem remove-peer muss der entfernte Knoten sein Raft-Datenverzeichnis löschen, bevor er erneut joinen darf (source: raw/docs/commands/operator/raft.md). Sonst gibt es Inkonsistenzen.

Fall B — Quorum verloren

Zwei von drei Knoten sind weg, oder drei von fünf. Der Cluster akzeptiert keine Schreibvorgänge mehr. Recovery-Optionen (source: raw/docs/concepts/integrated-storage/index.md):

  1. Tote Knoten reanimieren — selbe Adresse, einfach starten lassen. Wenn das geht: vorziehen.

  2. peers.json-Recovery — manueller Eingriff. Stop aller Knoten, dann in jedem Datenverzeichnis (<path>/raft/peers.json) eine identische JSON-Liste der überlebenden Knoten ablegen:

    [
      { "id": "node1", "address": "node1.openbao.local:8201", "non_voter": false },
      { "id": "node2", "address": "node2.openbao.local:8201", "non_voter": false }
    ]

    Alle Überlebenden mit identischer peers.json starten — einer wird Leader. Im Extremfall reicht ein einziger Knoten in der peers.json (source: raw/docs/concepts/integrated-storage/index.md).

    Wichtige Nebenwirkung: Die peers.json-Recovery committed implizit alle noch offenen Raft-Log-Einträge (source: raw/docs/concepts/integrated-storage/index.md). Das kann Daten enthalten, die vor dem Ausfall bewusst nicht committed waren. Nutze diese Methode nur als letzten Ausweg, niemals als geplanten Workflow.

  3. Snapshot-Restore — wenn die Datenkorruption tiefer geht, ist ein Restore aus einem snapshot save-File die definitive Lösung. Voraussetzung: das Snapshot existiert und der Seal/KMS, mit dem es verschlüsselt wurde, ist verfügbar — sonst lässt sich nichts unsealen (siehe seal-unseal, storage).

  4. Recovery Moderaw/docs/concepts/recovery-mode.md beschreibt einen Sondermodus für Reparaturen jenseits von Quorum-Problemen.

Backups: was du wirklich brauchst

Ein Restore funktioniert nur mit:

  • dem Snapshot-File (raft.snap),
  • der OpenBao-Konfiguration (Listener, Storage, Seal-Stanza),
  • Zugang zum Seal-Material (KMS, HSM, oder Shamir-Recovery-Keys).

Ein Snapshot alleine ist nutzlos — er enthält nur barrier-verschlüsselte Daten.

Level 6 — Antipatterns und Fallstricke

  • Single-node Production. Funktioniert formal, ist aber laut Doku ausdrücklich abgeraten — bei jedem Disk-Fehler ist der Vault weg (source: raw/docs/internals/integrated-storage.md).
  • Gerade Voter-Anzahlen. 4 Knoten geben die gleiche Failure Tolerance wie 3 Knoten, aber mehr Splitbrain-Risiko. Immer ungerade Voter halten — Non-Voter sind ok zum Ausgleich.
  • Mehrere neue Knoten gleichzeitig hinzufügen. Sie zählen fürs Quorum, bevor sie wirklich mitspielen können. In einem 3-Knoten-Cluster bedeutet “drei neue Knoten gleichzeitig” sehr wahrscheinlich Quorum-Verlust (source: raw/docs/internals/integrated-storage.md). Immer paarweise und einen nach dem anderen einlaufen lassen.
  • Unterschiedliche Seals beim Join. Der Challenge/Answer schlägt fehl — der Join-Prozess kann die UUID nicht entschlüsseln (source: raw/docs/internals/integrated-storage.md).
  • Auto-Join + TLS + IP SANs. go-discover liefert auf vielen Cloud-Plattformen IPs, nicht Hostnamen — die müssen entweder in den IP-SANs des Zertifikats stehen, oder du arbeitest mit leader_tls_servername (DNS-SAN matching) oder einem Load-Balancer als Frontend (source: raw/docs/concepts/integrated-storage/index.md).
  • peers.json als Routine-Werkzeug. Diese Datei ist eine Notfallpatrone, kein Konfigurations-Mechanismus. Automatisierungen, die sie regelmäßig schreiben, sind ein Vehikel für Datenverlust (source: raw/docs/concepts/integrated-storage/index.md).
  • Wiederbeitritt eines entfernten Knotens ohne Daten-Löschung. Nach remove-peer muss <path>/raft/ gelöscht werden, bevor der Knoten erneut joinen darf (source: raw/docs/commands/operator/raft.md).
  • Auto-Unseal in K8s vergessen. Auf Kubernetes ist Shamir-Unseal pro Pod-Restart operativ untragbar; Auto-Unseal ist faktisch verpflichtend (siehe k8s-ha-setup, deployment-vm-vs-k8s).
  • Standby-Reads vor v2.5.0 erwarten. Bis OpenBao v2.5.0 waren Followers reine Replikat-Speicher und haben keine Reads bedient — mehr Knoten gibt keinen Read-Throughput. Standby-Read-Serving ist erst ab v2.5.0 verfügbar (siehe high-availability).

Schnellnachschlag

  • Quorum bei n: (n+1)/2.
  • Failure Tolerance bei n Votern: n - quorum.
  • Empfohlene Produktionsgröße: 5 Voter.
  • Mindestens zwei Audit-Devices aktivieren.
  • Auto-Unseal verwenden (Cloud-KMS, HSM, transit).
  • Snapshots regelmäßig sichern und Restore mindestens einmal üben.
  • Auf autopilot.failure_tolerance alarmieren.