Kubernetes operator for managing Vault resources (database roles, policies, auth roles)
  • Go 97.3%
  • Makefile 1.5%
  • Shell 0.8%
  • Dockerfile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pipeline-release-prepare faf51b0b2f
All checks were successful
pipeline/ci CI green v0.12.29
release: v0.12.29 (auto-release)
2026-07-24 09:38:03 +00:00
.devcontainer Initial scaffold: kubebuilder v4 + 4 Vault CRDs 2026-04-20 19:58:36 +02:00
api fix(static-role): make remote identity immutable 2026-07-24 11:03:30 +02:00
cmd chore(quality): clear static analysis backlog 2026-07-24 07:09:47 +02:00
config fix(static-role): make remote identity immutable 2026-07-24 11:03:30 +02:00
dist fix(static-role): make remote identity immutable 2026-07-24 11:03:30 +02:00
hack refactor: rename openbao-resource-operator -> openbao-operator 2026-06-10 18:07:59 +02:00
internal fix(client): normalize OpenBao API error kinds 2026-07-24 11:03:35 +02:00
test fix(security): upgrade reachable vulnerable dependencies 2026-07-24 07:09:34 +02:00
.custom-gcl.yml Initial scaffold: kubebuilder v4 + 4 Vault CRDs 2026-04-20 19:58:36 +02:00
.dockerignore Initial scaffold: kubebuilder v4 + 4 Vault CRDs 2026-04-20 19:58:36 +02:00
.gitattributes fix(cicd): auto-resolve CHANGELOG land conflicts via .gitattributes merge=union 2026-07-05 12:14:40 +00:00
.gitignore Initial scaffold: kubebuilder v4 + 4 Vault CRDs 2026-04-20 19:58:36 +02:00
.golangci.yml chore(lint): clean up lint debt and tighten config 2026-04-30 11:59:53 +02:00
AGENTS.md refactor: rename openbao-resource-operator -> openbao-operator 2026-06-10 18:07:59 +02:00
CHANGELOG.md release: v0.12.29 (auto-release) 2026-07-24 09:38:03 +00:00
ci-test.Dockerfile fix(ci): pin golang:1.25 builder base by digest 2026-07-05 13:19:44 +02:00
Dockerfile fix(ci): pin golang:1.25 builder base by digest 2026-07-05 13:19:44 +02:00
go.mod fix(security): upgrade reachable vulnerable dependencies 2026-07-24 07:09:34 +02:00
go.sum fix(security): upgrade reachable vulnerable dependencies 2026-07-24 07:09:34 +02:00
Makefile chore(scaffolding): adopt operator.mk (lib v0.4.12) 2026-07-17 12:32:33 +02:00
operator.mk chore(scaffolding): adopt operator.mk (lib v0.4.12) 2026-07-17 12:32:33 +02:00
PROJECT refactor: rename openbao-resource-operator -> openbao-operator 2026-06-10 18:07:59 +02:00
README.md chore(release): v0.12.3 — README rewrite, drop scaffolded Actions CI, bump dist 2026-06-11 14:01:22 +02:00
VERSION release: v0.12.29 (auto-release) 2026-07-24 09:38:03 +00:00

openbao-operator

A Kubernetes operator that manages OpenBao (a HashiCorp Vault fork) configuration declaratively from custom resources. Instead of Terraform runs or imperative bao write calls, you express OpenBao state — ACL policies, auth roles, PKI hierarchies, database engines, transit keys, the GCP secrets engine — as Kubernetes objects, and the operator continuously reconciles the live OpenBao server toward that spec. It is one of the seanfarm operator fleet (alongside forgejo-operator, kratos-identity-operator, and minio-resource-operator) and shares their reconcile harness.

The operator authenticates to OpenBao via Kubernetes auth using its own ServiceAccount token; no root token is in flight at steady state. It comes up even when OpenBao is unreachable, retrying login in the background, so a cold-cluster bootstrap never deadlocks on ordering.

Custom resources

Two API groups. The first is this operator's native config surface; the second is a separate, vendored secret-sync subsystem (see Secret sync).

openbao.sean.farm/v1alpha1 — OpenBao configuration

Every kind is namespaced and carries spec.openBaoRef (which server), spec.deletionPolicy (Delete default, Retain), status.observedGeneration, and a unified Ready condition with a shared reason taxonomy.

