Context
cozystack/cozystack#4078 drops plaintext users[].password from the postgres and mariadb apps: passwords are now always chart-generated into the <release>-credentials Secret and read only from there. That part is sound and stays in #4078. The PR also introduced passwordRotation — an integer counter a tenant bumps to regenerate every managed password — and review showed that piece needs a different foundation, so it is being split out here for its own design discussion before implementation.
Why rotation cannot live in the chart render
While the credentials Secret is rendered by the chart, its cleartext value is part of the Helm release manifest, and helm-controller retains the last MaxHistory revisions of that manifest. A passwordRotation bump performed in response to a leak therefore does not retire the leaked password: it stays in up to several stored release revisions until unrelated upgrades push it out of history. Rotation whose purpose is to revoke, but which cannot revoke, is worse than no rotation — the credential the tenant used to control in one scrubbable place now lives in several the tenant cannot see. This is Helm behaviour, not something a template can work around.
Three further problems share the same root — there is no durable state and no actor outside the render:
- No completion signal. A tenant writes an integer and nothing observable follows: no status, no condition, no event, no way to answer "did the new password reach the database". If the applying Job/operator step fails, the Secret advertises a credential the database never received and nothing says so.
- Fragile
lookup. Rotation state is a counter compared against a value read back with Helm's lookup, which returns empty during any render without API access; the template cannot distinguish "first install" from "the read did not happen", and both paths regenerate and overwrite the Secret while the database keeps the old password — a silent credential split.
- Two mechanisms, and root outside both. Postgres converges through a post-upgrade hook Job; MariaDB through the operator reacting to a watch-labelled Secret. Different windows, different failure modes, one values field. MariaDB
root is excluded from rotation entirely (the operator only applies rootPasswordSecretKeyRef at datadir bootstrap), so the feature does not cover the most privileged account on that engine.
Proposed direction
Give rotation an owner outside the Helm render: a controller that creates and holds the credentials Secret, with the chart referencing it by name rather than rendering it. The controller applies the rotation and reports the outcome in status. That also dissolves the problems above by construction — the swallowed first bump exists only because there is nowhere to keep the baseline, and the missing completion signal only because there is nothing to carry it.
Requirements for the design:
- The credentials Secret is owned by the controller, not by the chart render, so its value never enters the Helm release manifest or its retained history.
- Rotation reports a completion signal (a status/condition on the app CR) distinguishing "requested", "applied to the database", and "failed".
- A single mechanism covers both engines, or the per-engine difference is explicit and justified.
root (MariaDB) is covered, or its exclusion is a deliberate, documented decision rather than an accident of the delivery path.
- The baseline for an existing release is durable state, so the first bump is not spent establishing it.
Open questions
- Where the controller lives (extend an existing operator vs a small dedicated one) and how it holds the Secret relative to Flux/Helm ownership.
- How rotation interacts with a restore into a copy (the recovered roles carry the source hashes until reconciled — see #4078's restore handling).
- Whether
root rotation on MariaDB is feasible at all given rootPasswordSecretKeyRef is bootstrap-only, or needs an out-of-band reset.
Links
Context
cozystack/cozystack#4078 drops plaintext
users[].passwordfrom thepostgresandmariadbapps: passwords are now always chart-generated into the<release>-credentialsSecret and read only from there. That part is sound and stays in #4078. The PR also introducedpasswordRotation— an integer counter a tenant bumps to regenerate every managed password — and review showed that piece needs a different foundation, so it is being split out here for its own design discussion before implementation.Why rotation cannot live in the chart render
While the credentials Secret is rendered by the chart, its cleartext value is part of the Helm release manifest, and helm-controller retains the last
MaxHistoryrevisions of that manifest. ApasswordRotationbump performed in response to a leak therefore does not retire the leaked password: it stays in up to several stored release revisions until unrelated upgrades push it out of history. Rotation whose purpose is to revoke, but which cannot revoke, is worse than no rotation — the credential the tenant used to control in one scrubbable place now lives in several the tenant cannot see. This is Helm behaviour, not something a template can work around.Three further problems share the same root — there is no durable state and no actor outside the render:
lookup. Rotation state is a counter compared against a value read back with Helm'slookup, which returns empty during any render without API access; the template cannot distinguish "first install" from "the read did not happen", and both paths regenerate and overwrite the Secret while the database keeps the old password — a silent credential split.rootis excluded from rotation entirely (the operator only appliesrootPasswordSecretKeyRefat datadir bootstrap), so the feature does not cover the most privileged account on that engine.Proposed direction
Give rotation an owner outside the Helm render: a controller that creates and holds the credentials Secret, with the chart referencing it by name rather than rendering it. The controller applies the rotation and reports the outcome in status. That also dissolves the problems above by construction — the swallowed first bump exists only because there is nowhere to keep the baseline, and the missing completion signal only because there is nothing to carry it.
Requirements for the design:
root(MariaDB) is covered, or its exclusion is a deliberate, documented decision rather than an accident of the delivery path.Open questions
rootrotation on MariaDB is feasible at all givenrootPasswordSecretKeyRefis bootstrap-only, or needs an out-of-band reset.Links