Back to Hub

cargo.ayedo.cloud/ayedo/k8s/openbao

Registry block

polycrate block pull cargo.ayedo.cloud/ayedo/k8s/openbao:0.3.4

cargo.ayedo.cloud/ayedo/k8s/openbao

Deploys OpenBao — den community-getriebenen Open-Source-Fork von HashiCorp Vault — via dem offiziellen Helm-Chart.

Unterstützt zwei Betriebsmodi: - standalone — einzelne Instanz, Datei-Storage, kein HA - ha — Raft-basierter HA-Cluster, ausschließlich Integrated Raft Storage (kein Consul)


Konfiguration

Alle Optionen liegen unter config.bao.* im Block oder in der workspace.poly.

Minimal — Standalone

- name: openbao
  from: cargo.ayedo.cloud/ayedo/k8s/openbao:0.3.0
  config:
    bao:
      namespace: openbao
      mode: standalone
      readiness:
        mode: strict   # after first install; omit or bootstrap on greenfield
      datastorage:
        size: 10Gi
        storage_class: "longhorn"
      ingress:
        enabled: true
        classname: nginx
        clusterissuer: letsencrypt-production
        host: vault.example.com

Produktiv — HA mit Raft (3 Replicas)

- name: openbao
  from: cargo.ayedo.cloud/ayedo/k8s/openbao:0.3.0
  config:
    bao:
      namespace: openbao
      mode: ha
      readiness:
        mode: strict
      ha:
        replicas: 3
      datastorage:
        size: 20Gi
        storage_class: "longhorn"
      auditstorage:
        enabled: true
        size: 10Gi
        storage_class: "longhorn"
      backup:
        enabled: true
        schedule: "0 */6 * * *"
        pvc:
          size: 5Gi
          storage_class: "longhorn"
      resources:
        requests:
          cpu: 250m
          memory: 256Mi
        limits:
          cpu: "1"
          memory: 1Gi
      ingress:
        enabled: true
        classname: nginx
        clusterissuer: letsencrypt-production
        host: vault.example.com
      vmpodscrape:
        enabled: true
        namespace: victoria-metrics-stack

Alle Konfigurationsoptionen

Schlüssel Default Beschreibung
bao.namespace openbao Kubernetes-Namespace
bao.mode standalone Betriebsmodus: standalone oder ha
bao.image.registry quay.io Image-Registry
bao.image.repository openbao/openbao Image-Repository
bao.image.tag "" Image-Tag (leer = Chart appVersion)
bao.injector.replicas 2 Anzahl Agent-Injector-Pods
bao.webhook.namespaceselector.matchexpressions [] Webhook Namespace-Selector
bao.tolerations [] Tolerations für Server-Pods
bao.tls.enabled false TLS zwischen Vault-Pods (intern)
bao.ha.replicas 3 Anzahl Server-Pods im HA-Modus
bao.datastorage.size 10Gi PVC-Größe für Raft-Datenspeicher
bao.datastorage.storage_class "" StorageClass (leer = Cluster-Default)
bao.auditstorage.enabled false Separates PVC für Audit-Logs anlegen
bao.auditstorage.size 10Gi PVC-Größe für Audit-Logs
bao.auditstorage.storage_class "" StorageClass für Audit-PVC
bao.ingress.enabled false Ingress für die UI/API anlegen
bao.ingress.classname nginx IngressClass
bao.ingress.clusterissuer letsencrypt-production cert-manager ClusterIssuer
bao.ingress.host "" Hostname für den Ingress
bao.resources {} CPU/Memory-Requests und Limits
bao.vmpodscrape.enabled false VMPodScrape für VictoriaMetrics anlegen
bao.vmpodscrape.namespace victoria-metrics-stack Namespace des VMPodScrape-Objekts
bao.readiness.mode bootstrap bootstrap oder strict (siehe Erstinstallation)
bao.readiness.force_bootstrap false true verhindert Auto-Upgrade auf strict
bao.backup.enabled false Snapshot-Store/CronJob (HA) bzw. Live-FSB (standalone)
bao.backup.standalone_strategy data-volume-velero Standalone: Velero FSB des Data-PVC
bao.chart.version 0.27.2 Helm-Chart-Version
bao.operator_init siehe block.poly Nach Helm: idempotenter operator init, JSON-Artefakt
bao.unseal.unseal_keys [] Unseal-Keys (z. B. via secrets.poly); Action unseal
bao.setup siehe unten KV-Engines, AppRoles, Policies — Action setup
bao.oidc siehe block.poly Optional OIDC-Auth — Action setup

