Skip to main content
Version: Devel

PostgreSQL Role management

CloudNativePG provides full lifecycle management for PostgreSQL database roles. This page is about doing it with the DatabaseRole custom resource, which is the recommended way: it is a dedicated, Kubernetes-native resource that decouples the role lifecycle from the cluster infrastructure, which suits GitOps workflows, and it manages more of the role than the alternative can.

Only a DatabaseRole can:

  • generate the password of the role and keep it in a Secret it owns, shaped by criteria you choose and rotated on a schedule;
  • issue a TLS client certificate for the role, signed by the cluster's client CA, enabling certificate authentication instead of passwords;
  • say how the password is managed through a single password.mode field, instead of a combination of passwordSecret and disablePassword;
  • express role removal through a reclaim policy.

Roles can also be declared inline, in the managed stanza of the Cluster spec. That method is not deprecated and keeps working, but it is no longer where the feature grows: new capabilities land on DatabaseRole, and it may be deprecated one day. See Inline managed roles for what it covers, and Migrating from inline managed roles to a DatabaseRole for moving across.

important

The two methods can be used at the same time, on different roles, which is what makes a gradual migration possible. When the same role name is defined in both places, the Cluster specification (managed.roles) always takes precedence: the DatabaseRole is not reconciled, and reports the conflict in its status.

note

A DatabaseRole is applied when its specification or its password Secret changes. Changes made directly in the database, such as a manual ALTER ROLE, are not detected or reverted until the next time the resource is applied. Inline managed roles, by contrast, are periodically compared with the database catalog and brought back to their specification.

See Security for the RBAC implications of granting access to DatabaseRole resources.

A DatabaseRole is namespace-scoped: the resource, the Cluster it references through spec.cluster, and any Secret it reads a password from must all live in the same namespace.


Roles you can manage​

Declarative role management acts only on the roles you name. A role that exists in the database but appears in neither the Cluster specification nor a DatabaseRole is left alone, and its lifecycle continues to be managed within PostgreSQL. You can adopt this feature one role at a time, at your convenience.

Some names are reserved, and both methods refuse them:

Reserved nameBelongs to
postgresThe superuser, managed by the operator
streaming_replicaThe operator, for streaming replication
pg_*PostgreSQL
cnpg_*The operator, including cnpg_pooler_pgbouncer when a PgBouncer Pooler is used

A DatabaseRole naming one of these is rejected by the API server as soon as you apply it, whether or not the operator's webhooks are enabled. The same name in managed.roles is rejected by the Cluster webhook.

Reserved roles are created when the cluster is initialized, as described in Bootstrap, along with the owner of the application database (app, unless your bootstrap configuration names it otherwise). That owner is provided by CloudNativePG but is not reserved, so a DatabaseRole can manage it. Leave the password stanza out when it does: the operator keeps that password aligned with the <cluster-name>-app Secret, and a DatabaseRole carrying no password stanza manages the other attributes of the role while leaving its password alone.

General role configuration notes​

Regardless of the management method used, the role specification adheres to the PostgreSQL structure and naming conventions.

tip

Please refer to the API reference for the full list of attributes.

A few points are worth noting:

  1. The ensure attribute is not part of PostgreSQL. It enables declarative role management to create (present, the default) or remove (absent) a role, and is available only in the inline managed.roles stanza. A DatabaseRole does not support ensure; it expresses role removal through its reclaim policy instead.
  2. The inherit attribute is true by default, following PostgreSQL conventions.
  3. The connectionLimit attribute defaults to -1, in line with PostgreSQL conventions.
  4. Role membership with inRoles defaults to no memberships.

Example manifest​

apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: cluster-example-dante
spec:
cluster:
name: cluster-example
name: dante
comment: "Dante Alighieri"
login: true
password:
mode: generate
duration: 1008h # 6 weeks
renewBefore: 96h # 4 days

An example manifest for a role definition can be found in the file role-examples.yaml.

A role authenticates to PostgreSQL with a password, with a TLS client certificate, or with both: password authentication through the password stanza, and client certificate authentication through the clientCertificate stanza. The two are not exclusive: a role can carry a generated password and a generated client certificate, with pg_hba.conf deciding which one a given connection has to present.

Password authentication​

The password stanza states how the operator manages the password of the role, as the example manifest above does.

password.mode is required whenever the stanza is present: it has no default, so password: {} is rejected. Asking for a password without saying how it is managed is ambiguous, and neither answer is a safe guess: one generates a credential, another removes one.

