Dev & EngARTICLE

CloudNativePG 1.31 automates PostgreSQL password generation and rotation with native enforcement

A pull request merged on October 24, 2026 teaches the operator to create, rotate, and expire PostgreSQL role passwords, with the database itself enforcing the deadline via VALID UNTIL.

The CloudNativePG operator is about to solve an annoyance that any DBA running PostgreSQL on Kubernetes knows well: where a role's password comes from and who guarantees it doesn't last forever. On October 24, 2026, a pull request was merged that teaches the DatabaseRole resource to generate its own password, store it in a Secret the operator controls, and rotate it on a schedule. The account comes from Gabriele Bartolini, an engineer at EDB, in the 28th edition of his CNPG Recipes series published on Planet PostgreSQL.

The code closed issue #3788, opened by the community some time ago, and is expected to land in CloudNativePG version 1.31, planned for the last quarter of 2026. There is no release yet: anyone who wants to test it needs to build the development snapshot from the cloudnative-pg/artifacts repository, installable with kubectl apply --server-side -f pointing to the operator-manifest.yaml of the main branch. It's only worth it on a disposable cluster, like kind, because nothing there is a guarantee of stable API.

The gap CloudNativePG 1.30 left open

Version 1.30 introduced DatabaseRole as a declarative way to model PostgreSQL roles, including password-less authentication via TLS certificate. But if the role needed a password, the work of creating the corresponding Secret remained manual: the operator applied the value, but where it came from and how long it stayed valid was the human operator's problem, not CloudNativePG's. That asymmetry is the kind of gap that turns into a production security incident, because a password nobody rotates tends to leak and remain valid indefinitely.

The new password stanza and its four modes

The fix arrives as a password stanza inside DatabaseRole, with a required mode field. It replaces the old combination of passwordSecret and disablePassword, which Bartolini himself describes as hard to read and which is now deprecated (but still works, and cannot be combined with the new stanza). The four modes cover a credential's entire lifecycle:

  • generate: the operator creates the password and keeps the Secret under its ownership.
  • secret: the operator reads the password from a Secret you provide and never writes to it.
  • external: the operator doesn't touch the password, because something external manages it.
  • setNull: the operator zeroes out the password with NULL, turning off password authentication for the role.

There is no default mode, on purpose: asking for a password without saying how it's managed is ambiguous, and a wrong guess can either create or remove a credential.

A DatabaseRole with monthly rotation, in practice

The source's example starts from a minimal single-instance cluster and declares a role that logs in with a password generated and rotated every 30 days:

yaml
kind: DatabaseRole
name: angus
cluster: acdc
login: true
password:
 mode: generate
 duration: 720h
 renewBefore: 24h
databaseRoleReclaimPolicy: delete

duration: 720h gives the password a 30-day lifespan, and renewBefore: 24h asks the operator to replace it one day before expiration, which in practice makes the password change every 29 days. Bartolini sums up the logic behind this cadence in a sentence worth remembering:

That is the cadence I would start with for most workloads: frequent enough that a leaked credential has a bounded lifetime, rare enough that nobody gets paged because of it.

Gabriele Bartolini, engineer at EDB and author of the CNPG Recipes series

What goes inside the Secret, and who applies what

The operator creates a Secret of type kubernetes.io/basic-auth, named <role>-password (in the example, angus-password), with three keys: username, password, and pgpass, the latter already in the format of a ready-to-use .pgpass line, with wildcarded host and database (*:5432:*:angus:<password>). Pasting that line into ~/.pgpass keeps the password from showing up in a connection string or shell history. The Secret has an owner reference to the DatabaseRole: deleting the role deletes the Secret along with it, regardless of the retention policy configured for the role in PostgreSQL.

PostgreSQL enforces the deadline, it's not just the operator's goodwill

In the database, the password is stored as a SCRAM-SHA-256 verifier, and the role gets a VALID UNTIL aligned with the generated password's expiration. This means the deadline isn't a policy that depends on the operator working: PostgreSQL itself refuses the expired password on new connections, even if the operator's rotation silently fails for some reason. For those coming from traditional DBA work, it's the difference between trusting a rotation script and trusting a lock built into the database engine.

