Kubernetes operator
The PostgresPolicy resource
What goes in a PostgresPolicy, field by field.
Custom resource
A PostgresPolicy declares the roles, memberships, schemas, grants, and default privileges pgroles should converge the database to, plus Kubernetes-specific fields for connection and scheduling. Those policy sections are the same ones the CLI manifest uses, so the manifest guide and manifest reference describe them field by field — under spec: rather than at the top level.
The example below uses a connection URL Secret; see Database connection for structured parameter support (Zalando, CloudNativePG, PGO).
The custom resources use short names in kubectl: pgr for PostgresPolicy, pgplan for PostgresPolicyPlan, pgeap for EphemeralAccessPolicy, and pgear for EphemeralAccessRequest.
apiVersion: pgroles.io/v1alpha1
kind: PostgresPolicy
metadata:
name: myapp-roles
namespace: default
spec:
connection:
secretRef:
name: mydb-credentials
secretKey: DATABASE_URL # optional, defaults to DATABASE_URL
interval: "5m" # reconciliation interval (supports 5m, 1h, 30s, 1h30m)
suspend: false # set true to pause reconciliation
mode: apply # apply changes, or use observe for non-mutating drift preview
approval: auto # auto applies immediately; manual gates behind a reviewed plan
reconciliation_mode: authoritative # authoritative | additive | adopt
default_owner: app_owner
profiles:
editor:
login: false
inherit: false
grants:
- privileges: [USAGE]
object: { type: schema }
- privileges: [SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER]
object: { type: table, name: "*" }
- privileges: [USAGE, SELECT, UPDATE]
object: { type: sequence, name: "*" }
- privileges: [EXECUTE]
object: { type: function, name: "*" }
default_privileges:
- privileges: [SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER]
on_type: table
- privileges: [USAGE, SELECT, UPDATE]
on_type: sequence
- privileges: [EXECUTE]
on_type: function
schemas:
- name: inventory
owner: app_owner
profiles: [editor]
roles:
- name: app-service
login: true
comment: "Application service account"
config:
search_path: inventory
statement_timeout: "30s" # config values are always strings
grants:
- role: app-service
privileges: [CONNECT]
object: { type: database, name: mydb }
memberships:
- role: inventory-editor
members:
- name: app-service
retirements:
- role: legacy_app
reassign_owned_to: app_owner
drop_owned: true
Declared schemas can be created and have ownership converged by the operator. Schemas that are only referenced from top-level grants or default privileges must already exist in the database. Profile-generated roles also inherit the profile-level login and inherit attributes shown above.
Role passwords
Roles can either sync a password from an existing Kubernetes Secret or ask the operator to generate and manage a per-role Secret. In both cases the password value is resolved at reconcile time and only sent to PostgreSQL inside the apply transaction.
Sync from an existing Secret
spec:
roles:
- name: app-service
login: true
password:
secretRef:
name: app-passwords
secretKey: app-service # optional, defaults to the role name
password_valid_until: "2026-12-31T00:00:00Z"
password.secretRef.name— the Secret containing the password value.password.secretKey— the key within the Secret. Defaults to the role name if omitted.password_valid_until— ISO 8601 timestamp for PostgreSQLVALID UNTIL.
Generate and manage a Secret
spec:
roles:
- name: app-service
login: true
password:
generate:
length: 48 # optional, must be 16-128
secretName: app-service-password # optional
secretKey: password # optional, defaults to password
password.generate.length— generated password length. Defaults to32. Must be between16and128.password.generate.secretName— override the generated Secret name. If omitted, the operator derives a Kubernetes-safe name from{policy}-pgr-{role}.password.generate.secretKey— key written into the generated Secret. Defaults topassword.
Generated Secrets are created in the same namespace as the PostgresPolicy, owned by that policy, and include both the cleartext password and a SCRAM verifier. The cleartext password is written at password.generate.secretKey (default password) and the SCRAM verifier is written at the fixed key verifier. password.generate.secretKey must not be verifier; the CRD rejects that value. Deleting the generated Secret causes the operator to recreate it and rotate the PostgreSQL password on the next reconcile, reporting a GeneratedSecretMissing Warning Event as it does. The Secret is created only while an approved plan executes, so under approval: manual it appears when the plan applies, not when it is proposed.
Validation and reconcile semantics
- Passwords are only allowed on roles with
login: true. - Exactly one of
password.secretReforpassword.generatemust be set. - Password values are redacted in operator logs and in the SQL preview stored on
PostgresPolicyPlan. - If the referenced Secret or key is missing, the operator sets
Ready=FalseandDegraded=Truewith reasonSecretMissingorSecretFetchFailed, and retries on the normal interval. - Password updates are driven by password-source Secret changes. After a successful
apply, unchanged password sources do not create permanent drift in laterobservereconciles. - pgroles cannot detect direct password changes made in PostgreSQL outside the operator, because PostgreSQL does not expose comparable password state safely.
The controller also emits Kubernetes Events for notable state transitions. These are intended for kubectl describe and quick operational debugging, not as a durable audit trail or alerting mechanism.
Role configuration defaults
Roles can declare session defaults that PostgreSQL applies at login, managed via ALTER ROLE ... SET:
spec:
roles:
- name: combined
- name: blue
login: true
config:
role: combined
- name: green
login: true
config:
role: combined
memberships:
- role: combined
members:
- name: blue
- name: green
Config values are always strings in the CRD schema — quote numbers and booleans (statement_timeout: "30000", jit: "off"); the API server rejects unquoted scalars at admission time. The role: <group> setting shown above implements blue/green credential rotation: both login roles switch to a shared owner role at connect, so objects created under either credential survive rotation. See role configuration defaults in the manifest reference for semantics, reconciliation-mode behavior, and validation rules.
Note that this is distinct from connection.params.setRole, which controls the role the operator's own connection switches to; roles[].config.role controls what managed application roles switch to when they log in.