modeThe operator…Details
generategenerates the password, keeps it in a Secret it owns, and rotates it if askedmode: generate
secretreads the password from an existing Secret you supply, and never writes to itmode: secret
externalstops managing the password, leaving it to something elsemode: external
setNullsets the password to NULL, disabling password authenticationmode: setNull

The remaining fields of the stanza are restricted to the modes that use them:

FieldAllowed under
secretgenerate (optional), secret (required)
criteria, duration, renewBeforegenerate only
important

The stanza is mutually exclusive with the deprecated passwordSecret and disablePassword fields, and it is only available on DatabaseRole resources, not on inline managed roles.

warning

Once present, the stanza cannot be removed: mode is what says how the password is managed from now on, so a role that stops generating one has to state what happens to it instead. See Changing the mode.

mode: generate​

The operator generates the password of the role, stores it in a Secret it owns, and applies it to PostgreSQL:

password:
mode: generate

On its own, as above, the password has no lifetime: it is generated once and never expires or gets replaced. Adding a duration is recommended, and is what makes the operator rotate it. See Rotation.

Generated password Secret​

The operator creates a Secret of type kubernetes.io/basic-auth, named <databaserole-name>-password unless password.secret says otherwise. The first two keys are the ones the instance manager applies to PostgreSQL; the third is there for whoever consumes the credential:

KeyContents
usernamethe name of the PostgreSQL role, spec.name
passwordthe generated password
pgpassa ready-made .pgpass line for the role

The pgpass line wildcards the host and the database, since the credential belongs to the role and says nothing about which endpoint of the cluster, or which database, it is used against:

*:5432:*:dante:<the generated password>

It can be mounted or copied straight into a ~/.pgpass file, so a client authenticates without the password appearing in a connection string or in the shell history:

kubectl get secret cluster-example-dante-password -o jsonpath='{.data.pgpass}' \
| base64 -d >> ~/.pgpass && chmod 0600 ~/.pgpass

The line is appended, since ~/.pgpass usually holds the credentials of more than one role: libpq reads the file top to bottom and uses the first line whose host, port, database and user all match.

The Secret does not need the cnpg.io/reload label: the operator owns it, and reacts to any change to it.

A Secret with that name that the operator does not own is never overwritten, nor deleted: the conflict is reported in status.password.message and no password is generated until it is resolved.

Criteria​

The shape of the generated password follows the criteria block, modeled on the external-secrets password generator:

FieldMeaningDefault
lengthlength of the password24
digitshow many characters are digitsa quarter of length, never more than 10
symbolshow many characters are symbols0
symbolCharactersthe symbols to draw fromthe set below
noUpperexclude uppercase charactersfalse
allowRepeatlet a character appear more than oncefalse

The symbols the generator draws from, unless symbolCharacters narrows them:

~!@#$%^&*()_+`-={}|[]\:"<>?,./

Symbols are excluded by default: a password ends up in connection strings, URIs and .pgpass files, where several of those characters need escaping.

symbolCharacters accepts ASCII punctuation only: a letter or a digit there would collide with the rest of the password when characters cannot be repeated, and whitespace would be trimmed away before the password reaches PostgreSQL.

Since characters are not repeated unless allowRepeat is set, a password cannot contain more letters than the 52 available (26 with noUpper), more than ten digits, or more symbols than symbolCharacters has: criteria asking for more than that are rejected when the role is created. A password of 64 characters, for instance, needs allowRepeat. When criteria the API cannot check turn out to be unsatisfiable anyway, the operator explains it in status.password.message and waits for the specification to be corrected, rather than retrying.

Rotation​

Without duration, a generated password is created once and never changes: it has no expiration, and the operator never replaces it. Setting duration gives it a lifetime, and is recommended: the operator then generates a new password renewBefore ahead of the expiration, the instance manager applies it to PostgreSQL, and the role stops accepting the previous one. A credential that is never replaced is one that stays valid for as long as the role exists, however widely it has been copied in the meantime.

password:
mode: generate
duration: 1008h # 6 weeks
renewBefore: 96h # 4 days

renewBefore defaults to the operator's EXPIRING_CHECK_THRESHOLD setting (7 days) capped at half of duration, so that a short-lived password is not due for renewal as soon as it is generated. This is the same setting that governs certificate expiration checks, so changing it shifts password rotation timing too. For the same reason duration must be at least one minute, and an explicit renewBefore at most half of it.

status.password records when the current password was issued and when it expires, and the expiration is also a column of kubectl get databaserole:

status:
password:
secretName: cluster-example-dante-password
issuedAt: "2026-08-16T09:12:44Z"
expiration: "2026-11-16T09:12:44Z"

