secure k8 s

Kubernetes HA setup with Helm

Summary: Step-by-step procedure for bringing up a 3-node OpenBao HA cluster on Kubernetes using the official Helm chart with integrated Raft storage. Covers the minimum install, TLS, auto-unseal, and what to add for production.

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/configuration.md, raw/docs/platform/k8s/helm/run.md, raw/docs/platform/k8s/helm/terraform.md, raw/docs/platform/k8s/helm/examples/snapshot-cronjob.md.

Last updated: 2026-05-19


Prerequisites

  • Helm 3.6+. The chart is not compatible with Helm 2 (source: raw/docs/platform/k8s/helm/examples/ha-with-raft.md).
  • A Kubernetes cluster with persistent-volume support (Raft needs durable storage per pod).

If you do not yet have a Kubernetes cluster, see k8s-ha-from-scratch — that page covers the full path from three bare Ubuntu 24.04 servers (kubeadm + Cilium + cert-manager + this Helm chart) and lands at the same operational state as this page. This page assumes the cluster, Helm and a default StorageClass already exist.

1 — Add the OpenBao Helm chart repository

The chart is published at https://openbao.github.io/openbao-helm (source: raw/docs/platform/k8s/helm/terraform.md). Register it locally and confirm the chart is reachable:

helm repo add openbao https://openbao.github.io/openbao-helm
helm repo update
helm search repo openbao/openbao

helm search repo openbao/openbao should list the chart and its current version (source: raw/docs/platform/k8s/helm/run.md). If you need to pin a specific chart version later, pass --version <x.y.z> to helm install/helm upgrade.

A few alternatives if helm repo add doesn’t fit your workflow:

  • Terraform — use the helm_release resource with repository = "https://openbao.github.io/openbao-helm" and chart = "openbao". See Terraform below and raw/docs/platform/k8s/helm/terraform.md.
  • Local checkout — clone github.com/openbao/openbao-helm and install from the path: helm install openbao ./charts/openbao.

2 — Install the chart in HA + Raft mode

helm install openbao openbao/openbao \
  --set='server.ha.enabled=true' \
  --set='server.ha.raft.enabled=true'

(source: raw/docs/platform/k8s/helm/examples/ha-with-raft.md)

By default this brings up three pods — openbao-0, openbao-1, openbao-2 — all in sealed state. The chart provisions a headless internal service (openbao-internal) for node-to-node traffic plus a regular service for client access.

3 — Initialize and unseal the first pod

kubectl exec -ti openbao-0 -- bao operator init
kubectl exec -ti openbao-0 -- bao operator unseal

Save the unseal-key shards and the initial root token. With a Shamir seal you need a threshold of shards on every node every time it restarts — partial unseal does not propagate across the cluster. This is why production deployments switch to auto-unseal (see below).

4 — Join and unseal the followers

Each follower joins the cluster via the headless service, then is unsealed independently:

kubectl exec -ti openbao-1 -- bao operator raft join http://openbao-0.openbao-internal:8200
kubectl exec -ti openbao-1 -- bao operator unseal

kubectl exec -ti openbao-2 -- bao operator raft join http://openbao-0.openbao-internal:8200
kubectl exec -ti openbao-2 -- bao operator unseal

(source: raw/docs/platform/k8s/helm/examples/ha-with-raft.md)

5 — Verify

kubectl exec -ti openbao-0 -- bao login
kubectl exec -ti openbao-0 -- bao operator raft list-peers

Expected: openbao-0 as leader, the others as follower, all Voter: true. Example output (source: raw/docs/platform/k8s/helm/examples/ha-with-raft.md):

Node                                    Address                            State       Voter
----                                    -------                            -----       -----
a1799962-...                            openbao-0.openbao-internal:8201    leader      true
e6876c97-...                            openbao-1.openbao-internal:8201    follower    true
4b5d7383-...                            openbao-2.openbao-internal:8201    follower    true

Production hardening

The minimum install above is functional but not production-ready. Add:

TLS

Without TLS configuration, Raft join logs x509: certificate is valid for ${SERVICE}, … not openbao-${N}.${SERVICE} because the per-pod DNS name doesn’t match the cert’s CN (source: raw/docs/platform/k8s/helm/examples/ha-tls.md).

Two supported fixes:

  • Solution 1 — auto-join with leader_tls_servername. One retry_join per pod, each pinning leader_tls_servername to the cert CN:

    storage "raft" {
      path = "/openbao/data"
    
      retry_join {
        leader_api_addr = "https://openbao-0.${SERVICE}:8200"
        leader_tls_servername = "${CN}"
        leader_client_cert_file = "/openbao/tls/openbao.crt"
        leader_client_key_file  = "/openbao/tls/openbao.key"
        leader_ca_cert_file     = "/openbao/tls/openbao.ca"
      }
      # ...one retry_join per node
    }
  • Solution 2 — front with a load balancer. A single retry_join against the LB’s stable name:

    retry_join {
      leader_api_addr         = "https://openbao-active:8200"
      leader_client_cert_file = "/openbao/tls/openbao.crt"
      leader_client_key_file  = "/openbao/tls/openbao.key"
      leader_ca_cert_file     = "/openbao/tls/openbao.ca"
    }

Cert provisioning examples (cert-manager and manual) live in raw/docs/platform/k8s/helm/examples/standalone-tls.md and raw/docs/platform/k8s/helm/examples/injector-tls-cert-manager.md.

Auto-unseal

Manual Shamir unseal of every pod after every restart is operationally painful in Kubernetes. Switch to auto-unseal via a seal stanza:

  • Cloud KMS — seal "awskms" { … }, seal "gcpckms" { … }, seal "azurekeyvault" { … }, seal "ocikms" { … }, seal "alicloudkms" { … }.
  • Another OpenBao cluster — seal "transit" { … }.
  • PKCS#11 HSM — seal "pkcs11" { … }.

Full backend list and config at raw/docs/configuration/seal/. Conceptual overview in seal-unseal. Auto-unseal also defines a separate recovery key (Shamir-split) used for operator generate-root-style operations.

Snapshots

Schedule periodic Raft snapshots with the CronJob in raw/docs/platform/k8s/helm/examples/snapshot-cronjob.md. Snapshots are the supported backup primitive for the integrated storage backend — see storage and upgrading for restore considerations.

Audit

The cluster has no audit device until you enable one. Always enable at least two (different sinks) — if the sole audit device blocks, OpenBao stops serving requests.

Values customization

The Helm values surface is documented at raw/docs/platform/k8s/helm/configuration.md and raw/docs/platform/k8s/helm/run.md. Common knobs:

  • server.ha.replicas — number of pods (3 or 5 typical for Raft).
  • server.dataStorage / server.auditStorage — PVC sizes and storage classes.
  • server.resources — requests/limits.
  • server.affinity — pod anti-affinity to spread across nodes/zones (essential for real HA).
  • server.extraEnvironmentVars and server.extraVolumes — for KMS credentials, TLS certs.
  • injector.enabled — toggle the Agent Injector mutating webhook.

OpenShift

OpenShift has its own SCC/permissions tweaks — see raw/docs/platform/k8s/helm/openshift.md.

Terraform

If you’d rather provision via Terraform than direct Helm, see raw/docs/platform/k8s/helm/terraform.md.