Kind Purpose
OpenBaoPolicy ACL policy document (HCL), written to sys/policies/acl/<name>.
OpenBaoKubernetesAuthRole Role on the Kubernetes auth backend binding ServiceAccounts to policies.
OpenBaoPKIBackend A PKI secrets-engine mount and its tuning.
OpenBaoPKIRootCert A self-signed root CA generated inside a PKI mount.
OpenBaoIntermediateCA An intermediate CA: generate CSR, sign against a parent, set the signed chain.
OpenBaoPKIIssuer A named issuer within a PKI mount; integrates with cert-manager to issue certificates from OpenBao PKI.
OpenBaoPKIRole A PKI role constraining what certificates may be issued.
OpenBaoPKICertificate A leaf certificate issued from a PKI role.
OpenBaoTransitBackend A transit secrets-engine mount and its encryption keys (with rotation).
OpenBaoDatabaseConnection A database secrets-engine connection (credentials read from a referenced Secret).
OpenBaoDatabaseStaticRole A static DB role: OpenBao rotates the password of a pre-existing DB user.
OpenBaoGCPSecretsBackend A GCP secrets-engine mount configured with service-account credentials from a Secret.
OpenBaoGCPRoleset A GCP roleset (bindings + token/key generation) on a GCP backend.

OpenBaoKubernetesAuthRole.spec.policyRefs[] and the PKI/intermediate-CA parent references resolve to sibling CRs: a missing or not-yet-Ready reference holds the dependent at Ready=False/DependencyNotReady so the operator never writes OpenBao state that depends on something it has not yet created.

secrets.openbao.sean.farm/v1beta1 — secret sync

Kind Purpose
OpenBaoConnection How to reach an OpenBao server for secret sync.
OpenBaoAuth How to authenticate (auth method + role) for secret sync.
OpenBaoDynamicSecret Render a dynamic/leased secret from OpenBao into a Kubernetes Secret, keeping it renewed.

Architecture

Shared reconcile harness

The 13 openbao.sean.farm controllers run on the shared reconcile harness from libseanfarm-operator (.../reconcile), the same generic lifecycle all four seanfarm operators import. The harness was extracted from four near-identical, drifting hand-written reconcile loops into one generic entrypoint, reconcile.Run[T, C]. It owns the entire reconcile lifecycle: finalizer mechanics, external-client resolution, mapping a handler's convergence result to the Ready condition, a reason→requeue taxonomy, deletion-policy semantics, conflict-safe status writes, sanitized Events, and uniform concurrency + exponential backoff.

Each controller supplies only a thin policy shim, all of which lives in internal/controller/harness.go:

  • A Handler (Converge + Delete). Converge drives OpenBao toward the spec and reports Converged or Progressing; it must return Progressing — never Converged — until it has verified the remote state. Delete removes the OpenBao-side object.
  • A Taxonomy table (openbaoTaxonomy) classifying each failure reason as Terminal (InvalidSpec, ConfigurationError — no requeue until the spec changes), FixedRequeue (DependencyNotReady, TransientError — self-heal on the transient cadence), or Backoff (anything else, via controller-runtime).

Ready is structural. The harness — never the handler — sets Ready=true, and only after Converge reports the external state actually verified. "Ready means verified" is enforced by the library, not re-implemented per controller.

Openbao-specific wiring

  • resolveClient turns an OpenBaoRef into an OpenBao client via the caching client factory, mapping a not-ready server to DependencyNotReady and other failures to TransientError so both self-heal on the transient cadence rather than terminal-parking.
  • adapted[T] bridges the openbao Handler onto the lib's: it supplies the package finalizer, treats OpenBaoNotFound on read/delete as success (idempotent finalize), and classifies other errors through classifyOpenBaoError.
  • Deletion uses HoldUntilSuccess. A deletionPolicy=Retain resource drops its finalizer without touching OpenBao; otherwise every failed delete holds the finalizer and retries on the transient cadence — it never terminal-parks and never backs off unboundedly — while status still shows the accurate classified reason.
  • The operator's DeletionPolicy type is a type-alias of the library's, so every CRD's GetDeletionPolicy satisfies the harness's deletion-policy interface for free.
  • Several controllers watch referenced Secrets (DB connection password, GCP credentials) and re-enqueue the owning CR when the Secret lands or changes, so a credential that arrives after the CR converges without manual nudging.
  • cert-manager integration: OpenBaoPKIIssuer issues certificates from OpenBao PKI through cert-manager.