Only issuedAt is load-bearing: expiration is recomputed from it and the role's current duration/renewBefore on every reconciliation, so that shortening or lengthening either takes effect immediately instead of being measured against a deadline computed under the previous settings.

A lifetime is also enforced by PostgreSQL, not just honored by the operator: the role's VALID UNTIL follows the expiration of the generated password. See Password expiry, VALID UNTIL for what that means when a rotation does not happen on time.

warning

PostgreSQL stores a single password per role, so a rotation invalidates the previous one as soon as it is applied: any consumer still holding it fails to authenticate until it reads the Secret again. Enable rotation only for consumers that pick the new password up, and expect a short window during which the Secret already carries a password that the database has not applied yet.

Adding duration to a role whose password was generated earlier counts the lifetime the password already had, from the creation of its Secret: one older than the requested duration is rotated at once. Removing duration stops rotation and clears the recorded deadline, keeping the current password.

The issue time of a new password is recorded in the status right after the password is written to its Secret. When recording it fails, for instance because the API server cannot be reached or the operator restarts in between, the operator has no way to tell that the password in the Secret is new, and measures its lifetime from the previous issue time: it expires, and is rotated, earlier than planned, possibly a few seconds later. Consumers that already read the first new password then have to read the Secret again. A failure of this kind never skips or postpones a rotation.

Manual rotation​

To rotate a generated password immediately, regardless of its renewal deadline or of whether duration is set at all, annotate the DatabaseRole with cnpg.io/rotatePassword (any value):

kubectl annotate databaserole cluster-example-dante cnpg.io/rotatePassword=true

The annotation is a one-shot request: the operator removes it as soon as the rotation it asked for has happened, rather than leaving it in place as a standing setting. A request made while password generation is off is removed without effect, since there is nothing to rotate; a request made while generation is only temporarily blocked (for instance by a replica cluster) is left in place and honored once the block clears.

The annotation therefore disappears whether the request was honored or discarded: what tells the two apart is the event the operator records on the role, PasswordRotated or PasswordRotationIgnored (see Status of DatabaseRole resources).

On a role with a duration, the rotation restarts its lifetime: the expiration is recomputed from the moment the new password is issued, and with it the VALID UNTIL of the role (see Generated passwords with a lifetime).

Replica clusters​

On a replica cluster the role, and therefore its password, is owned by the primary cluster and replicated from it. The operator does not generate a password there, and says so in status.password.message: generation, and rotation, start once the cluster is promoted.

mode: secret​

The operator reads the password of the role from an existing Secret that you create and maintain, named by password.secret:

password:
mode: secret
secret: cluster-example-dante

secret is required in this mode. The Secret must follow the basic-auth format common to both management methods, including the cnpg.io/reload label for changes to be picked up promptly.

The operator neither owns nor writes to that Secret: nothing is generated, and nothing is rotated. The password it holds is applied to the role as it is.

This is the password-stanza equivalent of the deprecated passwordSecret field, and the mode to use when the password is produced by something else: an external secret manager, a CI pipeline, or a human.

For a worked example, see managing the password of a DatabaseRole with the External Secrets Operator, which generates and rotates the password while CloudNativePG applies it.

mode: external​

The operator stops managing the password of the role, leaving whatever is set in PostgreSQL to whatever produced it:

password:
mode: external

Use this when the password is managed outside CloudNativePG entirely, whether set by hand or by a tool that talks to PostgreSQL directly, and you want the operator to leave it alone while still managing the rest of the role.

important

A password the operator had generated is the one exception: its Secret is deleted when generation stops, so nothing could read that credential any more, and the operator sets it to NULL once before leaving the role alone. A password it never generated is never touched.

mode: setNull​

The operator sets the password of the role to NULL in PostgreSQL, disabling password authentication for it:

password:
mode: setNull

This is different from simply not managing a password: NULL is applied explicitly, so the role cannot authenticate with a password at all. Use it for roles that authenticate by client certificate or another method, or for a role whose password you want positively removed.

This is the password-stanza equivalent of the deprecated disablePassword field.

Changing the mode​

Switching mode is how a role stops doing one thing and starts doing another; what happens to a Secret the operator generated depends on where it is going:

ChangeResult
generate → externalThe generated Secret is deleted, and the password it held is set to NULL in PostgreSQL: nothing could read that password any more, so it is not left behind as a credential nobody knows. The operator then stops managing the password
generate → setNullThe generated Secret is deleted, and the role's password is set to NULL in PostgreSQL
generate → secretThe generated Secret is deleted, that one included if password.secret names it, and the password is read from the Secret that name refers to from now on
password.secret pointed at another name, still generateThe password is generated again into the new Secret, and the previous one is deleted
DatabaseRole deletedThe generated Secret, if any, is garbage-collected via owner reference, regardless of databaseRoleReclaimPolicy

