- Go 97.4%
- Makefile 1.4%
- Shell 0.8%
- Dockerfile 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| .devcontainer | ||
| api | ||
| cmd | ||
| config | ||
| dist | ||
| hack | ||
| internal | ||
| test | ||
| .custom-gcl.yml | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| .golangci.yml | ||
| AGENTS.md | ||
| CHANGELOG.md | ||
| ci-test.Dockerfile | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| operator.mk | ||
| PROJECT | ||
| README.md | ||
| VERSION | ||
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 (
ConvergeandDelete).Convergedrives OpenBao toward the spec and reportsConvergedorProgressing. It must returnProgressing— neverConverged— until it has verified the remote state.Deleteremoves the OpenBao-side object. - A Taxonomy table (
openbaoTaxonomy) that classifies each failure reason.Terminalreasons (InvalidSpec,ConfigurationError) get no requeue until the spec changes.FixedRequeuereasons (DependencyNotReady,TransientError) self-heal on the transient cadence.Backoffcovers 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
resolveClientturns anOpenBaoRefinto an OpenBao client through the caching client factory. It maps a not-ready server toDependencyNotReady. It maps other failures toTransientError. Both cases self-heal on the transient cadence instead of terminal-parking.adapted[T]bridges the openbaoHandleronto the library's Handler. It supplies the package finalizer. It treatsOpenBaoNotFoundon read or delete as success, for idempotent finalize. It classifies other errors throughclassifyOpenBaoError.- Deletion uses
HoldUntilSuccess. AdeletionPolicy=Retainresource 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
DeletionPolicytype is a type-alias of the library's. So every CRD'sGetDeletionPolicysatisfies 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:
OpenBaoPKIIssuerissues 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 incmd/main.gois 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. AForgejoPushMirrorbacks up the repo tocodeberg.org/someara/openbao-operatorroughly every ten minutes.make rebuildre-seeds Forgejo from that codeberg backup on a fresh cluster. Go module fetches (for this module andlibseanfarm-operator) read codeberg through the defaultGOPROXY. - 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.