secure k8 s

Service-Registration wieder aktivieren

Summary: Schritt-für-Schritt-Anleitung, um eine zuvor auskommentierte service_registration "kubernetes" {}-Stanza im Helm-Chart sauber wieder einzuschalten. Reihenfolge ist wichtig — erst die Voraussetzungen (NetworkPolicy-Egress, RBAC, Downward API) verifizieren, dann die Stanza setzen, dann das Rollout staffeln und Active-/Standby-Services prüfen.

Sources: raw/docs/configuration/service-registration/kubernetes.md, raw/docs/configuration/service-registration/index.md, raw/docs/platform/k8s/helm/terraform.md, raw/docs/platform/k8s/helm/run.md, raw/docs/concepts/ha.md.

Last updated: 2026-05-20


0 — Wo wir herkommen

Die Stanza service_registration "kubernetes" {} war im Chart aktiv, hat aber beim Start einen TLS-Handshake-Hang ausgelöst. Als Workaround wurde sie auskommentiert. Konsequenz (siehe kubernetes-service-registration):

  • Pods werden nicht mehr mit openbao-active, openbao-sealed, openbao-initialized, openbao-version gelabelt.
  • Die Selector-basierten Services *-active und *-standby haben leere Endpoints.
  • Routing läuft heute über den Headless-Service bzw. den ClusterIP-Service round-robin — Writes auf Followern werden über das Request-Forwarding bzw. einen 307-Redirect zur api_addr des Leaders aufgelöst (source: raw/docs/concepts/ha.md). Das funktioniert, ist aber ein Hop teurer und ohne Sealed-Schutz.
  • Das Chart-Feature ui.activeOpenbaoPodOnly und chirurgische Upgrades anhand der Labels sind nicht nutzbar.

Ziel der Reaktivierung: Pod-Labels wieder schreiben lassen, ohne dass der Start hängt.

1 — Warum die Reihenfolge so ist

Sobald die Stanza aktiv ist, ruft OpenBao beim Start die Kubernetes-API auf (PATCH /api/v1/namespaces/<ns>/pods/<pod>). Hängt dieser Call, hängt der Pod-Start. Genau das war das ursprüngliche Symptom. Drei Voraussetzungen müssen deshalb vor dem Aktivieren der Stanza nachweisbar erfüllt sein:

  1. Netzwerk: Egress vom OpenBao-Pod zur Kubernetes-API (kubernetes.default.svc:443) ist erlaubt.
  2. Identität: Der ServiceAccount der OpenBao-Pods darf get/update/patch auf das eigene Pod-Objekt (source: raw/docs/configuration/service-registration/kubernetes.md).
  3. Selbst-Identifikation: Die Pods kennen ihren eigenen Namespace und Pod-Namen über BAO_K8S_NAMESPACE und BAO_K8S_POD_NAME aus der Downward API (source: raw/docs/configuration/service-registration/kubernetes.md).

Erst danach wird die Stanza eingeschaltet und ein gestaffeltes Rollout durchgezogen.

2 — Schritt 1: NetworkPolicy-Egress zur K8s-API freigeben

Verdachtsdiagnose zuerst aus einem laufenden Pod bestätigen, bevor irgendetwas geändert wird:

kubectl exec -n openbao-tm openbao-tm-0 -- \
  sh -c 'wget -O- -T 5 --no-check-certificate https://kubernetes.default.svc/version 2>&1 | head -20'

Erwartet bei funktionierendem Egress: ein JSON mit gitVersion. Hängt der Call oder kommt connection timed out, blockiert eine NetworkPolicy (oder CNI-Layer-Policy wie Cilium CiliumNetworkPolicy) den Egress.

Die NetworkPolicy braucht einen Egress-Regel-Eintrag zur K8s-API. Bei CiliumNetworkPolicy typischerweise über das kube-apiserver-Entity:

egress:
  - toEntities:
      - kube-apiserver

Bei vanilla NetworkPolicy muss man den API-Server-Endpoint via ipBlock adressieren (die K8s-API ist kein normales Pod-Target — podSelector/namespaceSelector matcht sie nicht). Die konkrete CIDR ergibt sich aus kubectl get endpoints kubernetes -n default:

egress:
  - to:
      - ipBlock:
          cidr: <api-server-ip>/32
    ports:
      - protocol: TCP
        port: 443

Nach Apply den Test aus dem Pod wiederholen — die Antwort muss innerhalb von 1–2 Sekunden kommen. Ist sie da, ist die Hauptursache des TLS-Hangs beseitigt.

Hängt der Call obwohl die NetPol passt, ist die nächstwahrscheinliche Ursache eine falsche CA für die K8s-API im Pod (/var/run/secrets/kubernetes.io/serviceaccount/ca.crt muss zum API-Server passen — bei selbst gebauten Images leicht zu übersehen).

3 — Schritt 2: RBAC prüfen