Reliability: auth self-heal

A permission-denied read (OpenBao 401/403) — caused by the operator token being rotated/revoked server-side, or a roll-window race — self-heals on the transient requeue cadence instead of terminal-parking as a ConfigurationError. The client factory evicts the cached client and re-logs in on the next pass; the reason stays visible on status, but the requeue is forced transient. (This auth case is matched before the generic config-error case, which also matches a bare 403.)

Security

OpenBao is the cluster's credential broker, so the operator treats every status surface as hostile. Condition reasons/messages and Kubernetes Events land in world-readable places (status, kubectl describe, the API audit log), so:

  • Every condition/Event message is bounded (the library caps message length, 512 bytes) and emitted through the library's SanitizingRecorder, which every recorder in cmd/main.go is wrapped in.
  • Credential paths emit generic messages. The rule is name the Secret, never quote it: the GCP backend and database-connection handlers reference a Secret by name/key and never surface credential bytes; OpenBao client errors are reduced to a short summary rather than interpolating the raw error, whose .Error() can echo a request URL or a server response body containing a credentialed DSN or DB hostname. The full error goes to the controller log, never to status or Events.

The result: no plaintext DB passwords, GCP service-account key material, tokens, or credentialed connection strings ever reach status, Events, or the unbounded default Event stream.

Build & distribution

  • Container images are built in-cluster by Shipwright (buildkit strategy) and cosign-signed — never on a laptop. Image references are pinned by tag and verified by signature at admission.
  • Forgejo (code.sean.farm) is the canonical day-to-day remote. A ForgejoPushMirror backs the repo up to codeberg.org/someara/openbao-operator roughly every ten minutes; make rebuild re-seeds Forgejo from that codeberg backup on a fresh cluster. Go module fetches (this module and libseanfarm-operator) read codeberg through the default GOPROXY.
  • CI runs in-cluster via Argo Workflows / Tekton, alongside the Shipwright builds — not via repository-hosted Actions.
  • Release tags are immutable; a re-pushed identical tree produces an identical digest, so an existing cosign signature still verifies it.

Development

Red/green TDD: write the failing test, make it pass, refactor. Tests live alongside each reconciler under internal/controller/ (envtest-backed), the secret-sync subsystem under internal/controller/secrets/, and the bootstrap goroutine under cmd/.

Standard targets (make help for the full list):

Target What it does
make test Generate, fmt, vet, then run the unit + envtest suites with coverage.
make lint / make lint-fix Run golangci-lint.
make manifests / make generate Regenerate CRDs/RBAC and DeepCopy code from +kubebuilder markers.
make build Build the manager binary.
make build-installer Generate the consolidated dist/install.yaml.
make verify-installer Fail if dist/install.yaml has drifted from config/.
make test-e2e Stand up a kind cluster + in-cluster OpenBao dev server and run the e2e suite.

How a change ships: edit → make test (green) → push to Forgejo. A Shipwright Build/BuildRun produces the cosign-signed image; the deployed tag is then advanced via the Flux/Crossplane-composition pins that reference it. No image is built or pushed from a developer machine for cluster use.

Secret sync

The code under internal/secretsync/, internal/controller/secrets/, and api/secrets/v1beta1/ (the secrets.openbao.sean.farm group) is a separate, clean-room secret-sync subsystem, vendored from openbao/openbao-secrets-operator (itself a fork of HashiCorp's vault-secrets-operator) and licensed MPL-2.0 — see internal/secretsync/LICENSE and internal/secretsync/NOTICE.md. It uses a lease-horizon renewal lifecycle (renew before a fraction of the lease elapses, with jitter) and is intentionally NOT on the shared harness — it keeps its own VSO-derived reconcile loops. It is built into the same manager binary but should be reasoned about as a distinct subsystem.

License

The operator (everything outside the secret-sync paths above) is licensed under the Apache License 2.0 — see LICENSE. The secret-sync subsystem is MPL-2.0 as noted above.