- Go 97.3%
- Makefile 1.5%
- 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
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).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) classifying each failure reason asTerminal(InvalidSpec,ConfigurationError— no requeue until the spec changes),FixedRequeue(DependencyNotReady,TransientError— self-heal on the transient cadence), orBackoff(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
resolveClientturns anOpenBaoRefinto an OpenBao client via the caching client factory, mapping a not-ready server toDependencyNotReadyand other failures toTransientErrorso both self-heal on the transient cadence rather than terminal-parking.adapted[T]bridges the openbaoHandleronto the lib's: it supplies the package finalizer, treatsOpenBaoNotFoundon read/delete as success (idempotent finalize), and 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 — while 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) 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:
OpenBaoPKIIssuerissues 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 incmd/main.gois 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. AForgejoPushMirrorbacks the repo up tocodeberg.org/someara/openbao-operatorroughly every ten minutes;make rebuildre-seeds Forgejo from that codeberg backup on a fresh cluster. Go module fetches (this module andlibseanfarm-operator) read codeberg through the defaultGOPROXY. - 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.