Erstinstallation (Runbook)

  1. Workspace: mode: standalone|ha, StorageClass, optional Ingress. bao.readiness.mode weglassen oder bootstrap (Default).
  2. polycrate run openbao install — Helm → Wait auf Pod Running → operator init → Bootstrap-Unseal → automatischer Helm-Upgrade auf strict Readiness (außer force_bootstrap: true).
  3. Unseal-Keys / Root-Token aus block.artifacts.secrets/.../operator-init.json offline sichern; für Folge-Unseals in secrets.poly unter bao.unseal.unseal_keys spiegeln.
  4. In workspace.poly dauerhaft setzen: bao.readiness.mode: strict (Source of Truth für Re-Installs).
  5. Optional: polycrate run openbao setup / setup-ceph-kms.
  6. Optional Backups: - HA: polycrate run openbao setup-backup (Admin/Root-Token wie bei setup / setup-ceph-kms: bao.setup.token oder bao.backup.token), dann bao.backup.enabled: true und erneut install. Die Action schreibt die Snapshot-Policy und legt ein eigenes Token im Secret openbao-snapshot-token an. Velero sichert Snapshot-Store und die data-Volumes der Server-StatefulSets (Cluster defaultVolumesToFsBackup: true). - Standalone: bao.backup.enabled: true (Data-PVC via Velero Node-Agent / FSB; Live-Copy-Risiko akzeptieren). Namespace in Velero-Schedule aufnehmen.

Readiness und Alerting

  • bootstrap: sealed/uninitialized zählen als Ready (Bootstrap ohne Deadlock).
  • strict: sealed → Pod not Ready → bestehendes not-Ready-Alerting greift.
  • Metriken allein sind als Seal-Signal ungeeignet (fehlen oft statt 0).
  • Wartung/Re-Init: polycrate run openbao enable-bootstrap-readiness bzw. force_bootstrap: true; danach wieder enable-strict-readiness.

Re-Install / Upgrade (Cluster bereits unsealed)

  • readiness.mode: strict in der Workspace belassen.
  • install wartet weiter auf Running; strict Probes sind ok, solange unsealed.
  • Chart-/App-Upgrades: install erneut ausführen (idempotent).

KV-Engines, Policies und AppRoles (Action setup)

Voraussetzungen:

  • OpenBao ist installiert, initialisiert und unsealed (siehe unten: install inkl. operator_init, ggf. Action unseal).
  • bao.setup.token: Root- oder Admin-Token (üblicherweise Merge aus secrets.poly).
  • BAO_ADDR / VAULT_ADDR: Polycrate-Runner muss die API erreichen — Standard ist https://<bao.ingress.host> wenn Ingress aktiv ist, sonst bao.setup.api_address setzen. Die Action setzt für die CLI beide Präfixe (BAO_* und die in der OpenBao-Doku genannten VAULT_*-Variablen), damit Token und Adresse zuverlässig erkannt werden.

Die Action setup:

  1. Legt die unter bao.setup.engines konfigurierten KV-Mounts an (bao secrets enable … kv, idempotent bei „path already in use“).
  2. Aktiviert AppRole (bao auth enable approle).
  3. Pro Eintrag in bao.setup.secrets: ACL-Policy <name>-policy (Lesen/Liste nur für den angegebenen Pfad im referenzierten Engine) und AppRole auth/approle/role/<name> mit TTL aus approle.
  4. Optional bao.oidc (siehe Kommentare in block.poly).

KV v2: Die generierte Policy erlaubt read unter …/data/<path> sowie list unter …/metadata/<path> (inkl. Unterpfade). KV v1: read+list auf …/<path>.

Beispiel — workspace.poly (öffentlich) + sensible Werte in secrets.poly

workspace.poly (Auszug):