The operator recognizes the Secret as its own through status.password.secretName, which is the name it last generated a password into: that is how a Secret whose name has disappeared from the specification is still cleaned up, rather than left behind holding a credential nobody maintains.

warning

A Secret the operator generated is deleted as soon as it stops generating into it, and password.secret naming that very Secret does not take the password over: what the operator created, the operator deletes. Copy the password into a Secret of your own, under a different name, before turning generation off. The same applies to databaseRoleReclaimPolicy: retain, where the role survives the deletion of the DatabaseRole but its generated Secret does not.

Client certificate authentication​

The DatabaseRole resource supports opt-in generation of TLS client certificates, signed by the cluster's client CA and stored in a Kubernetes Secret. This enables PostgreSQL cert authentication as an alternative to passwords: no passwords to rotate manually, and private keys are stored as Kubernetes Secrets and never transmitted outside the cluster.

To enable it, add a clientCertificate block to the spec:

apiVersion: postgresql.cnpg.io/v1
kind: DatabaseRole
metadata:
name: cluster-example-dante
spec:
cluster:
name: cluster-example
name: dante
login: true
clientCertificate:
enabled: true

clientCertificate.enabled defaults to true when the block is present, so clientCertificate: {} is equivalent to enabling it. Set enabled: false to turn issuance off while keeping the block in place.

The optional duration and renewBefore fields override the operator-wide certificate lifetime for this role; see Certificate renewal.

important

login: true is required when clientCertificate issuance is enabled. The operator enforces this via validation and will reject the resource otherwise.

Generated certificate Secret​

The operator creates a Secret named <databaserole-name>-client-cert in the same namespace. It contains two keys:

KeyContents
tls.crtPEM-encoded client certificate, signed by the cluster's client CA
tls.keyPEM-encoded private key

The expiration time of the certificate is visible in status.clientCertificate.expiration:

status:
clientCertificate:
expiration: "2026-07-01T12:00:00Z"

When no certificate could be issued, status.clientCertificate.message explains why. The expiration of a certificate issued earlier is kept, since the Secret still holds it, unless that Secret is not owned by the role.

Configuring pg_hba.conf​

The operator generates the certificate but does not modify pg_hba.conf automatically. You must add a hostssl rule with the cert method to the cluster for the role to be able to authenticate:

spec:
postgresql:
pg_hba:
- hostssl all dante all cert

A working connection string using the generated Secret would look like:

psql "host=<cluster>-rw.<namespace>.svc port=5432 dbname=<db> user=dante \
sslcert=/path/to/tls.crt sslkey=/path/to/tls.key \
sslrootcert=/path/to/ca.crt sslmode=verify-full"

tls.crt and tls.key are the two keys of the role's <databaserole-name>-client-cert Secret. ca.crt is not: sslmode=verify-full makes the client verify the server certificate, so that file is the ca.crt key of the cluster's server CA Secret, <cluster>-ca.

Set the umask before writing the files out: libpq rejects a private key that is group- or world-readable, with an error that reads like a problem with the certificate rather than with its permissions.

umask 077
kubectl get secret role-dante-client-cert \
-o jsonpath='{.data.tls\.crt}' | base64 -d > tls.crt
kubectl get secret role-dante-client-cert \
-o jsonpath='{.data.tls\.key}' | base64 -d > tls.key
kubectl get secret cluster-example-ca \
-o jsonpath='{.data.ca\.crt}' | base64 -d > ca.crt

The same applies to a Secret mounted into a pod. See Client TLS/SSL connections for a complete example.

Certificate renewal​

By default, client certificates inherit the operator's global certificate settings: a 90-day lifetime, renewed automatically once they fall within 7 days of expiry. These defaults are controlled by the operator-wide CERTIFICATE_DURATION and EXPIRING_CHECK_THRESHOLD settings.

Renewal is driven by the reconcile loop: the operator checks whether the certificate is approaching expiry and re-signs it if needed. Reconciles are scheduled at least once per hour when clientCertificate issuance is enabled, so renewal happens well before expiry even without a triggering event. The current expiration is always reflected in status.clientCertificate.expiration, and in the Certificate Expiration column of kubectl get databaserole; each renewal is recorded as a ClientCertificateRenewed event on the role, saying what triggered it.

A role can set its own lifetime and renewal lead time instead:

clientCertificate:
enabled: true
duration: 720h # 30 days
renewBefore: 24h # renew one day before expiry