ServiceAccount des StatefulSets feststellen:

kubectl get statefulset openbao-tm -n openbao-tm -o jsonpath='{.spec.template.spec.serviceAccountName}{"\n"}'

Berechtigung explizit prüfen — das ist der zuverlässigste Test, weil kubectl auth can-i direkt das RBAC-Subsystem fragt:

SA=$(kubectl get sts openbao-tm -n openbao-tm -o jsonpath='{.spec.template.spec.serviceAccountName}')
kubectl auth can-i patch pods --as=system:serviceaccount:openbao-tm:$SA -n openbao-tm
kubectl auth can-i get   pods --as=system:serviceaccount:openbao-tm:$SA -n openbao-tm
kubectl auth can-i update pods --as=system:serviceaccount:openbao-tm:$SA -n openbao-tm

Alle drei müssen yes antworten. Falls nicht, fehlt die Role oder die RoleBinding. Sollwert (source: raw/docs/configuration/service-registration/kubernetes.md):

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  namespace: openbao-tm
  name: openbao-tm
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "update", "patch"]

Das offizielle Helm-Chart legt diese Role automatisch an, wenn server.serviceAccount.create=true und HA aktiv ist (source: raw/docs/platform/k8s/helm/terraform.md). Wer den ServiceAccount manuell pflegt, muss die Role manuell mitliefern.

4 — Schritt 3: Downward API verifizieren

Aus einem laufenden Pod:

kubectl exec -n openbao-tm openbao-tm-0 -- sh -c 'echo "ns=$BAO_K8S_NAMESPACE pod=$BAO_K8S_POD_NAME"'

Erwartet: ns=openbao-tm pod=openbao-tm-0. Sind die Variablen leer, fehlt der entsprechende env-Block im StatefulSet-Template. Beim offiziellen Chart ist das im HA-Mode Default; wenn die values-tm.yaml aber server.standalone.config oder ein eigenes Pod-Template setzt, kann das Mapping fehlen. Sollwert:

env:
  - name: BAO_K8S_NAMESPACE
    valueFrom:
      fieldRef:
        fieldPath: metadata.namespace
  - name: BAO_K8S_POD_NAME
    valueFrom:
      fieldRef:
        fieldPath: metadata.name

Alternativ — falls die Downward API in diesem Setup nicht nachrüstbar ist — kann man die Werte explizit in die Stanza schreiben. Das geht im StatefulSet aber nur über einen pro-Pod-Generator-Workaround und ist nicht empfehlenswert.

5 — Schritt 4: Stanza in der Helm-Values reaktivieren

Erst jetzt — wenn Egress, RBAC und Downward API alle bestätigt sind — wird die Stanza wieder eingeschaltet. In values-tm.yaml:

storage "raft" {
  path = "/openbao/data"
  # retry_join … explizit, wie bisher
}

service_registration "kubernetes" {}

Die leere {}-Form genügt: Namespace und Pod-Name kommen aus der Downward API (source: raw/docs/configuration/service-registration/kubernetes.md).

helm upgrade ausführen — noch nicht die Pods neu starten. Erst die rendered ConfigMap kontrollieren:

helm upgrade openbao-tm openbao/openbao -f values-tm.yaml -n openbao-tm
kubectl get configmap -n openbao-tm openbao-tm-config -o jsonpath='{.data.extraconfig-from-values\.hcl}' | grep -A1 service_registration

Erwartet: service_registration "kubernetes" {} ist im gerenderten Output enthalten.

6 — Schritt 5: Gestaffeltes Rollout

Update-Strategy des StatefulSets auf OnDelete lassen (oder kurzfristig setzen) und die Pods einzeln neu starten — Standbys zuerst, aktiver Pod zuletzt. Welcher Pod aktuell Leader ist, weiß noch nicht das Label (das gibt es ja gerade noch nicht), sondern die API:

for i in 0 1 2; do
  echo -n "openbao-tm-$i: "
  kubectl exec -n openbao-tm openbao-tm-$i -- bao status 2>/dev/null | grep -E 'HA Mode'
done

Zuerst die beiden Standbys neu erzeugen:

kubectl delete pod -n openbao-tm openbao-tm-1
# warten bis Ready und unsealed:
kubectl wait pod openbao-tm-1 -n openbao-tm --for=condition=Ready --timeout=120s
kubectl exec -n openbao-tm openbao-tm-1 -- bao status

kubectl delete pod -n openbao-tm openbao-tm-2
kubectl wait pod openbao-tm-2 -n openbao-tm --for=condition=Ready --timeout=120s

Wenn beide Standbys mit gesetztem openbao-active=false-Label hochkommen, ist klar: der Patch-Call funktioniert. Erst dann den Leader zerstören:

kubectl delete pod -n openbao-tm openbao-tm-0

Ein Standby übernimmt; der neu hochkommende openbao-tm-0 wird Standby.

