Repository navigation
v0.10.0
·
48 commits
to main
since this release
Immutable
release. Only release title and notes can be modified.
Highlights
Revokes now target each grant's recorded grantor, so delegated grants and foreign-granted memberships finally converge; including for non-superuser executors; PostgresPolicyCandidate brings a propose–review–promote workflow for policy changes; memberships in PostgreSQL predefined (pg_*) and externally managed roles are declaratively manageable; and brownfield adoption is safer with preserve_undeclared_grants, protected owner privileges, and explicit approval for schema ownership transfers.
Upgrade notes
- The approval digest encoding is now
pgroles.io/approval-effect/v3, binding the target database's identity and default-privilege scopes into every approval. On the first reconcile after upgrading, every open plan is superseded and replaced by an equivalent plan under v3; recorded decisions do not carry over, so open plans need one fresh approval. Nothing executes in the meantime. Deliberately, apg_upgrade(freshsystem_identifier) or a blue-green cutover also invalidates any approval open across it — re-approve the fresh plan afterwards. (#180) - Policy content now has explicit size limits. Identifiers are capped at 63 characters and 63 bytes (where PostgreSQL silently truncates), and every list and map has a bound — 1024 roles, 4096 grants, 2048 memberships, and so on (full table in the manifest reference). A policy exceeding a limit is rejected on its next apply with a field-level error; each limit sits at least 20× above the largest policy known to run in production. (#182, #173)
- Bundle plan JSON is now
pgroles.bundle_plan.v2, anddiff --format jsonchanges shape the same way: default-privilege changes carry a taggedscope({"type": "schema", "schema": "app"}or{"type": "global"}) in place of the bareschemastring. Migration: readscope.schemawhere you readschema, and handlescope.type == "global"entries having no schema at all. ThediffJSON output remains unversioned — treat its shape as unstable and pin the pgroles version if you parse it. spec.mode: planis renamed tospec.mode: observe. The old value keeps working with identical behaviour through a deprecation window (aModeValueDeprecatedcondition, an operator-log warning, and thepgroles.deprecated.mode_planmetric); a future release removes it. Update manifests at your convenience.password_valid_untilmust be a UTC whole-second timestamp (YYYY-MM-DDTHH:MM:SSZ). An offset or fractional-second timestamp is now a field-level validation error — it never converged before (see Fixed below).- Declared memberships in
external: trueroles now take effect. Previous releases accepted these declarations but did not include them in plans. Review the next plan before applying it; pgroles may now grant the declared memberships. Undeclared memberships remain untouched unless the membership rule setsexclusive: true. (#200) - Adopt-mode schema ownership transfers require explicit approval. If an adopt plan would change a schema's owner, pass
--allow-schema-owner-transferstopgroles applyor setspec.allow_schema_owner_transfers: trueon an apply-modePostgresPolicy. Without the opt-in, apply stops before making changes and the operator reportsOwnerTransferBlocked; diff and observe mode still show the proposed transfer. (#201) - Database grants now name the connected database explicitly.
object.nameis required fortype: database, and inspection rejects a name that differs fromcurrent_database()instead of planning SQL against an ACL it did not inspect. Operator policies reportInvalidDatabaseTargetfor a mismatch.
Added
- Revokes target each grant's recorded grantor, so delegated grants converge. PostgreSQL's
REVOKEremoves only grants attributed to the revoker (a superuser acts as the object owner, or the bootstrap superuser for role memberships), so a privilege granted onward by a delegate — or a membership edge granted by another admin — survived every plain revoke, superuser executors included, and re-planned forever. Inspection now records the grantor of every ACL entry and membership edge (multiple edges per role/member pair are aggregated for diffing and revoked individually), and rendered revokes act as that grantor:SET ROLE grantor; REVOKE ...;for object privileges (restoring the connection's configured execution role afterwards),REVOKE ... GRANTED BY grantorfor memberships. Delegated grants toPUBLICconverge the same way. The plan preflight verifies per grantor that the executor can become it and reports what is missing (UnsatisfiableRevoke:diffwarns,applyblocks), checking authority phase by phase against the plan's own membership changes so an executable plan is not falsely rejected. Object-ACL grantor targeting works on every supported version; membership targeting uses the per-edge grantor PostgreSQL 16 records, and older servers keep plain membership revokes. Grantor-less plan digests and JSON output are unchanged. See limitations. PostgresPolicyCandidate: propose, review, and promote policy content without touching the live policy. A candidate points at an existingPostgresPolicyand carries only proposed content; execution settings come from the parent. Candidates are immutable once created — the version reviewed is exactly the version approved; revise by filing a successor withreplaces. Each open candidate gets its ownPostgresPolicyPlan, computed with the parent's credentials and locks against post-enforcement state, reviewed and decided like any other plan — and candidate planning never writes: no SQL in any state, no generated-password Secrets. Merging an approved candidate's content makes its reviewed plan the one that executes: the operator adopts the candidate's plan (never minting an approval of its own) and executes only if the recomputed effects still match the approved digest; anything else is reported on the candidate (PromotedWithoutApproval,PromotionDigestMismatch,PromotionNotExecuted) rather than ignored. See the candidate docs for thekubectland CI recipes. (#182, #173)pgroles candidatecovers the review side of the candidate workflow.createfiles a candidate from an ordinary manifest, validated locally through the same path aspgroles validateand created withgenerateNameso concurrent proposals never collide;list,status, anddiffshow every candidate's phase and digest, expand one down to its plan's decision and promotion state, and print the SQL approving it would run. Deciding a plan stayskubectl-shaped on purpose: it is a status write gated by admission, sodecidedByrecords an authenticated identity. (#189)- Approvals are bound to the database they were reviewed against. Every plan records the server's physical identity (
pg_control_system().system_identifier) and a logical fingerprint of the resolved host, port, and database; if either changes between approval and execution, the plan is superseded instead of executed. Setspec.connection.requirePhysicalIdentity: trueto stop reconciliation entirely (TargetIdentityBlocked) when the identifier cannot be read. URL-mode connections bind the endpoint they resolve to, not only the Secret name — editing the URL inside a referenced Secret is no longer invisible to an open approval, while credential rotation still does not invalidate one. (#180, #185) - Manage memberships in PostgreSQL predefined roles and
external: trueroles. Membership rules can grant roles such aspg_monitordirectly, without adding them toroles. By default pgroles manages only the declared members; setexclusive: trueto revoke other ordinary members as well. PostgreSQL's built-inpg_*hierarchy is always preserved.generateexports predefined-role memberships,inspectreports them, and the preflight checks the executor'sADMIN OPTIONand the role's availability on the connected PostgreSQL version. (#200) - Preserve existing grants while adopting a role. Set
preserve_undeclared_grants: trueon a role to add its declared privileges without removing other in-scope grants; explicitensure: absentrules still revoke access. Remove the setting once the role's complete access has been captured in policy. (#201) - Owner-wide default privileges.
default_privilegesentries acceptscope: {type: global}beside theschema:shorthand, emittingALTER DEFAULT PRIVILEGES FOR ROLE ...with noIN SCHEMAclause — the layer that applies to every schema an owner creates objects in. Inspection reads the global layer for exactly the declared(owner, object type)pairs, reporting the effective default, and excludes owner self-entries so authoritative mode never revokes an owner's own default. See default privileges. ensure: absentand a typedPUBLICgrantee. Grant and default-privilege entries acceptensure: absent, which revokes a privilege where it is held, androle: PUBLICaddresses the pseudo-role — closing the gap where PostgreSQL's built-inEXECUTEforPUBLICon every function could never be taken away declaratively. Inspection reports PUBLIC's effective privileges, synthesizingacldefault(...)where the ACL is still NULL. PUBLIC is reconciled only where a rule names it, in every mode: unmentioned PUBLIC privileges are never revoked, and deleting apresentPUBLIC rule does not revoke it — switch the rule toensure: absent.additiveignores absence assertions with a warning; profiles reject them by name. See grants.- Persistent operator plan warnings.
PostgresPolicy.status.plan_warningsrecords warnings such as an undeclareddefault_owneror a pending adopt-mode schema ownership transfer, so they remain visible after reconciliation logs expire. (#201) - Interactive PostgreSQL access guide. The documentation now includes a browser-based course covering roles, ownership, grants, default privileges, drift, offboarding, and security review, with runnable exercises that need no local PostgreSQL. (#197)
Changed
- Plan retention is bounded per phase, so replan churn no longer evicts the record of what ran. The bounds are
Applied25 (never fewer than 30 days' worth, hard ceiling 200, measured bystatus.appliedAt),FailedandRejected10 shared,Superseded3; live phases are never evicted, andpgroles.io/keep=truestill exempts a plan. Each bound is operator-level configuration (PLAN_RETENTION_*environment variables, replacing the unusablemax_plansparameter); an invalid value refuses operator startup. The same bounds govern terminal-candidate pruning, andkeepon a candidate or its plan exempts the pair. (#194) - Planning every open candidate costs one database inspection instead of one each. The operator reads the database once over the union of the candidates' scopes and derives each candidate's own scoped inspection in memory — each candidate's wildcard expansion and diagnostics stay entirely its own,
spec.targetoverrides still inspect for themselves, and a shared-read failure falls back per candidate so a proposal can never break enforcement.pgroles.candidate.inspectionsandpgroles.candidate.planning.durationreport the cost. (#191) - Open candidates are bounded by a budget and a TTL. At most 32 open candidates per policy are planned per pass (oldest first, so a CI loop cannot evict proposals under review); the rest report
CandidateBudgetExceededuntil older ones are decided or expire — nothing is deleted. An open candidate nobody decides within 14 days becomesSuperseded=True, reason=Expired.pgroles.io/keep=trueexempts a candidate from the TTL but not the budget. (#191) - Generated password Secrets are created when the approved plan executes, not when it is proposed, so a rejected or never-approved plan no longer leaves a credential in the cluster. Deleting a generated Secret still rotates the password on the next apply — with a
GeneratedSecretMissingwarning Event first instead of rotating silently. (#181, #174) spec.schemas,spec.roles, andspec.retirementsare now map-lists (keyed by name), so server-side apply merges entries instead of replacing whole lists, and duplicate keys are rejected atkubectl applytime.memberships,grants, anddefault_privilegesstay plain arrays. (#126)
Fixed
- The operator no longer re-applies a non-convergent plan several times a second. A plan whose SQL succeeds without changing inspected state re-applied about once a second under
approval: auto, because each replan minted a fresh plan and every plan status write woke the controller. The reconciler now detects that identical effects were just applied and still diff, reportsReady=Falsewith reasonNonConvergentPlan, and retries at the policy interval. password_valid_untilnow converges. Offsets and fractional seconds could never compare equal to the inspected UTC whole-second rendering, andVALID UNTIL 'infinity'/'-infinity'inspected as an empty string — each re-planned anALTER ROLEon every run.infinitynow inspects as "no expiration",-infinitystays distinguishable so one apply repairs it, and validation accepts exactly the convergent form.- Never revoke an object's privileges from its owner. pgroles previously interpreted PostgreSQL's implicit owner privileges as ordinary grants and could plan revocations against the owner after another ACL was added. Owner access is now protected across every managed object type, including wildcard rules, and generated manifests omit these implicit privileges. (#201)
- The Kyverno plan-decision policy no longer identifies the operator by a hardcoded ServiceAccount name — decision rules exempt callers holding the logical
manageverb on the parentPostgresPolicy, checked with aSubjectAccessReview, so one policy covers any operator install and the standing bypass for anyone able to impersonate the hardcoded account is gone. Every newly terminal decision still records the authenticated identity; underapproval: auto,decidedBynames the operator's ServiceAccount. (#187, #179) - A superseded plan names why it was superseded (effects changed, effects vanished, replaced, or the target moved) instead of always claiming the database changed, and superseding no longer tries to rewrite the decision recorded on the plan — the reviewer's record stays exactly as they left it. (#184, #185)
- Plan lifecycle robustness: a crash between retiring a plan and creating its replacement can no longer leave a policy with nothing actionable;
status.current_plan_refis cleared before its plan is retired; amode: observepolicy retires its pending plan when drift disappears out of band; and a change set waiting out its failure-retry window reportsPlanFailedRetryBackoffinstead of "awaiting approval". (#185) - The plan approval documentation describes the mechanism that exists — a status-subresource decision with
decidedBy— with workingkubectlcommands;kubectl get pgplan -o wideshows the digest a decision actually binds; and the operator's RBAC covers the candidatepatchanddeleteit performs. (#182, #184)
Full Changelog: v0.9.0...v0.10.0