Both fields take Go duration units, so a day has to be written as hours: 90d is not a valid value, 2160h is. duration must be at least 1 minute, and renewBefore at least 30 seconds and at most half of duration. Setting renewBefore requires duration; leaving it out keeps the operator-wide threshold, capped at half the lifetime.

Changing duration, or the operator-wide CERTIFICATE_DURATION for the roles that do not set their own, re-issues the certificate.

important

Every renewal replaces tls.key as well as tls.crt, and the Secret only ever holds one key pair. Established sessions are unaffected, but a client that reads the files once at startup, as connection pools and most drivers do, keeps presenting the old certificate until it reloads them, and authentication starts failing once that certificate expires. Set duration no shorter than the interval at which your clients reload their credentials.

Turning certificate issuance off​

ScenarioResult
clientCertificate.enabled set to false, or the clientCertificate block removedThe cert Secret is deleted; status.clientCertificate is cleared
DatabaseRole deletedThe cert Secret is garbage-collected via owner reference, regardless of databaseRoleReclaimPolicy
note

databaseRoleReclaimPolicy: retain retains the PostgreSQL role, not the generated Secret. The Secret is only meaningful while the operator is managing the role, so it is always cleaned up on deletion.

Bring-your-own-CA limitation​

If the cluster's client CA Secret does not contain a private key (i.e. you supplied your own CA via spec.certificates.clientCASecret), the operator cannot sign new certificates. It will record the reason in status.clientCertificate.message and stop retrying:

status:
clientCertificate:
message: 'client CA secret "my-ca" has no private key; bring-your-own-CA
clusters require manual certificate management'

In this case, you must issue and renew client certificates manually.

note

CNPG does not manage Certificate Revocation Lists (CRLs). If a certificate must be invalidated before it expires, rotate the cluster's client CA: on the next reconcile the operator detects that the existing certificates are no longer signed by the current CA and re-issues all managed client certificates. Alternatively, delete the certificate's Secret to have the operator issue a fresh one signed by the current CA.

Role reclaim policy​

The databaseRoleReclaimPolicy field defines the "final act" of the operator when a DatabaseRole Custom Resource is removed from the Kubernetes API. This mirrors the behavior of Kubernetes Persistent Volumes.

  • retain (default): The role is left in the database. This is the safest setting for production, ensuring that even if a manifest is accidentally deleted, the database user (and any objects they own) remains untouched.
  • delete: The operator attempts to execute a DROP ROLE in PostgreSQL before the Kubernetes object is finalized. This is ideal for ephemeral or automated environments.
note

If a role owns objects (tables, schemas, etc.), DROP ROLE fails and the DatabaseRole stays in Terminating, retried periodically until those objects are reassigned or dropped. The operator never drops owned objects on your behalf: reassign or drop them in PostgreSQL, or switch to retain, to let the deletion complete.

Removing a role​

How you remove a role depends on how it was created:

  • Created through a DatabaseRole: delete the resource. Whether the role is also dropped from PostgreSQL is governed by its reclaim policy.
  • Pre-existing, or managed elsewhere: a DatabaseRole is not the tool to drop it. Declare it absent through the inline managed.roles stanza, or run DROP ROLE directly.
warning

Creating a DatabaseRole for a role that already exists adopts it: the operator alters the existing role so that every attribute matches the manifest, including the attributes you omit, which are forced back to their defaults. In particular, memberships not listed in inRoles are revoked, an omitted connectionLimit is reset to -1 (unlimited), and an omitted validUntil becomes infinity if the role had an expiration date. Review the current attributes and memberships of a role before adopting it, and do not point a DatabaseRole at a role you only want to drop, since it will be modified before it can be removed.

Status of DatabaseRole resources​

The DatabaseRole resource includes a dedicated status section for per-role observability:

status:
applied: true
observedGeneration: 3
conditions:
- lastTransitionTime: "2026-04-04T15:06:23Z"
message: "2051"
reason: ChangeDetected
status: "True"
type: PasswordSecretChange

The PasswordSecretChange condition is maintained by the operator as an internal signal for the instance manager: its message carries the resourceVersion of the password Secret the operator last observed, and a change in that value triggers the re-application of the password. The condition appears once a password Secret is in use, whether generated by the operator, read from one you supply with mode: secret, or named by the deprecated passwordSecret field, and is removed when the role no longer has one.

The operator also records what it did to the password of a role as Kubernetes events, so that a rotation is visible without reading the logs of the operator Pod:

kubectl describe databaserole cluster-example-dante
ReasonTypeWhen
PasswordGeneratedNormala password was generated into the Secret for the first time
PasswordRotatedNormala new password replaced the previous one, whether on its deadline or on request
PasswordRotationIgnoredWarninga rotation was requested for a role whose password the operator does not generate
PasswordGenerationSkippedWarningno password could be generated, with the reason: a Secret the operator does not own, unsatisfiable criteria, a replica cluster
ClientCertificateIssuedNormala client certificate was issued for the first time
ClientCertificateRenewedNormalthe certificate was replaced, with the reason: an approaching expiration, a rotated client CA, a certificate that could not be read
ClientCertificateIssuanceSkippedWarningno certificate could be issued, with the reason: a Secret the operator does not own, a CA without a private key

The two Skipped events are recorded when the reason changes, not on every reconciliation: a cause that lasts, such as a replica cluster, is stated once rather than repeated. The same explanation is always available in status.password.message and status.clientCertificate.message.

If a DatabaseRole targets a name already managed in the Cluster spec, which takes precedence over it, the applied field will be false with the message:

database role is already managed by the CNPG cluster

On a replica cluster the role is owned by the primary cluster, not reconciled locally. In that case the instance manager reports the role as unknown rather than failed: the applied field is left unset (nil) with an explanatory message. The role is reconciled normally once the cluster is promoted to primary.

Deprecated password fields​

A DatabaseRole inherits passwordSecret and disablePassword from the shared role configuration it has in common with inline managed roles. Both still work, and neither is going away in the short term, but on a DatabaseRole they are deprecated in favor of the password stanza, which covers what they do and more:

Deprecated fieldEquivalentWhat you also gain
passwordSecret: {name: foo}password: {mode: secret, secret: foo}the same Secret, read the same way, but stated in one place with the rest of the password configuration
disablePassword: truepassword: {mode: setNull}no separate flag whose interaction with passwordSecret has to be validated
(neither set)password: {mode: external}says explicitly that something else manages the password, instead of leaving it implied
(no equivalent)password: {mode: generate}the operator generates the password, with criteria and rotation

The stanza and the deprecated fields are mutually exclusive: a role uses either the stanza or the older fields, not both. See Migrating from inline managed roles to a DatabaseRole for moving an existing role across.

important

There is no automatic migration. A role keeps using whichever form its manifest specifies, so existing DatabaseRole resources continue to work untouched. Adopting the stanza is an explicit edit, and one worth making deliberately, because the stanza cannot be removed once set.


Password reference​

The sections below apply to roles managed either way, through a DatabaseRole or through the inline stanza. Where behavior differs between the two, it is called out.

Supplying a password in a Secret​

Both methods can take a role's password from a Secret you create: inline through passwordSecret, and on a DatabaseRole through mode: secret (or the deprecated passwordSecret).

The Secret must be of type kubernetes.io/basic-auth. The username (encoded in Base64 as is usual in Kubernetes) should match the role we are setting the password for. For example:

apiVersion: v1
data:
username: ZGFudGU=
password: ZGFudGU=
kind: Secret
metadata:
name: cluster-example-dante
labels:
cnpg.io/reload: "true"
type: kubernetes.io/basic-auth
important

Label the Secret with cnpg.io/reload: "true", as shown above. Password changes in labeled Secrets are applied immediately, while changes in unlabeled Secrets are only applied at a subsequent reconciliation, for example when the operator refreshes its internal cache.

A Secret the operator generates has the same format but does not need the label: the operator owns it and reacts to any change to it.

Password expiry, VALID UNTIL​

The VALID UNTIL role attribute in PostgreSQL controls password expiry. Roles created without VALID UNTIL specified get NULL by default in PostgreSQL, meaning that their password will never expire.

PostgreSQL uses a timestamp type for VALID UNTIL, which includes support for the value 'infinity' indicating that the password never expires. Please see the PostgreSQL documentation for reference.

With declarative role management, the validUntil attribute for managed roles controls password expiry. validUntil can only take:

  • a Kubernetes timestamp, or
  • be omitted (defaulting to null)

In the first case, the given validUntil timestamp will be set in the database as the VALID UNTIL attribute of the role.

In the second case (omitted validUntil) the operator will ensure password never expires, mirroring the behavior of PostgreSQL. Specifically:

  • in case of new role, it will omit the VALID UNTIL clause in the role creation statement
  • in case of existing role, it will set VALID UNTIL to infinity if VALID UNTIL was not set to NULL in the database (this is due to PostgreSQL not allowing VALID UNTIL NULL in the ALTER ROLE SQL statement)

Generated passwords with a lifetime​