Verifikation:

kubectl get pods -n openbao-tm -L openbao-active,openbao-sealed,openbao-version

Erwartet: genau ein Pod hat openbao-active=true, alle drei haben openbao-sealed=false und eine gesetzte Versions-Spalte.

7 — Schritt 6: Active- und Standby-Services testen

Die Selector-Services bekommen jetzt automatisch Endpoints:

kubectl get endpoints -n openbao-tm openbao-tm-active openbao-tm-standby

Erwartet:

  • openbao-tm-active: genau eine Pod-IP — die des aktuellen Leaders.
  • openbao-tm-standby: zwei Pod-IPs — die der beiden Followers.

Funktionscheck aus einem Test-Pod:

kubectl run curl --rm -it --image=curlimages/curl -n openbao-tm -- \
  curl -s -k https://openbao-tm-active:8200/v1/sys/health | jq .

Im Response muss "standby": false stehen — wir landen tatsächlich beim Leader.

Failover-Test (optional, aber empfohlen, wenn das Setup heiß ist):

LEADER=$(kubectl get pods -n openbao-tm -l openbao-active=true -o name)
kubectl delete -n openbao-tm $LEADER
# warten — neuer Leader übernimmt
sleep 15
kubectl get pods -n openbao-tm -L openbao-active
kubectl get endpoints -n openbao-tm openbao-tm-active

Erwartet: ein anderer Pod ist jetzt openbao-active=true, und das einzige Endpoint im *-active-Service zeigt auf den neuen Leader.

8 — Rollback-Plan

Falls beim Re-Aktivieren wieder ein Hang auftritt:

  1. Stanza in values-tm.yaml wieder auskommentieren.
  2. helm upgrade … mit alten Values.
  3. kubectl rollout restart statefulset/openbao-tm -n openbao-tm.
  4. Diagnose aufnehmen — typischerweise war einer der drei Voraussetzungs-Checks (Netzwerk, RBAC, Downward API) doch nicht erfüllt, hat aber im statischen Test gepasst, weil z. B. die NetworkPolicy nur auf bestimmte Pods matched.

Wichtig: Beim Rollback bleibt das Pod-Labelset stale, bis die Pods rotieren. Die Selector-Services haben in der Zwischenzeit weiter Endpoints, die aber nicht mehr aktuell sind. Deshalb beim Rollback immer auch die Pods neu starten, damit die alten Labels verschwinden.

9 — Was bewusst nicht geändert wird

  • retry_join-Block in storage "raft" bleibt mit expliziten Peer-Adressen stehen. retry_join ist ein DNS-/HTTP-Discovery-Mechanismus von Raft selbst und hat nichts mit Pod-Labels oder der Kubernetes-API zu tun (source: raw/docs/configuration/storage/raft.md, raft § “retry_join”). Service Registration ersetzt es nicht. Der Block ist mit drei explizit benannten Peer-DNS-Namen so robust, wie es geht — ihn anzufassen, nur weil Service Registration zurückkommt, würde ohne Gewinn Komplexität entfernen.
  • TLS-Konfiguration (listener "tcp" { tls_… }) und api_addr bleiben unverändert. Service Registration hängt nicht davon ab.

10 — Antipatterns

  • Stanza vor NetPol-Fix reaktivieren: führt direkt zurück in den ursprünglichen Hang.
  • Alle drei Pods gleichzeitig neu starten: kein Quorum-Schutz, Leader-Übergabe potentiell mehrfach, Endpoints flackern. Immer staffeln.
  • publishNotReadyAddresses: true am Active-Service stehen lassen: hebelt den Sealed-Schutz aus, den Service Registration gerade liefern soll. Default false lassen (siehe kubernetes-service-registration § 10).
  • Auto-Unseal vergessen: Nach Pod-Restart bleiben die Pods sealed, openbao-active kann nicht auf true gehen, der Active-Service hat keine Endpoints. In K8s ist Auto-Unseal für service registration de facto Pflicht.
  • RBAC nur am ClusterRole-Level prüfen: Die benötigte Role ist namespaced auf die eigene Namespace — eine globale ClusterRole erfüllt das zwar auch, aber die Mehrheit der Setups arbeitet mit Role + RoleBinding. kubectl auth can-i ist die zuverlässige Probe.
  • kubernetes-service-registration — was die Stanza tut, welche Labels gesetzt werden, RBAC im Detail
  • k8s-ha-setup — das Gesamt-Setup, in dem diese Reaktivierung stattfindet
  • high-availability — Request-Forwarding und der 307-Redirect-Fallback, der heute das Routing trägt
  • raftretry_join und warum es vom Service-Registration-Pfad getrennt bleibt
  • seal-unseal — warum Auto-Unseal hier praktisch verpflichtend ist
  • configuration — die service_registration-Stanza im Kontext der gesamten Server-Konfiguration