- name: openbao
  from: cargo.ayedo.cloud/ayedo/k8s/openbao:0.2.1
  config:
    bao:
      namespace: openbao
      mode: ha
      ingress:
        enabled: true
        host: vault.example.com
        classname: nginx
        clusterissuer: letsencrypt-production
      setup:
        token: ""           # wird aus secrets.poly gemerged
        engines:
          - name: secrets
            path: secrets
            version: 2
            description: "Application KV v2"
        secrets:
          - name: billing-api
            engine: secrets
            path: billing/api
            approle:
              token_ttl: "1h"
              token_max_ttl: "8h"
          - name: reports-worker
            engine: secrets
            path: reports/worker
            approle:
              token_ttl: "2h"
              token_max_ttl: "24h"

secrets.poly (Auszug, gleiches blocks-Schema):

blocks:
  - name: openbao
    config:
      bao:
        setup:
          token: "s.xxxxxxxxxxxxx"   # Root- oder Admin-Token

Ausführen:

polycrate run openbao setup

Im Output erscheinen die RoleIDs je AppRole.

SecretID erzeugen: OpenBao/Vault modelliert das als write auf den Pfad auth/approle/role/<name>/secret-id — es wird dabei eine neue SecretID ausgestellt (kein read). -f („force“) vermeidet eine interaktive Eingabe. Vorher einloggen bzw. Token setzen — in der Doku heißen die Variablen VAULT_ADDR / VAULT_TOKEN; bao akzeptiert je nach Version auch BAO_ADDR / BAO_TOKEN.

export VAULT_ADDR=https://vault.example.com
# oder: export BAO_ADDR=…
bao login   # oder: export VAULT_TOKEN=s.xxx  bzw. BAO_TOKEN=…

bao write -f auth/approle/role/billing-api/secret-id
# Ausgabe enthält u. a. secret_id (ggf. -field=secret_id)

bao read -field=role_id auth/approle/role/billing-api/role-id

Policies in OpenBao anlegen: Die Action legt nur die Policies aus secrets[] an (<name>-policy). Wenn OIDC-Rollen andere Policy-Namen referenzieren (z. B. openbao-kv), müsst ihr diese Policies separat definieren (manuell, anderes Tool, oder eigene secrets[]-Einträge mit passenden Pfaden).

Entfernen: polycrate run openbao setup-uninstall löscht AppRoles und die zugehörigen *-policy-Einträge; KV-Mounts bleiben bestehen.


Nach der Installation: Initialisierung und Unseal

Der Block führt nach install optional operator init aus (siehe bao.operator_init) und schreibt root_token und Unseal-Keys nach block.artifacts.secrets (operator-init.json). Zum Unseal aller Replicas: polycrate run openbao unseal (Keys aus Artefakt oder bao.unseal.keys, z. B. aus secrets.poly).

Manuell (falls ohne Block-Automation), z. B. Standalone:

kubectl exec -n openbao openbao-0 -- bao operator init
kubectl exec -n openbao openbao-0 -- bao operator unseal <key-1>
kubectl exec -n openbao openbao-0 -- bao operator unseal <key-2>
kubectl exec -n openbao openbao-0 -- bao operator unseal <key-3>

HA (3 Replicas) — manuell

kubectl exec -n openbao openbao-0 -- bao operator init
for pod in openbao-0 openbao-1 openbao-2; do
  kubectl exec -n openbao $pod -- bao operator unseal <key-1>
  kubectl exec -n openbao $pod -- bao operator unseal <key-2>
  kubectl exec -n openbao $pod -- bao operator unseal <key-3>
done

Unseal Keys und Root Token sicher außerhalb des Clusters aufbewahren. Ohne Quorum an Unseal-Keys bleibt OpenBao dauerhaft sealed.

Nach Pod-Neustarts erneut unsealen, sofern kein Auto-Unseal (KMS etc.) genutzt wird.


HA-Skalierungspattern und Node-Anforderungen

Wie Raft-HA funktioniert

OpenBao HA mit Raft ist ein Consensus-Cluster: ein Pod ist der aktive Leader, alle anderen sind Follower (Standby). Writes gehen immer an den Leader, Reads können je nach Konfiguration auch von Standbys bedient werden. Bei einem Leader-Ausfall wählen die verbleibenden Pods via Raft einen neuen Leader.

Für einen stabilen Consensus-Cluster gilt die Quorum-Regel: mehr als die Hälfte der Pods müssen erreichbar sein.

Empfohlene Cluster-Größen