A DatabaseRole that has the operator generate its password with a duration owns the expiry of the role: VALID UNTIL follows the expiration of the generated password, so PostgreSQL stops accepting that password at the same moment the operator considers it expired. validUntil cannot be set on such a role, and is rejected at admission: there would otherwise be two competing answers to when the password stops working.

This makes the lifetime a real deadline rather than a convention. The operator rotates the password renewBefore ahead of it, so under normal operation the password is replaced before VALID UNTIL is ever reached.

warning

Because VALID UNTIL is a hard deadline in PostgreSQL, a rotation that does not happen costs the role its access: if generation is blocked for longer than renewBefore (an unsatisfiable criteria, a Secret the operator does not own, a demotion to a replica cluster, or an operator that is down), the password expires and the role can no longer authenticate with it. The Password Expiration column of kubectl get databaserole is the deadline to watch, and a PasswordGenerationSkipped event on the role says what is in the way. Pick a renewBefore that leaves room to notice a stalled rotation and fix it before that deadline.

Pre-hashed passwords​

You can also provide pre-encrypted passwords by specifying the password in MD5/SCRAM-SHA-256 hash format:

kind: Secret
type: kubernetes.io/basic-auth
metadata:
name: cluster-example-cavalcanti
labels:
cnpg.io/reload: "true"
apiVersion: v1
stringData:
username: cavalcanti
password: SCRAM-SHA-256$<iteration count>:<salt>$<StoredKey>:<ServerKey>
warning

The example above uses stringData:, where Kubernetes encodes the value for you, which is the safest path for pre-hashed passwords. If you must use data:, encode the bytes exactly with printf '%s' "$hash" | base64 (or echo -n "$hash" | base64). A trailing newline from a naive echo "$hash" | base64 makes the value miss the SCRAM/MD5 shadow-format check, so the operator falls back to treating it as cleartext and re-hashes it, and login stops working.

Pre-hashed passwords apply to a Secret you supply. A password the operator generates is always cleartext in its Secret, and encoded on the way to PostgreSQL as described below.

Safety when transmitting cleartext passwords​

Role passwords are safely managed in Kubernetes using Secrets, but the SQL path between the operator and PostgreSQL is also a concern. As noted in the PostgreSQL documentation:

The password will be transmitted to the server in cleartext, and it might also be logged in the client's command history or the server log

