Kubernetes operator for managing Vault resources (database roles, policies, auth roles)
  • Go 97.4%
  • Makefile 1.4%
  • Shell 0.8%
  • Dockerfile 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Sean OMeara d72d96bbc0
All checks were successful
binjovi/ci Binjovi completed the frozen plan
docs: explain native delivery and task telemetry
2026-09-09 18:23:05 +02:00
.devcontainer Initial scaffold: kubebuilder v4 + 4 Vault CRDs 2026-04-20 19:58:36 +02:00
api docs: rewrite documentation to ASD-STE100 2026-08-07 19:20:45 +02:00
cmd fix(security): constrain auth endpoints and certificate issuance 2026-08-06 12:15:16 +02:00
config docs: rewrite documentation to ASD-STE100 2026-08-07 19:20:45 +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 docs: rewrite documentation to ASD-STE100 2026-08-07 19:20:45 +02:00
test docs: rewrite documentation to ASD-STE100 2026-08-07 19:20:45 +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 docs: rewrite documentation to ASD-STE100 2026-08-07 19:20:45 +02:00
CHANGELOG.md docs: rewrite documentation to ASD-STE100 2026-08-07 19:20:45 +02:00
ci-test.Dockerfile fix(ci): pin golang:1.25 builder base by digest 2026-07-05 13:19:44 +02:00
Dockerfile perf(openbao): namespace BuildKit Go caches 2026-09-06 00:18:31 +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 docs: explain native delivery and task telemetry 2026-09-09 18:23:05 +02:00
VERSION chore: devbump 0.12.29 -> 0.12.30-dev 2026-07-24 09:58:48 +00:00

openbao-operator

openbao-operator is a Kubernetes operator. It manages OpenBao (a HashiCorp Vault fork) configuration declaratively from custom resources. You do not run Terraform or issue imperative bao write calls. Instead, you express OpenBao state as Kubernetes objects. This state includes ACL policies, auth roles, PKI hierarchies, database engines, transit keys, and the GCP secrets engine. The operator continuously reconciles the live OpenBao server toward that spec. openbao-operator is one of the seanfarm operator fleet. The fleet also includes forgejo-operator, kratos-identity-operator, and minio-resource-operator. These operators share the same reconcile harness.

The operator authenticates to OpenBao through Kubernetes auth. It uses its own ServiceAccount token. No root token is in flight at steady state. The operator starts even when OpenBao is unreachable. It retries login in the background. This way, a cold-cluster bootstrap never deadlocks on ordering.

Custom resources

There are 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. Each kind carries spec.openBaoRef (which server), spec.deletionPolicy (Delete default, Retain), and status.observedGeneration. Each kind also carries 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. It integrates with cert-manager to issue certificates from OpenBao PKI.
OpenBaoPKIRole A PKI role that limits which certificates the mount can issue.
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 and token or key generation) on a GCP backend.

OpenBaoKubernetesAuthRole.spec.policyRefs[] and the PKI and intermediate-CA parent references resolve to sibling CRs. A missing or not-yet-Ready reference holds the dependent at Ready=False/DependencyNotReady. This way, 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 or leased secret from OpenBao into a Kubernetes Secret, and keep it renewed.

Architecture

Shared reconcile harness

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

Each controller supplies only a thin policy shim. All of it lives in internal/controller/harness.go:

  • A Handler (Converge and 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) that classifies each failure reason. Terminal reasons (InvalidSpec, ConfigurationError) get no requeue until the spec changes. FixedRequeue reasons (DependencyNotReady, TransientError) self-heal on the transient cadence. Backoff covers anything else, through controller-runtime.

Ready is structural. Only the harness sets Ready=true — the handler never sets it directly. The harness sets Ready=true only after Converge reports the external state as verified. The library enforces "Ready means verified". Each controller does not re-implement this rule.

Openbao-specific wiring

  • resolveClient turns an OpenBaoRef into an OpenBao client through the caching client factory. It maps a not-ready server to DependencyNotReady. It maps other failures to TransientError. Both cases self-heal on the transient cadence instead of terminal-parking.
  • adapted[T] bridges the openbao Handler onto the library's Handler. It supplies the package finalizer. It treats OpenBaoNotFound on read or delete as success, for idempotent finalize. It 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. 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). They re-enqueue the owning CR when the Secret lands or changes. This way, a credential that arrives after the CR converges triggers reconciliation with no manual nudge needed.
  • cert-manager integration: OpenBaoPKIIssuer issues certificates from OpenBao PKI through cert-manager.

Reliability: auth self-heal

A permission-denied read (OpenBao 401 or 403) can happen when the operator token is rotated or revoked server-side, or during a roll-window race. This case 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. (The operator matches this auth case 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 and messages, and Kubernetes Events, land in world-readable places (status, kubectl describe, the API audit log). For this reason:

  • Every condition or Event message is bounded (the library caps message length at 512 bytes). The library emits every message through its SanitizingRecorder. Every recorder in cmd/main.go is wrapped in it.
  • 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 and key. They never surface credential bytes. OpenBao client errors are reduced to a short summary. The handlers do not interpolate the raw error, because its .Error() can echo a request URL or a server response body. That body can contain 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

  • Images are built in-cluster. Native Binjovi tasks run the tests and build AMD64 and ARM64 candidates with BuildKit. Release publishes the exact candidate digests and verifies their cosign signatures. Local image builds are not the cluster delivery path.
  • Forgejo (code.sean.farm) is the canonical day-to-day remote. A ForgejoPushMirror backs up the repo 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 (for this module and libseanfarm-operator) read codeberg through the default GOPROXY.
  • CI uses native Binjovi agents. This project does not use Argo Workflows, Tekton, Shipwright, or repository-hosted Actions for delivery.
  • 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, then refactor. Tests live alongside each reconciler under internal/controller/ (envtest-backed). Secret-sync subsystem tests live under internal/controller/secrets/. Bootstrap goroutine tests live under cmd/.

Standard targets (make help for the full list):

Target What it does
make test Generate, fmt, vet, then run the unit and 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 and an in-cluster OpenBao dev server, then run the e2e suite.

How a change ships: edit, then make test (green), then push to Forgejo. Native Binjovi Build and Release tasks produce the cosign-signed image. The deployed tag then advances through the Flux and Crossplane composition pins that reference it. No developer machine builds or pushes an image for cluster use.

Binjovi Build and Release proof

Binjovi tests each pull request in an isolated envtest task. It then builds one AMD64 and ARM64 OCI candidate from the exact revision. Binjovi is now the guarded Build and Release authority. Pipelines no longer declares this project. This revision also proves immediate signed webhook delivery and schedulable image execution under shared cluster load.

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. It is 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: it renews before a fraction of the lease elapses, with jitter. It is intentionally NOT on the shared harness — it keeps its own VSO-derived reconcile loops. It runs in the same manager binary. Treat it as a distinct subsystem.

License

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

Native CI

Binjovi builds pull requests with the recipe in sean/binjovi-plans:recipes/openbao-operator/. The active plan fixes the recipe revision for each build. Recipe changes use a plans pull request and release; they do not require a Binjovi runtime deployment.

The native Binjovi result is the only admission result for this project.

Task timing and release verification

Open the project dashboard and expand an execution attempt for the native task timeline and available CPU, off-CPU, and syscall profiles. Release is this project's pipeline endpoint; there is no configured Deploy target. Consumer image pins are separate changes.

Use binjovictl release openbao-operator --build BUILD_ID --wait for an exact successful Build. Confirm the published image digest and signature in Release evidence. A task profile explains execution; it does not replace artifact verification.