Observing and forcing rotation

The password's state lives in the DatabaseRole's status, with fields like issuedAt, expiration, and appliedExpiration. issuedAt is the only field that actually underpins the calculation: at every reconciliation the operator recalculates expiration from it, from duration, and from renewBefore, so changing either parameter in the manifest takes effect immediately. kubectl get databaseroles shows PASSWORD EXPIRATION as a column, and events like PasswordGenerated and PasswordRotated stay in the resource's history via kubectl describe databaserole.

To force an off-schedule rotation, for example after a suspected leak, just annotate the resource:

kubectl annotate databaserole angus cnpg.io/rotatePassword=now

The annotation fires once: the operator removes it as soon as the rotation happens, and a PasswordRotated event (or PasswordRotationIgnored, if the condition doesn't apply) reports the outcome.

Shaping the password with criteria

By default the operator generates 24 characters with digits and uppercase and lowercase letters, with no symbols, deliberately: symbols require escaping in connection strings, URIs, and .pgpass files. When the security policy requires symbols, the criteria block, modeled on the External Secrets Operator's password generator, lets you adjust length, digits, symbols, symbolCharacters, noUpper, and allowRepeat. Impossible combinations, like requesting more symbols than symbolCharacters contains, are rejected right at kubectl apply; combinations only the generator can detect show up in status.password.message, and the operator waits for the fix instead of retrying indefinitely.

Why rotation doesn't come on by default

duration is optional, and without it the password is generated once and never changes. The reason is structural: a PostgreSQL role has exactly one password, with no overlap like what exists in certificate renewal, so as soon as ALTER ROLE runs, the old password stops working for new connections.

In practice the impact is smaller than it seems, because PostgreSQL only evaluates the password at connection time: open sessions keep working, and an application that rereads the Secret on every reconnection simply picks up the new password. The concern is with applications that cache the password for the entire life of the process, which fail on the next reconnection after a failover or a network hiccup.

Behaviors that matter for GitOps

Some transition rules deserve attention from anyone versioning manifests in pull requests:

Mode changeWhat happens
generate to external or setNulldeletes the generated Secret and zeroes the password, leaving no orphan credential
generate to secretalso deletes the generated Secret, even if password.secret points to the same name; you need to copy the password beforehand
changing password.secretgenerates a new password in the new Secret and deletes the previous one
replica clusterno password is generated, because the role belongs to the primary; generation only starts after promotion

The operator never overwrites or deletes a Secret it didn't create: if a Secret with the expected name already exists, the conflict shows up in status.password.message, and nothing is generated until the situation is resolved manually.

What this doesn't replace

Anyone who already centralizes secrets in a vault and distributes them with the External Secrets Operator (ESO) doesn't need to abandon that flow: mode: secret pointing to the Secret produced by ESO remains the right choice, and CloudNativePG's development documentation already includes a working example of that integration. mode: generate serves the opposite case, more common in smaller workloads: when the credential is born, lives, and dies alongside the role, and it's not worth the cost of maintaining another infrastructure component just for that.

What remains open

The feature is merged into trunk, but not in any released version of CloudNativePG, and the author himself is asking for feedback now, before release, via pull request or on the project's CNCF community Slack. It's also worth noting that another related feature is planned for the same 1.31: configurable duration and renewBefore for a DatabaseRole's TLS client certificates, a topic already covered in Bartolini's Recipe 25 and tracked in issue #11312.

Anyone managing PostgreSQL on Kubernetes with a significant volume of fixed-password roles thus gains a declarative path to harden credential policy without depending on an external script or on human discipline to remember to change the password.

Translated from the Brazilian Portuguese original · Read the original

Read also
Dev & Eng

PolarFS Implements Disk-Paxos Lease to Prevent Split-Brain on Shared Disk

PolarFS, the file system behind PolarDB, started writing a Disk-Paxos protocol to the shared block itself to ensure only one node writes at a time. The change, described in a post published on October 24, 2026, closes a flaw that only blocked two processes on the same host, not on different hosts.

Roberto Diniz··1 min