CloudNativePG protects this path in two complementary ways:

  1. Before emitting CREATE/ALTER ROLE ... PASSWORD '...', the operator SCRAM-SHA-256 encodes any cleartext password operator-side (client-side from PostgreSQL's point of view). This is the standard PostgreSQL practice for keeping cleartext out of server logs and extensions like pg_stat_statements or pgaudit, and is the same encoding that psql \password and libpq's PQencryptPasswordConn perform. The literal PostgreSQL receives is the SCRAM-SHA-256 verifier stored in pg_authid.rolpassword. Passwords already provided in MD5 or SCRAM-SHA-256 shadow form are forwarded unchanged.
  2. The same CREATE/ALTER ROLE statements are executed inside a transaction that temporarily suppresses both statement logging (log_statement) and error statement logging (log_min_error_statement), preventing leakage to the PostgreSQL log in both success and failure scenarios.

The Status section of the cluster does not print the query statement for any managed role operation.

Opting out of operator-side encoding​

If you need PostgreSQL (not the operator) to decide how the password is hashed (for example, on a cluster running password_encryption = md5), set the annotation cnpg.io/passwordPassthrough: "enabled" on the basic-auth Secret. The operator will then forward the password value verbatim.

warning

The cnpg.io/passwordPassthrough annotation must be set on the basic-auth Secret itself, not on the Cluster resource. Placing it on the Cluster has no effect, and the operator will continue to apply SCRAM-SHA-256 encoding to the password before sending it to PostgreSQL.

The opt-in is per-Secret and applies to every basic-auth Secret the operator consumes (managed-role secrets, but also the superuser and application-user secrets), so a single cluster can mix passthrough secrets and operator-encoded secrets freely. The statement-logging suppression layer described above still applies in both modes.

warning

With cnpg.io/passwordPassthrough: "enabled", the operator forwards the Secret's password value verbatim. If that value is cleartext (the common case on a password_encryption = md5 cluster), extensions such as pg_stat_statements or pgaudit will observe it. This is the expected trade-off for letting PostgreSQL choose the hash format.


Inline managed roles​

With the managed stanza in the cluster spec, CloudNativePG provides management for roles specified in .spec.managed.roles. This feature enables declarative management of existing roles, as well as the creation of new roles if they are not already present.

Inline managed roles cover the role attributes and their passwords. Password generation, rotation and client certificate issuance are only available with a DatabaseRole, which is the recommended method and the one this page otherwise describes.

Example cluster manifest​

An example manifest for a cluster with declarative role management can be found in the file cluster-example-with-roles.yaml.

Here is an excerpt from that file:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
spec:
managed:
roles:
- name: dante
ensure: present
comment: Dante Alighieri
login: true
superuser: false
inRoles:
- pg_monitor
- pg_signal_backend

Passwords in inline managed roles​

An inline managed role takes its password from a Secret you supply, through passwordSecret:

passwordSecret:
name: cluster-example-dante

The Secret follows the basic-auth format described above.

If no passwordSecret is specified, the instance manager will not try to CREATE/ALTER the role with a password, keeping with PostgreSQL conventions.

important

New roles created without passwordSecret will have a NULL password inside PostgreSQL.

To set a password to NULL explicitly, as opposed to simply not managing one, use disablePassword:

disablePassword: true
note

It is an error to set both passwordSecret and disablePassword on a given role.

These two fields are the only password controls available inline. On a DatabaseRole they are deprecated in favor of the richer password stanza.

Status of inline managed roles​

When using the inline method, the Cluster status includes a comprehensive summary:

status:
managedRolesStatus:
byStatus:
reconciled:
- dante
reserved:
- postgres
- streaming_replica
cannotReconcile:
petrarca:
- 'could not perform UPDATE_MEMBERSHIPS on role petrarca: role "poets" does not exist'

Note the special sub-section cannotReconcile for operations the database (and CloudNativePG) cannot honor, and which require human intervention.

This section covers roles reserved for operator use and those that are not under declarative management, providing a comprehensive view of the roles in the database instances.

The kubectl plugin also shows the status of managed roles in its status sub-command:

Managed roles status
Status Roles
------ -----
pending-reconciliation petrarca
reconciled app,dante
reserved postgres,streaming_replica

Irreconcilable roles
Role Errors
---- ------
petrarca could not perform UPDATE_MEMBERSHIPS on role petrarca: role "poets" does not exist

Migrating from inline managed roles to a DatabaseRole​

You can move a role from the inline managed.roles stanza to a standalone DatabaseRole without disruption:

  1. Create the DatabaseRole with the desired specification. Both methods share the same RoleConfiguration structure, so the stanza can be copied across as-is.
  2. Remove the matching entry from .spec.managed.roles in the Cluster manifest.
  3. The operator detects the change and hands management over to the DatabaseRole.

Because the Cluster spec takes precedence while both exist, the handover happens only once the inline entry is gone, so there is no window in which the role is left unmanaged.

When converting a role that the inline stanza removed with ensure: absent, note that a DatabaseRole does not support ensure: absent. Express removal through the reclaim policy instead: delete the resource with databaseRoleReclaimPolicy: delete to drop the role, or keep the default retain to leave it in place. See Removing a role for the full behavior.

Moving the password across​

Copying the inline stanza as-is carries passwordSecret and disablePassword with it, and that keeps working. On a DatabaseRole those fields are deprecated though, so the migration is a good moment to state the password through the password stanza instead:

InlineOn the DatabaseRole
passwordSecret: {name: foo}password: {mode: secret, secret: foo}
disablePassword: truepassword: {mode: setNull}
neither setpassword: {mode: external}, which says outright that the password is managed elsewhere

Moving to the stanza also opens up what inline roles cannot do at all: having the operator generate the password, shape it with criteria, and rotate it on a schedule. Switching an existing role to mode: generate replaces the password it currently has with a generated one, so plan it like any other credential change: the consumers of that role have to read the new password from the generated Secret.

important

Do the swap in one step: the password stanza is mutually exclusive with passwordSecret and disablePassword, so a manifest that carries both is rejected. Replace the old fields with the stanza in the same edit.

Unrealizable role configurations​

In PostgreSQL, in some cases, commands cannot be honored by the database and will be rejected. Please refer to the PostgreSQL documentation on error codes for details.

Role operations can produce such fundamental errors. Two examples:

  • We ask PostgreSQL to create the role petrarca as a member of the role (group) poets, but poets does not exist.
  • We ask PostgreSQL to drop the role dante, but the role dante is the owner of the database inferno.

These fundamental errors cannot be fixed by the database, nor the CloudNativePG operator, without clarification from the human administrator. The two examples above could be fixed by creating the role poets or dropping the database inferno respectively, but they might have originated due to human error, and in such case, the "fix" proposed might be the wrong thing to do.

CloudNativePG will record when such fundamental errors occur, and will display them in the cluster Status, as described in Status of inline managed roles.