Security
PGEE maintenance-window permissions: schema changes only when approved
CAB tickets don't revoke GRANTs. How PGEE time-related maintenance permissions stop mid-day ALTER TABLE, and why event-trigger DIY falls short.

Your change-advisory board approved the alter for Sunday 02:00. The ticket is green. At 14:17 on a trading day someone with a standing DBA role runs ALTER TABLE … ADD COLUMN on production anyway, because a release is stuck and "it's a tiny change." The lock waits pile up. Or worse: the change lands cleanly, nobody notices until the audit sample asks for the window that never opened.
The process worked. The database didn't enforce it. Community PostgreSQL will happily accept a GRANT that lasts forever and an ALTER that has no concept of "maintenance window." CYBERTEC PostgreSQL Enterprise Edition (PGEE) lists a feature for exactly this gap: time-related / rule-based maintenance permissions. They associate permissions with maintenance windows so data-structure changes are prevented outside the window and allowed inside it. Below: the standing-privilege problem, the community DIY teams try first (and why it is incomplete), what the public PGEE docs actually say, and how to pair the feature with audit and declarative permission diffs.
The standing privilege is the real risk
In a regulated shop the CAB already exists. The gap is enforcement at the object layer:
A
GRANT CREATE, ALTER(or a role that effectively has them via membership) is permanent until someone remembersREVOKE. Tickets close. Privileges do not.Application-release pressure turns "wait for the window" into "just this once." The person who can ship is often the person who can DDL.
Audit samples look for evidence that schema changes happened inside an approved window. A ServiceNow record that says Sunday, and a database catalog that says Thursday 14:17, is a finding, even when nobody meant harm.
Least privilege for DML is a solved conversation in most banks. Least privilege for DDL, *and* least privilege that varies with the clock, is not. That is the ICP for this feature: DBA leads and platform/security architects who already have a CAB and still see mid-day schema changes.
What community PostgreSQL lets you build (and what it misses)
PostgreSQL event triggers are the usual DIY. The official documentation even ships a table_rewrite example that encodes a maintenance window with current_time:
CREATE OR REPLACE FUNCTION no_rewrite() RETURNS event_trigger LANGUAGE plpgsql AS$$DECLARE table_oid oid := pg_event_trigger_table_rewrite_oid(); current_hour integer := extract('hour' from current_time); pages integer; max_pages integer := 100;BEGIN IF pg_event_trigger_table_rewrite_oid() = 'public.foo'::regclass THEN RAISE EXCEPTION 'you''re not allowed to rewrite the table %', table_oid::regclass; END IF; SELECT INTO pages relpages FROM pg_class WHERE oid = table_oid; IF pages > max_pages THEN RAISE EXCEPTION 'rewrites only allowed for table with less than % pages', max_pages; END IF; IF current_hour NOT BETWEEN 1 AND 6 THEN RAISE EXCEPTION 'rewrites only allowed between 1am and 6am'; END IF;END;$$;CREATE EVENT TRIGGER no_rewrite_allowed ON table_rewrite EXECUTE FUNCTION no_rewrite();
That example is real and useful as a teaching tool. It is also incomplete as a production change-control mechanism. Read the same docs carefully:
table_rewriteis narrow. It fires just before a table is rewritten by some actions ofALTER TABLEandALTER TYPE. The documentation states explicitly thatCLUSTERandVACUUMdo not firetable_rewrite, even though they can rewrite a table.ddl_command_startis broader, still not total. It fires just beforeCREATE,ALTER,DROP,COMMENT,GRANT,IMPORT FOREIGN SCHEMA,REINDEX,REFRESH MATERIALIZED VIEW,REVOKE, andSECURITY LABEL(andSELECT INTO). It does not fire for DDL on shared objects: databases, roles (definitions and memberships), tablespaces, parameter privileges,ALTER SYSTEM. It also does not fire for commands that target event triggers themselves.A superuser can remove the guard.
DROP EVENT TRIGGER no_rewrite_allowed;ends the policy. If your emergency break-glass role is also your daily DBA login, the window is optional.current_timefollows the sessionTimeZone. Two sessions with differentTimeZonesettings see different "hours." Clock sync across app servers, jump hosts, and the database host is ordinary ops hygiene; treating session-local time as a security boundary is not.Who owns the trigger function matters. A
SECURITY DEFINERfunction running as a powerful owner is a classic trap if the search path or the exception path is wrong. ASECURITY INVOKERfunction that the DBA role can replace is a different trap.
You can combine ddl_command_start with a WHEN TAG IN (...) filter and a central allow-list table. Many teams do. What you still do not get from community PostgreSQL is a first-class notion that *permissions themselves* are only valid inside a window. You get a procedural gate you maintain, that privileged roles can dismantle, that misses shared-object DDL by design.
What PGEE's public docs actually say
CYBERTEC's feature comparison lists "Time related permissions" as: Community PostgreSQL: not available natively. PGEE: included.
The Enterprise PostgreSQL Security PDF (v1.1, 2025-04-11), in the section "RULE BASED MAINTENANCE PERMISSIONS," states the product intent in prose:
> PGEE allows you to associate permissions with maintenance windows. You can prevent changes to your data structure during certain periods while allowing them during a maintenance window.
That is the claim we rely on. Read the limits as carefully as the promise:
Product feature. It ties permissions to windows, aimed at data-structure change control. Our working inference, labeled as such, is that "data structure" here means schema/DDL-class changes (tables, indexes, and similar), which is the failure mode in the CAB story above. Confirm the exact object scope for your PGEE version during evaluation.
No published public SQL API in the materials we cite. This article deliberately invents neither
GRANT … DURING WINDOWsyntax, nor ServiceNow auto-open hooks, nor an SLA around window switching. If your evaluation build exposes a specific window-definition interface, use that. Do not assume one from a blog post.No compliance certification. Associating permissions with windows helps you *implement* the change-control and least-privilege practices auditors ask about. On its own it does not "satisfy" any named framework.
The same security materials frame an accompanying capability under integrated / declarative permission management: see all permissions, define a desired state, calculate desired versus real, and fix automatically. In the open-source world that companion is CYBERTEC's pg_permissions extension (permission_target, permission_diffs(), views whose granted column can be updated to drive GRANT/REVOKE). PGEE's product framing of "integrated security auditing" sits next to that idea; be precise about which SQL you are running. The open-source extension's API is public. Treat PGEE's packaging of the broader story as product scope to confirm in evaluation, not as a second invented dialect.
Why the product feature exists if DIY "works"
Because the failure mode is social and permanent. Syntax is not the hard part.
A DIY event trigger answers "block this command right now if the hour is wrong." A standing GRANT answers "this role may change the schema any day of the week." Those are different questions. The CAB process tries to answer the second with tickets. The database answers the second with privileges. PGEE's stated feature is about making the privilege itself time-scoped for data-structure changes. The mid-day ALTER TABLE fails because the permission is not in force. That is a different control from a trigger function you wrote that remembers to check extract(hour …).
That matters when:
The person under release pressure is a superuser, who can drop your DIY trigger.
The change is not a table rewrite (so
table_rewritenever fires).The change is
GRANT/REVOKEon an object, or aDROP INDEX, or something yourWHEN TAGlist forgot.Last quarter's DIY trigger was disabled "temporarily" during a migration and never re-enabled.
None of that is a knock on event triggers. Use them. Know their holes. Do not mistake them for time-related permissions.
Pair the window with audit and with desired-state diffs
A blocked attempt that nobody can see is half a control.
Audit. Object-level and DDL-aware auditing is how you show that outside-window attempts happened, who made them, and whether break-glass was used. See our earlier post on tamper-resistant audit logging with PGEE.
Declarative diffs. After the window closes,
permission_diffs()-style desired-versus-real checks catch privileges that were raised for the change and never lowered. A maintenance window that leavesCREATEhanging on a human DBA role is how next Thursday's incident starts.
CREATE EXTENSION pg_permissions SCHEMA public;-- Desired state: app role may DML on appschema tables, not DDL.INSERT INTO public.permission_target (role_name, permissions, object_type, schema_name)VALUES ('appuser', '{SELECT,INSERT,UPDATE,DELETE}', 'TABLE', 'appschema');SELECT * FROM public.permission_diffs();
The snippet above is the open-source pg_permissions API. Use it as the reviewable desired-state layer. Do not read it as the PGEE window-definition API.
A staging checklist you can run this week
List every role that can run DDL on production schemas today (
has_schema_privilege, role membership, and anyBYPASSRLS/ superuser accounts). Assume anything that can DDL on Tuesday afternoon will eventually do so.Reproduce the official
table_rewriteexample in a non-production cluster. Confirm it blocks a rewritingALTER TABLEoutside hours 1-6, then confirm aCLUSTERstill proceeds. That is the documented hole.Add a
ddl_command_starttrigger with a tightWHEN TAG IN (...)list. AttemptCREATE ROLE/ALTER SYSTEMand record what is *not* blocked.As a break-glass exercise, drop the event trigger with a superuser and note how much of your "window" disappeared. Decide whether that role should exist as a daily login.
During PGEE evaluation, ask specifically: how are maintenance windows defined in *your* build, which object classes they cover, how break-glass works, whether superusers are bound by the window, and how window transitions appear in the audit trail. Take the PDF prose as intent; take the evaluation cluster as ground truth.
Talk to Worlber
Worlber is the authorised CYBERTEC reseller in Saudi Arabia. We deploy and support PGEE from Riyadh, including change-control design around maintenance-window permissions, audit configuration, and declarative permission reviews with pg_permissions.
Or email contactus@worlber.com
Sources
CYBERTEC, Enterprise PostgreSQL Security PDF v1.1 (2025-04-11), "RULE BASED MAINTENANCE PERMISSIONS": https://www.cybertec-postgresql.com/wp-content/uploads/2025/02/Enterprise-PostgreSQL-Security.pdf
CYBERTEC PGEE feature comparison ("Time related permissions"): https://www.cybertec-postgresql.com/en/cybertec-pgee-feature-comparison/
CYBERTEC PGEE product page: https://www.cybertec-postgresql.com/en/products/cybertec-postgresql-enterprise-edition/
PostgreSQL docs, Overview of Event Trigger Behavior (
ddl_command_start,table_rewritescope): https://www.postgresql.org/docs/current/event-trigger-definition.htmlPostgreSQL / CYBERTEC mirror, A Table Rewrite Event Trigger Example: https://repository.cybertec.at/doc/current/event-trigger-table-rewrite-example.html
CYBERTEC, pg_permissions (open source): https://github.com/cybertec-postgresql/pg_permissions
Worlber, PostgreSQL audit logging with PGEE: https://blog.worlber.sa/posts/postgresql-audit-logging-tamper-resistant-pgee
Next step
Bring us your database challenge
Send us your environment and goal. We will tell you what fits, what does not, and the safest next step.