• v0.12.7 4ee508ae81

    v0.12.7 Stable

    gitea_admin released this 2026-06-28 13:41:16 +00:00 | 98 commits to main since this release

    Added

    • In-cluster CI/CD pipeline (the seanfarm release path), completing the
      previously build-only pipeline: a make test (envtest) gate + the image-promote
      — skopeo-push the multi-arch image to registry.sean.farm/seanfarm/openbao-operator,
      cosign-EC sign it by digest, make build-installer, and an in-cluster release
      (ff main first, lightweight tag, Forgejo Release with these notes + the
      install.yaml asset). Adds ci-test.Dockerfile; curated notes switch to the
      Keep-a-Changelog ## [x.y.z] format (the ## vX.Y.Z entries below predate it).

    Unreleased

    No unreleased changes.

    v0.7.9

    OpenBaoIntermediateCA now imports the full chain into the intermediate
    backend.
    v0.7.x stopped at uploading the leaf intermediate cert via
    set-signed, so pki_internal only knew about itself — its
    /<backend>/ca_chain lookups returned [intermediate], and every leaf
    issued from pki_internal (cert-manager Certificates, OpenBaoPKICertificate)
    carried only the intermediate in ca.crt (typically [intermediate, intermediate]
    because OpenBao duplicates the issuing CA into ca_chain).

    Downstream consumers that walk the cert chain to a self-signed anchor —
    postgres ssl_ca_file in particular — then reject every client cert
    at depth 1 with unable to get issuer certificate. The failure mode
    surfaced on the first seanfarm make rebuild after the internal-ca
    wiring landed: postgres-2-join's pg_basebackup looped on
    tls: unknown certificate authority and replication never started.

    The fix concatenates signedCert.Certificate with every entry in
    signedCert.CAChain (the parent chain returned by sign-intermediate)
    into a single PEM bundle and passes that to SetSignedIntermediate. OpenBao
    imports each cert as a separate issuer entry on the backend, so subsequent
    /<backend>/issue and /<backend>/sign responses come back with the full
    chain.

    Existing intermediates imported by pre-v0.7.9 operators do NOT self-heal
    because the reconciler skips set-signed on the exists==true branch.
    For those backends either re-bootstrap (delete the OpenBaoIntermediateCA
    with deletionPolicy: Delete and let the operator regenerate) or invoke
    openbao write <backend>/intermediate/set-signed certificate=@bundle.pem
    manually with the concatenated chain.

    v0.7.8

    OpenBaoPKIIssuer now gates Ready=True on the backend having a usable
    default issuer.
    v0.7.5 added the auto-promote-after-set-signed fix but
    the OpenBaoPKIIssuer reconciler still flipped Ready as soon as the named
    PKI role existed in OpenBao. On a cold cluster boot, OpenBaoPKIRole
    reconciles can land before the parallel OpenBaoIntermediateCA reconcile
    finishes set-signed + ensureDefaultIssuer, so cert-manager sees a
    Ready issuer, dispatches a CertificateRequest, SignCSR hits

    could not fetch the CA certificate (was one set?):
    no default issuer currently configured
    

    and cert-manager marks the CertificateRequest as terminally Failed.
    The only recovery in pre-v0.7.8 deploys was kubectl delete certificaterequest.

    The reconciler now calls vc.GetIntermediateCA(backend) (which reads
    <backend>/cert/ca — exactly the endpoint OpenBao rejects when there is no
    default issuer) and refuses to flip Ready until that probe succeeds. New
    behaviour:

    • Role exists + default issuer set → Ready=True, Reason=Verified
      (unchanged).
    • Role exists + no default issuer → Ready=False,
      Reason=DependencyNotReady, requeue after TransientErrorRetryInterval.
    • Role missing → unchanged (Ready=False, Reason=ConfigError).

    cert-manager backoffs through the requeue window and re-issues a fresh
    CertificateRequest once the issuer flips Ready, so the cold-boot path
    self-converges with no manual intervention. Surfaced during the first
    seanfarm make rebuild after the internal-ca wiring landed — the dev
    cluster needed kubectl delete certificaterequest postgres-{server,replication}-tls-1
    once before CNPG started.

    New regression test
    TestOpenBaoPKIIssuerReconcile_NoDefaultIssuer reproduces the cold-boot
    race against the fake OpenBao client and asserts the issuer requeues with
    DependencyNotReady instead of flipping Ready.

    v0.7.7

    OpenBaoPKICertificate no longer pre-creates an empty kubernetes.io/tls
    Secret.
    The previous flow Get → Create empty → issue → Update lost
    every fresh reconcile to the apiserver's Secret-type validator:

    Secret "<name>" is invalid: [data[tls.crt]: Required value, data[tls.key]: Required value]
    

    kubernetes.io/tls Secrets must carry valid PEM cert + key bytes at
    Create time; the controller never reached the issuance step, so the
    Secret was never produced. Surfaced via the bundle-source
    OpenBaoPKICertificate in cert-manager for seanfarm's internal-ca
    distribution — every reconcile failed with the validation error and the
    trust-manager Bundle had no source.

    • Reordered the flow to Get → issue → Create-with-data (or Update if
      the Secret already existed and is owned by this CR). The in-memory
      secret object still carries the OwnerReference + SecretTypeTLS from
      the moment we know the Secret needs to exist, but the apiserver only
      sees the populated object.
    • needsIssuance now also triggers when the Secret does not yet exist,
      even if Status.SerialNumber somehow has a value — the on-disk Secret
      is what consumers actually read, so its absence forces a fresh leaf.
    • No interface or CRD changes; pure controller-flow refactor.

    v0.7.6

    OpenBaoPKIIssuer (cert-manager external issuer) now signs the submitted
    CSR instead of asking OpenBao to mint a fresh key pair.
    v0.7.5 still routed
    CertificateRequests to vc.IssueCertificate, which posts to
    /<backend>/issue/<role> and has OpenBao generate the private key. The cert's
    public key then mismatched cert-manager's local tls.key (cert-manager
    keeps its own key client-side and only stitches the signed certificate
    into the Secret), so cert-manager triggered an immediate re-issue with
    reason InvalidKeyPair — a tight loop that minted dozens of
    CertificateRequests per second per Certificate and filled OpenBao storage
    with unused leaves.

    • New openbaoclient.Client method SignCSR(ctx, backend, role, csrPEM, request) (CertificateInfo, error)
      posts to /<backend>/sign/<role> with the CSR body verbatim plus the
      same SAN/TTL/format parameters as IssueCertificate. The returned
      CertificateInfo has PrivateKey empty (the caller already has it).
    • openbaopkiissuer_controller.reconcileCertificateRequest now calls
      SignCSR with certReq.Spec.Request (the raw PEM CSR cert-manager
      put on the wire). IssueCertificate is reserved for OpenBaoPKICertificate,
      where there is no caller-side key.
    • fakeOpenBao.SignCSR records the call count + the last CSR PEM body; the
      PKI issuer test asserts the issuer hit /sign/<role> (not /issue/<role>)
      and that the CSR landed in OpenBao.

    v0.7.5

    OpenBaoIntermediateCA reconciler now promotes the imported issuer to be
    the backend's default.
    Without this, the first /<backend>/issue/<role>
    call after set-signed fails with no default issuer currently configured
    because OpenBao PKI v2 does not auto-promote unless
    default_follows_latest_issuer: true was written to
    /<backend>/config/issuers beforehand.

    • The fix runs on two paths: right after SetSignedIntermediate succeeds on
      the generate path, and on every reconcile that takes the
      intermediate CA exists branch — so legacy backends imported by older
      operator builds self-heal on the next sync (≤ DefaultResyncInterval)
      without a manual openbao write .../config/issuers intervention.
    • New helper ensureDefaultIssuer in
      internal/controller/openbaointermediateca_controller.go calls two new
      openbaoclient.Client methods:
      • ListPKIIssuers(ctx, backend) ([]string, error)LIST /<backend>/issuers.
      • SetDefaultPKIIssuer(ctx, backend, issuerRef string) error — writes
        /<backend>/config/issuers with default=<ref>. Idempotent.
    • Both methods are wired through Real, deferred, and the controller test
      fakeOpenBao. SetSignedIntermediate in the fake now appends to
      pkiIssuers[backend] so the existing
      TestOpenBaoIntermediateCAReconcile_GeneratesAndSignsIntermediateCA test
      can assert the default was set.
    • Surfaced while bootstrapping the per-stack internal-ca for seanfarm CNPG;
      every CertificateRequest against a OpenBaoPKIIssuer was returning
      IssuanceError: no default issuer currently configured even though the
      intermediate had Ready=True.

    v0.7.2

    Security hardening. Two findings from a directed audit of the operator's
    OpenBao-client and CRD surface, plus a small classifier addition that callers
    can use to drive smarter retries.

    Cross-namespace BackendRef rejected (confused-deputy)

    • OpenBaoGCPRoleset.spec.backendRef accepted an arbitrary .namespace and the
      reconciler honored it verbatim when probing OpenBaoGCPSecretsBackend.Ready.
      A CR in namespace A could observe Ready transitions of a backend in
      namespace B, bypassing the cross-namespace RBAC boundary as a timing
      side-channel and confusing the operator into completing a roleset write
      whose readiness gate had been validated against an out-of-namespace object.
    • The Namespace field on NamespacedName is now deprecated. CEL admission
      rejects any non-empty value
      (!has(self.namespace) || size(self.namespace) == 0); the controller's
      in-process check is kept as belt-and-suspenders so the failure mode is
      surfaced as Reason=InvalidSpec rather than silently widening trust scope.
    • The field is retained on the API for compatibility with stored CRs that may
      carry it; users must clear it on next edit.

    OpenBao-client cache eviction on auth failure

    • Factory.GetClient cached an authenticated *Real for up to 12 h. If the
      OpenBao-side token was revoked (operator-role rotation, manual revoke, policy
      change), every reconciler kept reusing the dead-token client until the TTL
      elapsed, surfacing as a cascade of ConfigurationError instead of a
      self-healing re-auth.
    • A new OnError hook on Real is invoked from each metric-instrumented
      method's error path. The Factory wires the hook to call Factory.Evict if
      the error classifies as 401/403, so the next GetClient re-authenticates.
    • New public helpers: openbaoclient.IsOpenBaoAuthError(err) (HTTP 401/403
      classifier) and Factory.Evict(address) (manual cache drop).

    Includes the v0.7.1 events RBAC fix

    The previously un-released b864c32 fix(rbac): grant events:create;patch
    ships in this release.

    v0.7.1

    Bug fix. v0.7.0 emitted Kubernetes Events but the operator SA lacked
    events: create;patch RBAC, so every emit was rejected by the API
    server (events is forbidden). v0.7.1 adds the kubebuilder rbac marker
    on the EmitNormal/EmitWarning helpers in internal/controller/common.go
    so the manager-role grants the verb. No code-path or image change.

    v0.7.0

    Operational hardening — events, RBAC split, image signing, lint zero.
    Four workstreams; backwards-compatible upgrade.

    Kubernetes events on every reconciler

    • record.EventRecorder is now threaded through all 21 reconcilers via a
      new Recorder field on each Reconciler struct. mgr.GetEventRecorderFor
      is wired in SetupWithManager so events flow without manager-config
      changes.
    • EmitNormal / EmitWarning helpers in internal/controller/common.go
      pair with every MarkDegraded site (Warning) and every primary write
      path (Normal). Drift-skip / idle-reconcile paths intentionally skip
      Normal emission to avoid event chatter.
    • kubectl describe on any openbao.sean.farm CR now surfaces the same
      reason and message as the status condition, so operators don't have to
      pivot between condition history and event log.

    RBAC: split secrets into reader + writer

    • manager-role (auto-generated) keeps only get;list;watch on
      secrets. The two controllers that mutate Secrets
      (openbaoapprolesecretid, openbaopkicertificate) document the split in
      their kubebuilder rbac markers.
    • New hand-maintained manager-secrets-writer ClusterRole +
      ClusterRoleBinding (config/rbac/secrets_writer_role*.yaml) carries
      the create;delete;patch;update verbs and binds to the same SA. The
      auto-generated role.yaml is now the canonical read-only audit
      surface; secret-write capability is reviewed at one explicit place
      and a future per-controller-SA refactor only has to rebind.

    Cosign image signing

    • make sign-image IMG=... runs cosign keyless OIDC signing; refuses
      :latest tags as a guard.
    • make verify-image IMG=... CERT_IDENTITY=... CERT_OIDC_ISSUER=...
      verifies provenance for downstream consumers (the seanfarm cluster
      pull pipeline can pin a verified identity before rolling out).

    Lint debt to zero

    • Auto-fixes from make lint-fix (modernize, QF1008/QF1003/S1009).
    • Manual fixes: TuneMountAllowNilWithContext migration in
      internal/openbaoclient/real.go; cmmeta.ObjectReference -> IssuerReference
      rename in pki issuer tests; lower-case error strings; line-wrap of
      long flag-help strings in cmd/main.go; rename of a shadowed
      reconcile import in metrics middleware; SA9003 empty branches.
    • New defaultAppRoleMount and defaultKubernetesAuthMount constants
      replace string literals.
    • Lint config raises gocyclo to 45 (reconcile loops are inherently
      branchy), restricts goconst to 5+ chars and 4+ occurrences, and
      excludes test files from goconst/unparam. Result: make lint
      reports 0 issues, down from 47 at the start of the v0.7.0 cycle.

    Migration

    • No CR or flag changes. Existing manifests apply unchanged.
    • RBAC: re-applying dist/install.yaml adds the new
      manager-secrets-writer ClusterRole and ClusterRoleBinding alongside
      manager-role. The old role's secrets rule narrows to get;list;watch;
      the SA retains writes via the new role.
    • Events: no opt-in; events emit immediately on first reconcile.

    v0.6.0

    Security hardening — tenant isolation + outbound OpenBao. Driven by the
    v0.5.x security audit. Seven workstreams; in-place upgrade with permissive
    defaults so seanfarm doesn't need to flip every flag at once.

    New flags

    • --openbao-address-allowlist — comma-separated regexes; a CR's resolved
      OpenBao address must match at least one. Empty default keeps v0.5
      permissive behavior; in production, set to e.g.
      ^https?://openbao\.(openbao-[a-z0-9-]+\.)?svc\.cluster\.local:8200$
      so a malicious CR cannot redirect operator writes to an external OpenBao.
    • --openbao-allow-plaintextnever | cluster-local | always.
      Default cluster-local accepts plaintext only for *.svc.cluster.local,
      localhost, 127.0.0.1. never enforces HTTPS everywhere.
    • --openbao-ca-cert — path to PEM bundle that pins the OpenBao server's
      TLS issuer; system roots ignored when set.

    Manifest changes

    • config/manager/manager.yaml mounts an audience-bound projected
      ServiceAccount token at /var/run/secrets/openbao.sean.farm/token
      (audience: openbao, expirationSeconds: 3600). The
      --openbao-sa-token-path flag's default flips to the new mount; legacy
      unbounded-token deployments must override.
    • config/rbac/leader_election_role.yaml pins get/update/patch/delete
      on configmaps and leases to resourceNames: [343a0da7.sean.farm].

    Hardening

    • OpenBaoRef CEL admission rule restored. Submitting a CR with both
      openbaoRef.name and openbaoRef.connection set is now rejected at
      admission. The original rule (commit 5c61915) was removed because the
      marker comment had Unicode curly double-quotes (U+201D) instead of
      ASCII "; reinstated with plain quotes.
    • TLS posture unified across bootstrap and per-OpenBaoRef paths. A new
      openbaoclient.ConfigureClient helper applies CA pin and plaintext gate
      to both cmd/bootstrap.buildOpenBaoClientOnce and
      factory.createAuthenticatedClient.
    • Field index on OpenBaoGCPRoleset.backendRef. Replaces the unbounded
      cluster-wide List in openbaogcpsecretsbackend's deletion path with an
      indexed lookup so spam-creation of unrelated rolesets cannot slow
      backend deletion.
    • Production-default zap logger. cmd/main.go flips
      Development: true → false. Verbose console output and stack traces
      at WarnLevel return only when --zap-devel is passed explicitly.

    Tests

    • TestConfigureClient_PlaintextGate (16 subtests) covers all three
      modes against representative addresses.
    • TestFactory_GetClient_Allowlist{Rejects,Accepts} covers the new
      address allowlist gate.
    • 4 new envtest specs in marker_validation_test.go pin the OpenBaoRef
      mutual-exclusion CEL rule (both / name-only / conn-only / neither).
    • TestBuildOpenBaoClientOnce_ValidationErrors extended with a
      plaintext-rejection case.

    v0.5.1

    Hot-fix on the v0.5.0 deploy. Caught immediately on cluster rollout.

    Reliability fix

    • IsOpenBaoServerNotReady no longer misclassifies a OpenBao 4xx whose body
      echoes a network keyword.
      When OpenBao performs an inline downstream
      check (e.g. verify_connection=true on
      database/config/<name> doing a TCP ping to the configured postgres
      host), a downstream DNS or socket failure is reported in the 400
      response body. The pre-fix raw-string fallback in
      IsOpenBaoServerNotReady matched on err.Error() and saw the keyword,
      routing what is actually a ReasonConfigurationError (OpenBao said
      "your config is bad") to ReasonDependencyNotReady (OpenBao is sealed
      or unreachable). The user would then chase a non-existent OpenBao
      outage while postgres connectivity was the real issue.

      Fix: when errors.As matches *openbaoapi.ResponseError, return the
      result of the API-response classification directly (without falling
      through to the raw-string fallback). The fallback now only applies
      to errors that never reached OpenBao — i.e. real operator-side
      network failures.

      New TestIsOpenBaoServerNotReady_4xxBodyKeywordIgnored regression-pins
      the exact 400-with-no such host shape that triggered this in the
      seanfarm cluster on the v0.5.0 rollout.

    • openbaodatabaseconnection_controller adds the
      IsOpenBaoConfigError branch
      that v0.5.0 added to the other
      reconcilers. A 4xx from OpenBao now classifies as
      ReasonConfigurationError with no requeue, instead of falling into
      the catch-all ReasonOpenBaoError that returns the error and triggers
      controller-runtime backoff. Missed in v0.5.0's W2 rollout.

    v0.5.0

    Correctness sweep — bug fixes, drift-detection gaps closed, JSON tag
    canonicalization. Includes one breaking v1alpha1 change (see "Breaking
    changes" below); since the operator is pre-1.0, in-place upgrades require
    re-applying CRs that use the renamed fields.

    Breaking changes

    • JSON tags standardized on lowercase initialisms. Inconsistent
      uppercase initialisms (tokenTTLSeconds, tokenBoundCIDRs,
      oidcDiscoveryURL, kubernetesCACert, connectionURL,
      defaultLeaseTTL, usePSS, extKeyUsageOIDs, etc.) renamed to their
      lowercase form (tokenTtlSeconds, tokenBoundCidrs,
      oidcDiscoveryUrl, kubernetesCaCert, connectionUrl,
      defaultLeaseTtl, usePss, extKeyUsageOids, etc.). 24 fields
      affected across 11 type files. Existing CRs using the old names will
      fail strict YAML validation; re-apply with the new field names. Go
      field names unchanged. A new api/v1alpha1/json_canonical_test.go
      meta-test prevents regressions.

    • OpenBaoPKIRootCert.spec.deletionPolicy enum tightened to Retain
      only.
      The operator does not implement root-cert revocation (would
      invalidate every issued cert), so the Delete value previously
      resulted in a silent no-op with a log warning. Admission now rejects
      Delete outright. CRs that explicitly set deletionPolicy: Delete
      must drop the field (default is Retain) or set it to Retain.

    Reliability fixes

    • StatusUpdateWithRetry helper added to internal/controller/common.go
      and applied to every success-path status update. Previously a 409 conflict
      on r.Status().Update after a successful OpenBao write would bubble up
      through the reconciler and trigger a redundant OpenBao write on the next
      pass. The helper retries the status update in place with a re-fetch +
      re-mutate loop so the OpenBao write stays single-shot. 15 call sites
      migrated.

    • Factory cache is now time-bounded (default 12h). Previously
      cached non-default-OpenBao clients stayed in the cache forever and
      silently held expired tokens long after the underlying OpenBao token
      TTL elapsed (default 24h). The cache now refreshes any entry older
      than CacheTTL. Configurable per-Factory; default 12h is safely
      below typical OpenBao token TTL.

    • Bootstrap type assertion fails loudly. If the future OpenBao
      client constructor returns anything other than *openbaoclient.Real,
      the build path now returns an explicit error rather than silently
      skipping the token-expiry watcher (which would have let the operator
      run with an expiring token).

    • OpenBaoRef rejects both name and connection set. The doc
      comment said mutually exclusive but enforcement was lax; reconciler
      silently used Connection and ignored Name. Now returns a clear
      ReasonInvalidSpec error.

    Drift-detection fixes

    • equalPKIRole extended with the v0.4.0 fields. Pre-fix, drift
      on issuerRef, noStore, generateLease, signatureBits,
      usePSS, keyUsage, extKeyUsage, extKeyUsageOIDs was invisible
      to the reconciler — OpenBao could be wildly out of sync with spec
      while the CR reported Ready=True.

    • appRoleConfigEqual switched to unordered comparison for
      secretIdBoundCidrs, tokenBoundCidrs, tokenPolicies.
      OpenBao
      treats these as sets; the previous order-sensitive comparison
      caused spurious OpenBao Puts every time the user re-ordered policies
      in spec.

    Failure-mode classification

    • Five-reason taxonomy documented and enforced. shared_types.go
      now codifies the rule:
      • InvalidSpec — caught before talking to OpenBao.
      • ConfigurationError — OpenBao rejected our request (4xx, won't
        self-heal).
      • OpenBaoError — unexpected server-side OpenBao error (5xx, parse
        fail).
      • TransientError — retriable client/network issue.
      • DependencyNotReady — precondition will become true on its own.
    • IsOpenBaoConfigError helper added to internal/openbaoclient for
      4xx-but-not-404 detection. 13 reconciler call sites re-classified to
      route OpenBao 4xx responses to ConfigurationError instead of
      OpenBaoError or TransientError.
    • MarkDegraded default branch logs loudly. Previously
      logf.Log.V(1).Info (verbose only); a controller passing an
      unsupported CRD type would silently no-op the status update. Now
      logs at error level so misconfiguration is visible in default
      operator logs.

    CRD validation markers

    • Added MinLength=1 to required string fields the reconcilers
      treated as required at runtime: OpenBaoPKIBackend.spec.path,
      OpenBaoPKIRootCert.spec.{backend, commonName},
      OpenBaoIntermediateCA.spec.{backend, parentBackend, commonName, ttl}, OpenBaoPKIRole.spec.backend,
      OpenBaoPKICertificate.spec.{backend, role, commonName},
      OpenBaoKVBackend.spec.path, OpenBaoAudit.spec.path,
      OpenBaoTransitBackend.spec.path,
      OpenBaoTransitBackend.spec.keys[].name. Empty-string admission is
      now rejected with a precise field reference rather than a generic
      reconciler-runtime error.

    Status correctness

    • OpenBaoPKIRootCert clears stale serial/expiration when the cert
      vanishes from OpenBao.
      A narrow race between the initial GetRootCert
      and the post-rotation re-Get could land in a state where the
      reconciler reported Ready=True with a stale serial while the cert
      was actually gone. Status closure now overwrites
      Status.SerialNumber and Status.Expiration unconditionally and
      marks Ready=False/DependencyNotReady when exists=false.

    Tests

    • New envtest specs validate every config/samples/*.yaml against
      the live admission schema (internal/controller/samples_validation_test.go).
    • New per-CRD admission tests for the MinLength=1 markers
      (internal/controller/marker_validation_test.go, 16 specs).
    • New TestStatusUpdateWithRetry_* suite covers the conflict-retry
      helper.
    • New TestOpenBaoPKIRoleDriftDetection_v04Fields exercises drift on
      each v0.4.0 PKI role field.
    • New per-controller integration tests for OpenBaoGCPSecretsBackend
      (7 tests) and OpenBaoGCPRoleset (8 tests) — these controllers had
      zero tests before this slice.
    • New TestStatusTimestamp_RapidReconciles replaces the three
      time.Sleep(1.1s) flakes in status_timestamp_test.go. Total
      controller test wall-clock down from ~10s to ~6s.
    • New factory tests for the time-bounded cache, mutual-exclusion
      rejection, and default-client no-cache behavior.

    Cleanup

    • Removed log.Error(nil, ...) anti-pattern at 9 sites; demoted to
      log.V(1).Info where the kv pairs aided debugging or dropped
      outright where the post-fix MarkDegraded(InvalidSpec) carried the
      user-visible signal.
    • Removed unused ObjectRefFromOwner stub in internal/openbaoclient.
    • Removed 9 dead per-controller markDegraded methods left over from
      before common.go's helper.
    • Removed duplicate stringMapEqual (use equalStringMap).
    • Removed unreachable empty-name guard in
      openbaotransitbackend_controller.go after MinLength=1 on
      TransitKeySpec.Name.

    v0.4.0

    Sprint 2: Medium-Priority Field Implementation - Added 33 new configuration fields across Database, PKI, Transit, KV, and AppRole engines. Improved error handling and retry logic for faster recovery from transient failures.

    New Fields Added (33 total)

    Database Engine (9 fields)

    • OpenBaoDatabaseConnection: verifyConnection, rootRotationStatements, passwordPolicy, allowedRoles, rootCredentialsRotateStatements
    • OpenBaoDatabaseDynamicRole: renewStatements, rollbackStatements, credentialType

    PKI Engine (15 fields)

    • OpenBaoPKIBackend: ocspServers, issuerName, enabledIssuers, manualChain, aiaPathEnableGlobal
    • OpenBaoPKIRole: issuerRef, noStore, generateLease, signatureBits, usePSS, keyUsage, extKeyUsage, extKeyUsageOIDs, clientFlag, codeSigningFlag, emailProtectionFlag
    • OpenBaoPKICertificate: format (pem/der/pem_bundle)

    Transit Engine (4 fields)

    • OpenBaoTransitBackend: forceNoCache, cacheSize, minDecryptionVersion, minEncryptionVersion

    KV Engine (3 fields)

    • OpenBaoKVBackend: casRequired, deleteVersionAfter, customMetadata (KV v2 only)

    AppRole Auth (2 fields)

    • OpenBaoAppRole: localSecretIDs, roleID (custom role-id support)

    Bug Fixes

    • Faster recovery from transient failures: Changed all GetOpenBaoClientOrDegrade failure paths to return TransientErrorRetryInterval (30s) instead of DefaultResyncInterval (5m). Speeds up recovery when OpenBao is temporarily sealed, in standby, or unreachable.
    • Improved error classification: GetOpenBaoClientOrDegrade now distinguishes OpenBaoServerNotReady errors and marks them as ReasonDependencyNotReady instead of generic ReasonTransientError.
    • Fixed database connection retry: When password Secret is missing, now uses 30s retry interval (was 5m).

    Test Improvements

    • Removed obsolete validation tests (TestOpenBaoDatabaseDynamicRoleReconcile_InvalidTTL, TestOpenBaoPKIRoleReconcile_InvalidMaxTTL) - validation now handled by CRD admission with +kubebuilder:validation markers.
    • All controller tests passing.

    Technical Details

    • Added ConfigurePKIIssuers and UpdatePKIIssuer methods to openbaoclient for PKI issuer management
    • Added ConfigureKVV2 method for KV v2-specific configuration
    • Extended CreateTransitKey signature to support new cache and version control parameters
    • Updated AppRoleConfig struct with new fields and drift detection

    v0.3.0

    Quick Wins: Operational Improvements - Five operational improvements for faster recovery, reduced API calls, and better user experience. No new OpenBao API features - focused on reliability and performance.

    Quick Win 1: Secret Watches (Auto-Reconcile on Secret Changes)

    • OpenBaoJWTAuthBackend: Watch 4 optional SecretRefs (OIDC client ID/secret, discovery CA, JWKS CA)
    • OpenBaoDatabaseConnection: Watch password SecretRef
    • Secret updates trigger immediate reconciliation without manual CR annotation
    • Eliminates operational friction for password rotation and certificate renewal
    • Existing generation+hash drift detection prevents infinite loops

    Quick Win 2: Faster Transient Error Retry

    • Add TransientErrorRetryInterval (30 seconds) constant for OpenBao unavailability
    • Replace 5-minute retry with 30-second retry when OpenBao sealed/network errors/not ready
    • Updated 49 occurrences across 21 controllers
    • Maintains 5-minute interval for normal drift detection and dependency resolution
    • Recovery time: 30s instead of 5min for transient issues

    Quick Win 3: Remove Redundant GET After PUT (JWT Auth Backend)

    • PutJWTAuthBackend now returns JWTAuthBackend directly instead of just error
    • Internal implementation performs GET after successful PUT and returns result
    • Controller no longer needs separate GET call to populate status.accessor
    • Reduces OpenBao API calls by 33% for JWT auth backend writes (1 call instead of 2)
    • Simplifies controller logic (32 lines → 10 lines)

    Quick Win 4: Password Drift Detection (Database Connections)

    • Track SHA256 hash of password in status.appliedPasswordHash
    • Skip OpenBao write when generation/name/hash unchanged and OpenBao state matches spec
    • Password change (Secret update) triggers write via Secret watch
    • Non-password spec change triggers write
    • Touch CR (kubectl annotate) with no changes skips write (drift check passes)
    • Password security preserved (one-way SHA256 hash in status, not reversible)

    Quick Win 5: Dry-Run Mode

    • Enable simulation via openbao.sean.farm/dry-run="true" annotation
    • Controllers skip OpenBao client creation and write operations
    • Immediately mark status.Ready=True with reason=DryRun
    • All 21 controllers support dry-run mode
    • Useful for testing CR syntax, validating manifests, CI pipelines
    • Zero OpenBao API calls in dry-run mode
    • Explicit opt-in via annotation (no default behavior change)

    Impact Summary

    • Faster recovery: 30s retry for transient errors (was 5min)
    • Automatic propagation: Secret changes reconcile immediately (was manual)
    • Reduced API calls: JWT auth backend writes 33% fewer calls
    • Better drift detection: Database connections skip unnecessary password writes
    • Safe testing: Dry-run mode for previewing OpenBao operations

    Files Modified

    • Added CRD field: OpenBaoDatabaseConnection.status.appliedPasswordHash
    • Updated 21 controllers with Secret watches, faster retry, and dry-run support
    • Updated openbaoclient interface: PutJWTAuthBackend signature change
    • Updated 6 test files for TransientErrorRetryInterval expectations

    v0.2.0

    Sprint 1: Token Security Hardening - Add 18 high-priority token lifecycle and identity fields across Kubernetes, JWT/OIDC, and AppRole auth backends.

    Kubernetes Auth Role (7 fields)

    • tokenExplicitMaxTTLSeconds: Hard cap on token lifetime including renewals
    • tokenNoDefaultPolicy: Exclude default policy (least privilege)
    • tokenNumUses: Maximum token usage count
    • tokenPeriodSeconds: Periodic token renewal window for indefinite renewals
    • boundServiceAccountNamespaceSelector: Label selector for dynamic namespace matching
    • aliasNameSource: Entity alias generation method (serviceaccount_uid/serviceaccount_name)
    • aliasMetadata: Custom entity alias metadata

    JWT/OIDC Auth Role (11 fields)

    • userClaimJsonPointer: Enable JSON pointer syntax for nested claims (e.g., data/user/email)
    • expirationLeewaySeconds: Tolerance for expiration claim checks
    • notBeforeLeewaySeconds: Tolerance for not-before claim checks
    • boundClaimsType: Claim matching mode (string vs glob)
    • verboseOIDCLogging: Enable detailed OIDC logging for debugging
    • maxAgeSeconds: Authentication recency requirement
    • tokenExplicitMaxTTLSeconds: Hard token expiration cap
    • tokenNoDefaultPolicy: Exclude default policy
    • tokenNumUses: Maximum token uses
    • tokenPeriodSeconds: Periodic token renewal window
    • aliasMetadata: Custom entity alias metadata

    AppRole Auth Role (5 fields)

    • tokenExplicitMaxTTLSeconds: Hard expiration cap
    • tokenPeriodSeconds: Periodic token duration
    • tokenType: Token category (default/service/batch)
    • tokenNoDefaultPolicy: Exclude default policy
    • aliasMetadata: Pre-populated entity alias data

    Impact

    • Enables comprehensive token lifecycle management (explicit max TTL, periodic tokens, usage limits)
    • Supports principle of least privilege (no default policy option)
    • Adds identity enrichment (alias metadata across all auth methods)
    • Implements dynamic namespace discovery (Kubernetes label selectors)
    • Supports nested JWT claims (JSON pointer syntax)

    v0.1.0

    Feature completeness for supported engines - Add 13 high-value fields to reach 60-65% coverage of OpenBao API capabilities for existing auth backends and secrets engines (no new backends added).

    Kubernetes Auth (2 fields)

    • tokenMaxTTL: Maximum token renewal TTL (security hardening)
    • tokenBoundCIDRs: CIDR-based source IP restrictions (already in API, now fully wired)

    JWT/OIDC Auth (2 fields)

    • boundClaims: Claim-based authentication restrictions for fine-grained access control
    • tokenType: Service/batch/default token types for performance optimization

    Database Secrets (2 fields)

    • rootRotationStatements: Automatic root credential rotation (OpenBao 1.8+)
    • passwordPolicy: Password generation policy references (OpenBao 1.13+)

    PKI Secrets (7 fields)

    • Distinguished Name fields for certificate compliance: country, organization, organizationalUnit, locality, province, streetAddress, postalCode
    • Added to both OpenBaoPKIRootCert and OpenBaoPKIIntermediateCA for enterprise certificate requirements

    Transit Secrets (2 fields)

    • derived: Key derivation support for context-based subkeys
    • convergentEncryption: Deterministic encryption for deduplication use cases

    Bug Fixes

    • Fixed 109 bugs across 40 rounds of systematic bug hunting:
      • Migrated all 21 controllers to generic helpers (GetOpenBaoClientOrDegrade, EnsureFinalizerOrRequeue)
      • Added spec validation to 6 controllers (OpenBaoDatabaseConnection, OpenBaoDatabaseDynamicRole, OpenBaoDatabaseStaticRole, OpenBaoJWTAuthBackend, OpenBaoJWTAuthRole, OpenBaoKubernetesAuthRole)
      • Sanitized error messages to prevent credential leakage (removed 20+ instances of err.Error() exposure)
      • Standardized error handling across all reconcileDelete functions
      • Added nil check preconditions for optional ref fields

    Internal

    • All CRDs regenerated with kubebuilder markers
    • Test coverage: 57.4% (2 pre-existing TTL validation test failures remain, documented as out of scope)
    • Code quality: Reduced duplication by ~1,000 lines through generic helper adoption

    v0.0.3

    Add generic JWT/OIDC auth backend and role support so OpenBao UI / OIDC login can
    be managed declaratively without the Terraform OpenBao provider.

    • api/v1alpha1: add OpenBaoJWTAuthBackend and OpenBaoJWTAuthRole under
      openbao.sean.farm/v1alpha1. OpenBaoJWTAuthBackend manages both the auth mount
      (sys/auth/<mount>) and config (auth/<mount>/config) with same-namespace
      secret refs for OIDC client secret and CA material. OpenBaoJWTAuthRole
      manages auth/<mount>/role/<name> with optional backendRef and the same
      policyRefs pattern used by Kubernetes-auth roles.
    • internal/openbaoclient: add typed JWT/OIDC backend and role operations,
      including adoption-safe backend writes that enable the mount only when absent
      and otherwise reconcile config in place.
    • internal/controller: add JWT/OIDC backend and role reconcilers, drift
      detection helpers, and unit coverage for backend adoption, dependency-not-
      ready gating, role rewrite, and Retain finalization.
    • cmd/main.go: register the new controllers with the shared deferred OpenBao
      client bootstrap path.
    • dist/install.yaml: publish the two new CRDs and updated RBAC/controller
      deployment surface.

    v0.0.1

    Initial scaffold and first-pass Terraform-replacement surface.

    • api/v1alpha1: 4 CRDs (OpenBaoPolicy, OpenBaoKubernetesAuthRole,
      OpenBaoDatabaseStaticRole, OpenBaoDatabaseDynamicRole) under
      openbao.sean.farm/v1alpha1. Shared DeletionPolicy (Delete/Retain),
      LocalObjectReference, unified condition reason taxonomy
      (Ready, Reconciling, DependencyNotReady, OpenBaoError,
      TransientError, InvalidSpec), and openbao.sean.farm/finalizer.
    • internal/openbaoclient: typed client interface, real implementation
      over hashicorp/openbao/api, deferred wrapper surfacing
      ErrOpenBaoClientNotReady until bootstrap installs a live client, and
      IsOpenBaoServerNotReady / IsOpenBaoNotFound classifiers consumed by
      reconcilers.
    • internal/controller: 4 reconcilers. Drift-skip upsert, Delete/
      Retain finalize, observedGeneration tracking, policyRefs
      resolution on OpenBaoKubernetesAuthRole (not-Ready refs keep the role
      at DependencyNotReady so OpenBao never sees a dangling policy name).
    • cmd/main.go: Kubernetes-auth bootstrap goroutine with exponential
      backoff (cap 30s), token-expiry watcher via
      auth/token/lookup-self cancelling the root context on low TTL, and
      a split /readyz/ping + /readyz/openbao-bootstrap. Seven new manager
      flags: --openbao-address, --openbao-auth-path, --openbao-kubernetes-role,
      --openbao-sa-token-path, --openbao-token-poll-interval,
      --openbao-token-renew-threshold, --openbao-bootstrap-base-backoff.
    • Makefile: non-mutating build-installer (traps and restores
      config/manager/kustomization.yaml), verify-installer drift guard,
      smoke-install (Kind + dist/install.yaml; asserts a OpenBaoPolicy
      lands at Ready=False/DependencyNotReady when no OpenBao is configured,
      which is the correct transient state), cleaner docker-buildx without
      a Dockerfile.cross sidecar.
    • test/e2e: minimal happy-path + failure-path reconcile suite.
      BeforeSuite builds + loads the image, installs CRDs, deploys the
      manager. fixtures_test.go stands up an in-cluster OpenBao dev server,
      runs a bootstrap Job that enables Kubernetes auth and writes an
      operator policy covering every controller's OpenBao surface, and
      patches the operator Deployment with the OpenBao flags.
      openbao_reconcile_test.go: happy path applies a OpenBaoPolicy and
      expects Ready=True; failure path scales OpenBao to zero, touches the
      CR, and expects Ready=False/DependencyNotReady. Scaffold
      metrics/curl specs dropped (no webhooks, no cert-manager).
    • dist/install.yaml: single flat manifest pinned to
      codeberg.org/someara/openbao-operator:v0.0.1.
    Downloads