Replicas Tolerierbarer Ausfall Quorum Empfohlene Nodes
1 0 1 1 (kein HA)
3 1 2 3
5 2 3 5

3 Replicas auf 3 Nodes ist der Standard für Produktionsumgebungen. 5 Replicas bieten mehr Resilienz, sind aber für die meisten Workloads überdimensioniert.

Weniger Nodes als Replicas (z.B. 3 Pods auf 2 Nodes)

Das Helm-Chart setzt standardmäßig podAntiAffinity mit requiredDuringSchedulingIgnoredDuringExecution — d.h. Kubernetes verweigert das Scheduling, wenn nicht genug unterschiedliche Nodes vorhanden sind.

Für Umgebungen mit weniger Nodes (z.B. Testcluster, Cost-optimized Setups) kann das auf preferredDuringSchedulingIgnoredDuringExecution gelockert werden. Das geht aktuell über server.affinity im Helm-Chart — dieser Wert kann über block.poly nicht direkt gesetzt werden, sondern müsste als Anpassung im values.yml.j2-Template erfolgen.

PodDisruptionBudget (PDB)

Das Chart erstellt bei mode: ha automatisch ein PDB, das sicherstellt, dass immer mindestens (replicas/2)+1 Pods verfügbar sind. Bei 3 Replicas: mindestens 2 Pods müssen immer laufen.

Konsequenz bei Rolling Updates oder Node-Drains: - Bei 3 Replicas auf 3 Nodes kann immer nur 1 Node gleichzeitig gedrained werden. - Bei 2 Nodes mit 3 Replicas schlägt ein Node-Drain in der Regel fehl, weil das PDB verletzt würde.

Das PDB kann über den Helm-Wert server.ha.disruptionBudget.enabled: false deaktiviert werden — das ist nur für Testumgebungen sinnvoll.

Upgrade von Standalone auf HA (Pfad A — Bestandsdaten behalten)

mode: standalone nutzt storage "file", mode: ha nutzt storage "raft". Das sind inkompatible Backends. Ein Config-Toggle in workspace.poly allein reicht nicht. Offizielle Operation: Offline bao operator migrate (file → raft), danach Peers per Chart-retry_join joinen. Quelle: operator migrate.

Pfad A ist verbindlich, wenn Transit/KV/Policies erhalten bleiben müssen (z. B. Ceph RGW SSE-S3). Neu-Init (Pfad B) erzeugt neue Shamir-Keys und macht bestehende SSE-Objekte unlesbar, sofern die Transit-Keys nicht exportierbar sind.

Erprobt 2026-08-28 auf aycloud-hel1-satellite-1 (1 → 5 Replicas). Dieselbe Wartung ist für aycloud-platform-1 vorgesehen. Immer -w als absoluten Workspace-Pfad übergeben.

