Repository navigation
feat: Replace Helm lookup with OpenBao + ESO + Operator architecture #1942
Copy link
Copy link
Open
Labels
area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/featureCategorizes issue or PR as related to a new featureCategorizes issue or PR as related to a new featurelifecycle/frozenIndicates that an issue or PR should not be auto-closed due to stalenessIndicates that an issue or PR should not be auto-closed due to stalenesspriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completeImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked onIndicates an issue is ready to be actively worked on
Description
Summary
Replace all Helm lookup functions with a proper secrets management architecture using OpenBao (open-source Vault fork), External Secrets Operator (ESO), and cozystack-controller for secret generation.
Why OpenBao instead of HashiCorp Vault?
HashiCorp Vault uses BSL 1.1 license which restricts "hosting or embedding" in competitive offerings sold to third parties. Since Cozystack is a PaaS platform, embedding Vault could create licensing risks. OpenBao is a Linux Foundation project under MPL 2.0 with full Vault API compatibility and no such restrictions.
Problem
Current charts use Helm's lookup function extensively for:
- Preserving generated passwords across upgrades
- Reading existing secrets/specs
- Validating cluster resources
- Managing ownerReferences
Issues with lookup
- GitOps incompatibility: Returns
nilwithhelm template, breaking ArgoCD - Testing difficulty: Cannot test charts with dry-run
- Scattered logic: Secret generation spread across many charts
- No audit trail: No visibility into secret access/changes
- No rotation: Passwords never rotate unless manually changed
Proposed Architecture
┌─────────────────────────────────────────────────────────────┐
│ cozystack-controller │
│ - Deploys OpenBao as part of system bundle │
│ - Generates secrets in OpenBao for new applications │
│ - Manages ownerReferences, validation, ordering │
└─────────────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ System Bundle │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │ OpenBao │ │ ESO │ │ cert-manager │ │
│ │ (NEW) │ │ (exists) │ │ (exists) │ │
│ └────┬─────┘ └────┬─────┘ └──────────────┘ │
│ │ │ │
│ │ K8s Secret │ ClusterSecretStore │
│ │ auto-unseal │ → OpenBao backend │
└───────┴─────────────┴───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ Application Charts │
│ │
│ ExternalSecret instead of lookup + randAlphaNum: │
│ │
│ apiVersion: external-secrets.io/v1beta1 │
│ kind: ExternalSecret │
│ spec: │
│ secretStoreRef: │
│ name: cozystack-openbao │
│ kind: ClusterSecretStore │
│ target: │
│ name: {{ .Release.Name }}-credentials │
│ dataFrom: │
│ - extract: │
│ key: apps/{{ .Release.Namespace }}/{{ .Release.Name }}│
└──────────────────────────────────────────────────────────────┘
Implementation Plan
Phase 1: Infrastructure
- Add
packages/system/openbaowith Kubernetes auth and auto-unseal - Configure ClusterSecretStore for ESO → OpenBao
- Add OpenBao to system bundle
Phase 2: Operator Enhancement
- cozystack-controller generates secrets in OpenBao when creating apps
- Move ownerReferences management to operator
- Move validation to admission webhooks (see feat(cozystack-api): Add validation webhook for instanceType and instanceProfile #1941)
- Move HelmRelease ordering to operator
Phase 3: Chart Migration
- Replace
lookupfor secrets with ExternalSecret (mysql, postgres, redis, etc.) - Replace
lookupfor TLS with cert-manager references - Remove validation
lookupafter feat(cozystack-api): Add validation webhook for instanceType and instanceProfile #1941 - Migrate remaining lookup cases
Phase 4: Cleanup
- Remove all
lookupfrom charts - Document new secret management approach
- Migration guide for existing installations
Phase 5: Explicit Resource Names
- Add
resourceNamesto values schema for explicit resource naming - Replace
{{ include "chart.fullname" . }}with configurable names - Default to current behavior when values are empty (backward compatibility)
Example values.yaml:
resourceNames:
deployment: "" # default: {{ .Release.Name }}
service: "" # default: {{ .Release.Name }}
configmap: "" # default: {{ .Release.Name }}-config
secret: "" # default: {{ .Release.Name }}-credentialsBenefits:
- Deterministic resource names (no template magic)
- Easier debugging and tracking
- Full GitOps compatibility
- Customizable without chart modifications
Benefits
| Aspect | Current (lookup) | Proposed (OpenBao + ESO + Operator) |
|---|---|---|
| GitOps | ❌ ArgoCD broken | ✅ Full compatibility |
| Testing | ❌ helm template fails | ✅ Works |
| Security | ❌ No audit | ✅ OpenBao audit log |
| Rotation | ❌ Manual only | ✅ Automatic rotation possible |
| Centralization | ❌ Scattered in charts | ✅ Operator controls |
| Naming | ❌ Computed from templates | ✅ Explicit in values |
| Licensing | N/A | ✅ MPL 2.0 (no embedding restrictions) |
Affected Charts (lookup usage)
Secrets generation:
- mysql, postgres, redis, nats, clickhouse, rabbitmq, mongodb
- vpn (passwords + TLS)
- keycloak, dashboard (OAuth secrets)
- grafana, alerta (admin passwords)
Other lookup uses:
- tenant (namespace uid for ownerReferences)
- virtual-machine, vm-instance, vm-disk (validation, spec preservation)
- kubernetes (instanceType lookup, patch-containerd, MachineTemplate preservation)
- platform (version tracking, migrations)
Open Questions
- OpenBao HA: Required for production? Storage backend (Raft vs external)?
- Unseal strategy: K8s Secret (simple) vs Cloud KMS (more secure)?
- Migration path: How to migrate existing secrets to OpenBao?
- Optional vs Required: Should OpenBao be optional with fallback to current behavior?
Related
- feat(cozystack-api): Add validation webhook for instanceType and instanceProfile #1941 - Validation webhook for instanceType/instanceProfile
- ESO already exists:
packages/system/external-secrets-operator - OpenBao - Linux Foundation project, MPL 2.0 licensed Vault fork
- ESO OpenBao provider - ESO uses same provider for Vault/OpenBao
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area/platformIssues or PRs related to platform infrastructure (bundle, flux, talos, installer)Issues or PRs related to platform infrastructure (bundle, flux, talos, installer)kind/featureCategorizes issue or PR as related to a new featureCategorizes issue or PR as related to a new featurelifecycle/frozenIndicates that an issue or PR should not be auto-closed due to stalenessIndicates that an issue or PR should not be auto-closed due to stalenesspriority/important-longtermImportant over the long term, but may not be staffed and/or may need multiple releases to completeImportant over the long term, but may not be staffed and/or may need multiple releases to completetriage/acceptedIndicates an issue is ready to be actively worked onIndicates an issue is ready to be actively worked on
Type
Projects
- StatusShow more project fieldsTodo
Activity
[-]feat: Replace Helm lookup with Vault + ESO + Operator architecture[/-][+]feat: Replace Helm lookup with OpenBao + ESO + Operator architecture[/+]2mind commentedon Feb 11, 2026
Aleksei Sviridkin (@lexfrei) Thanks for working on this! I have a question about the proposed architecture -does cozystack-controller really need to generate and write secrets directly to OpenBao?
Why this feels a bit off:
Alternative: use ESO Generators
1. Password generator (declarative)
2. ExternalSecret references the generator
Simplified architecture:
Benefits:
Fully declarative
OpenBao becomes optional (can be enabled later)
Less code in operator
Leverages ESO which is already there
github-actions commentedon May 2, 2026
This issue has had no activity for 60 days and was marked
lifecycle/stale.It will be closed in 14 days unless commented or labelled
lifecycle/frozen.myasnikovdaniil commentedon Jun 2, 2026
Reviving this — it's the right north star, and the platform has drifted enough since January that it's worth refreshing the scope. I did a full audit of how credentials are handled across every package today, and I want to reframe the goal slightly around two user-facing properties that matter more than the mechanism (
lookup→ OpenBao) we use to get there:A single, systemwide OpenBao wired into the cluster is then the backend that makes #2 secure and adds audit/rotation — one platform-managed instance every tenant app can draw from, rather than per-app credential plumbing — but it should be optional, layered on top of a contract that already holds without it. That also resolves the design tension Nikita Aboltin (@2mind) raised in the comment above (don't make OpenBao mandatory; lean on ESO Generators) — see "Backend" below.
Current state — the inconsistency is real, and it's the actual problem
Today there is no shared contract. Each package reinvents credential handling, and the differences are user-visible. Audit of
packages/apps/*andpackages/system/*:Auto-generation: consistent mechanism, inconsistent everything else
All generation uses the same
lookup "v1" "Secret"+randAlphaNumidiom (so upgrades don't rotate — good), but:<release>-credentials(postgres, mariadb, clickhouse, nats) vs per-user<release>-user-<name>(mongodb, opensearch) vs<release>-auth(redis) vs multi-secret (harbor)users.<name>.password; redis exposes onlyauthEnabled: bool— no user-supplied password, no users at allsecrets.includeselectors in*-rd/cozyrds/*.yaml— also inconsistent (mongodb per-user secrets aren't selected at all, #2678 defect 2)The override gap is the sharp edge
The only way to set a specific password today is inline:
There is no GitOps-safe override — no
passwordFromSecret: {name, key}that points at a Secret the user pre-created (or that ESO/OpenBao populated). That single missing field is what blocks property #2 above, independent of whether OpenBao exists.Concrete symptoms already filed against this design
These are all instances of "no shared credential contract", and should be tracked as children of this issue:
lookuprace + per-user Secret not selected for the dashboard)kubectl exec(blocks OpenBao-as-default; the auto-unseal "Open Question 2" below)labelSelector(surfacing layer is buggy)Proposed: a shared credential contract first, OpenBao backend second
Layer 1 — the contract (ships without OpenBao)
Define one helper in
cozy-libthat every stateful chart uses, exposing a uniform values block:Resolution order per credential:
passwordFromSecret→ inlinepassword→ existing generated Secret (lookup) → generate. Redis joins the same contract (gainsusers/ override instead of bareauthEnabled). Standardize the output Secret shape and the ApplicationDefinitionsecrets.includeselector so the dashboard surfacing is uniform (fixes the #2678 class).This alone delivers both user goals and is independently shippable + testable.
Layer 2 — optional OpenBao + ESO backend
With the contract in place,
passwordFromSecretcan be satisfied by a Secret that ESO materializes from OpenBao, and generation can move from HelmrandAlphaNumto an ESOPasswordGenerator +ExternalSecret(per Nikita Aboltin (@2mind)'s proposal) rather than the controller writing to OpenBao directly. That keeps OpenBao optional and the operator thin:The packages already exist and are wired into the platform bundle:
packages/system/openbao+packages/apps/openbao(registered:core/platform/sources/openbao-application.yaml,paas.yamlbundle)packages/system/external-secrets-operator(registered as optional insystem.yamlbundle)So Layer 2 is "wire + configure", not "introduce".
Why this actually removes the
lookupproblem (and the one caveat)It's worth being precise, because the existing-value check doesn't disappear — it relocates. The problem with
lookupwas never "checking existing secrets"; it's that Helm does the check at template-render time, and render must be a pure, deterministic function for GitOps to work.Today the chart reads live state and emits the secret value:
Three failures follow, all from reading cluster state during render:
helm template/ ArgoCD dry-run returnsnilforlookup→ renders a freshly generated password that differs from what's deployed → perpetual diff, or ArgoCD overwrites the real one. The core GitOps incompatibility.helm templateproduces a throwaway value.nilon first install → empty secret. That is exactly the bug(mongodb): dashboard shows no usable MongoDB credentials on fresh install #2678 mongodb bug.With ESO the chart emits a static reference, not a value:
This manifest is byte-identical on every render — no
lookup, norandAlphaNum, nothing environment-dependent — sohelm templateis deterministic, ArgoCD sees no drift, unit tests pass. The "preserve the existing value" job moves out of render and into a reconcile loop, where reading live state is exactly what a controller is supposed to do:refreshInterval: "0"later reconciles see it exists and leave it.lookup(today)helm template/ ArgoCDnil→ drift)Caveat —
refreshInterval: "0"is load-bearing. With a non-zero interval, ESO re-runs the Password generator on each refresh and overwrites the live password — worse thanlookup. The contract must pin0for generated credentials, and that should be enforced in thecozy-libhelper, not left to each chart author.This only cleanly solves the secret-generation lookups. Non-secret
lookups (tenant UID, VM/MachineTemplate spec preservation) stay — out of scope here.Refreshed plan
cozy-libcredential helper (resolution order above); addpasswordFromSecret; standardize length + Secret shape + AppDefsecrets.include. Migrate redis to the contract. Land symptom fixes bug(mongodb): dashboard shows no usable MongoDB credentials on fresh install #2678.randAlphaNumfor ESOPasswordGenerator +ExternalSecretbehind a platform flag (thecozy-libhelper pinsrefreshInterval: "0"— see caveat above); keeplookupfallback when ESO is absent (GitOps/ArgoCD compatibility — the original P1 win).PushSecretfor audit/rotation.lookups, migration guide, docs. (Non-secretlookupuses — tenant uid, VM/kubernetes spec preservation — stay out of scope; this issue is credentials only.)Open questions (refined)
PushSecretforward?cc Aleksei Sviridkin (@lexfrei) Nikita Aboltin (@2mind) — proposing we keep this as the canonical tracking issue and label it
lifecycle/frozenso it stops aging out.