Voraussetzungen

  • kubectl-Context auf den Zielcluster
  • Block cargo.ayedo.cloud/ayedo/k8s/openbao ≥ 0.3.3 (dieses Runbook + funktionierendes setup-backup + Snapshot-kubectl cp)
  • Unseal-Keys / Root-Token aus artifacts/secrets/openbao/operator-init.json oder bao.unseal.unseal_keys (nicht committen, nicht loggen)
  • Soviele untainted Nodes wie bao.ha.replicas (Chart: required hostname Anti-Affinity). 5 Replicas brauchen 5 Nodes; ein weiterer Node ist Spare
  • Service-DNS (http://openbao.openbao.svc:8200) bleibt — Rook/Clients nicht umziehen
  • setup-ceph-kms nicht erneut ausführen — rotiert rook-ceph-rgw-vault-token und trennt RGW vom bestehenden Transit

0. Inventar + Extra-Backup (OpenBao noch unsealed)

bao status
bao secrets list
bao list transit/keys
bao read transit/keys/<name>

Transit-Export versuchen, aber nicht als einzige Sicherung behandeln. Von Ceph angelegte Keys sind oft exportable: false (bao read transit/export/encryption-key/<name> → 400). Erhalt läuft über Migrate + Velero + _filebak. Export-JSON nicht ins Git und nach der Migration aus dem Workspace löschen.

  • Offline-Kopie von operator-init.json (Root-Token, Shamir n/t)
  • On-Demand Velero-Backup Namespace openbao (PVC data-openbao-0) unmittelbar vor Scale-0
  • RGW nicht stoppen; SSE-5xx während der OpenBao-Downtime sind akzeptiert, wenn wenig Traffic

1. Server offline

PVC data-openbao-0 darf nicht gelöscht werden.

kubectl -n openbao scale sts/openbao --replicas=0
# warten bis RBD detached (VolumeAttachment weg, Pods weg)
kubectl -n openbao get pods
kubectl get volumeattachment | grep data-openbao-0 || true

2. Offline bao operator migrate auf dem bestehenden PVC

Einmal-Pod, gleiches Image wie der Server (quay.io/openbao/openbao:<app_version>). Volume data-openbao-0 mounten. Server-Prozess nicht starten.

Chart-HA erwartet Raft unter /openbao/data (vault.db + raft/). Je nach Mount im Migrate-Pod ist die PVC-Wurzel /openbao/data oder /data (wenn ihr die PVC direkt nach /data bindet). Quell- und Zielpfad müssen verschieden sein. Zielverzeichnis vorher anlegen — sonst scheitert migrate mit „no such file or directory“.

setNodeId: true → node_id muss openbao-0 sein.

storage_source "file" {
  path = "/data"
}

storage_destination "raft" {
  path     = "/data/raftdata"
  node_id  = "openbao-0"
}

cluster_addr = "http://openbao-0.openbao-internal:8201"
mkdir -p /data/raftdata
bao operator migrate -config=/tmp/migrate.hcl

Danach File-Bäume beiseite legen, Raft auf die PVC-Wurzel (das wird später /openbao/data):

mkdir -p /data/_filebak
# core/ logical/ sys/ (und alles außer raftdata/_filebak) nach _filebak
mv /data/core /data/logical /data/sys /data/_filebak/ 2>/dev/null || true
# Raft-Inhalt nach PVC-Wurzel
mv /data/raftdata/vault.db /data/vault.db
mv /data/raftdata/raft /data/raft
rmdir /data/raftdata 2>/dev/null || true
chown -R openbao:openbao /data/vault.db /data/raft

Erwartung: vault.db + raft/ auf der PVC-Wurzel; File-Backend unter _filebak. Lock bei Bedarf: bao operator migrate -reset. Migrate-Pod danach löschen.

3. Workspace auf HA, Helm anwenden

bao:
  mode: ha
  readiness:
    mode: strict
  ha:
    replicas: 5          # oder 3; muss zu den Nodes passen
  backup:
    enabled: true
    # standalone_strategy entfernen
polycrate run openbao install -w /absolute/workspace

operator init skippt (bereits initialized). Fünf (bzw. drei) PVCs: data-openbao-0 (migrierte Raft-Daten) + neue leere data-openbao-1… die per retry_join joinen.

4. Unseal + Join

install bootstrap-unsealed die Replicas, wenn bao.unseal.unseal_keys oder das Init-Artefakt gesetzt sind. Sonst:

polycrate run openbao unseal -w /absolute/workspace

Check:

bao status                    # storage_type=raft, ha_enabled=true, same cluster_id
bao operator raft list-peers  # N Voters, ein Leader

5. Consumer + HA-Backup

  • Transit-Keys listen; bei derived Keys braucht Encrypt/Decrypt ein context (Ceph SSE). RGW-Token-Smoke: transit/keys?list=true und transit/datakey/plaintext/<key> mit dem Token aus /etc/vault/rgw/sses3/vault.token — Token und Plaintext nicht loggen
  • Bestehendes SSE-Objekt lesen/schreiben, falls Buckets existieren
  • polycrate run openbao setup-backup — schreibt Policy snapshot und legt ein eigenes Token in Secret openbao-snapshot-token (nicht das Root-Token)
  • backup.enabled: true + install erzeugt Snapshot-Store + CronJob
  • Erstes Snapshot-Job anstoßen und Dateigröße prüfen (test -s, nicht 0 Byte)
  • Velero des Namespace openbao danach (Snapshot-Store und data-PVCs der Server-StatefulSets)

6. Aufräumen

_filebak auf PVC-0 erst löschen, wenn ein Raft-Snapshot >0 Byte und ein Velero-Backup des Stores ok sind.

Troubleshooting

Symptom Ursache Fix
install failt: STS podManagementPolicy immutable (OrderedReady → Parallel) Helm kann das Feld nicht ändern kubectl -n openbao delete sts openbao --cascade=orphan — PVCs behalten — install erneut
Alle openbao-N Pending (7s, Warning am Namespace) Anti-Affinity (zu wenige Nodes) oder PVC noch attached oder STS gerade neu kubectl describe pod openbao-N: FailedScheduling / volume not attached. Nodes ohne Taint zählen. Nach STS-Recreate kurz warten; nicht PVCs löschen
bao operator migrate: dest dir missing raftdata existiert nicht mkdir -p auf dem Zielpfad, dann erneut
kubectl apply -f [None] beim Snapshot-Store Ansible argv + stdin-Dash wird zu null (≤ 0.3.2 Workaround fragil) ≥ 0.3.3 rendert nach /tmp/openbao-snapshot-store.yml und kubectl apply -f die Datei
setup-backup failt beim Policy-Write (stdin / no_log) 0.3.0 schreibt Policy per bao policy write - + stdin ≥ 0.3.1: bao write sys/policies/acl/snapshot policy=... ohne stdin; Admin-Token wie bei setup
Snapshot-Datei 0 Byte, Job trotzdem Complete kubectl exec … cat > file < snap überträgt stdin nicht zuverlässig ≥ 0.3.3: kubectl cp. Manuell: bao operator raft snapshot save im Leader, dann kubectl cp auf den Store-Pod
/v1/sys/health von RGW → HTTP 429 ClusterIP traf einen Raft-Standby Normal. ?standbyok=true für Health. Writes proxied der Standby zum Leader
Transit encrypt 400 missing context Derived Key (Ceph) context=<base64> mitsenden; Key ist trotzdem gültig
Transit export 400 not exportable Ceph-created keys Kein Offline-Key-Material. Nicht neu initialisieren. Migrate + Velero + _filebak
Follower joined nicht retry_join / sealed / falscher node_id Im Follower: bao operator raft join http://openbao-0.openbao-internal:8200, dann unsealen. Seed muss openbao-0 sein
Helm OnDelete, Config ändert sich nicht Chart updateStrategyType: OnDelete Nach Values-Änderung Pods löschen oder STS orphan-recreate
RGW kann nach Cutover nicht encrypten Token rotiert oder anderer cluster_id setup-ceph-kms nicht nach Pfad A. Secret rook-ceph-rgw-vault-token CreationTimestamp prüfen. bao status cluster_id unverändert

Rollback (solange _filebak / Velero existiert)

  1. STS auf 0
  2. Raft auf PVC-0 verwerfen, _filebak nach PVC-Wurzel zurück
  3. bao.mode: standalone, ha.replicas entfernen, standalone_strategy wieder setzen
  4. polycrate run openbao install + unseal mit alten Keys
  5. Alternative: Velero-Restore Namespace openbao / PVC data-openbao-0

Nach Löschen von _filebak: Rollback nur aus Velero oder Raft-Snapshot.

Pfad B (nur Disaster)

Neuer Cluster = neue Shamir-Keys + neuer Root-Token. Transit verloren ohne Export. Dann setup-ceph-kms (rotiert das RGW-Token) + RGW-Roll + Neuverschlüsselung oder Objekt-Verlust. Nicht verwenden, wenn SSE-Bestand existiert.


Audit-Logs aktivieren

Wenn bao.auditstorage.enabled: true gesetzt ist, wird ein PVC unter /openbao/audit gemountet. OpenBao schreibt dort aber erst dann Logs, wenn das Audit Device explizit aktiviert wird:

kubectl exec -n openbao openbao-0 -- \
  bao audit enable file file_path=/openbao/audit/audit.log

Metriken (VMServiceScrape)

Wenn bao.vmservicescrape.enabled: true gesetzt ist, wird ein VMServiceScrape-Objekt im Namespace victoria-metrics-stack (konfigurierbar) angelegt. Es scrapt den OpenBao-Server auf /v1/sys/metrics?format=prometheus.

OpenBao gibt Metriken nur aus, wenn das Telemetry-Stanza in der Server-Konfiguration aktiviert ist. Für unauthentifizierten Zugriff muss unauthenticated_metrics_access = true im listener "tcp" Block gesetzt sein — dies erfordert eine Anpassung im values.yml.j2-Template.