Security Model¶
- Function-gated everything. All table and sequence privileges are revoked from
PUBLIC; every function's defaultPUBLIC EXECUTEis revoked; access is re-granted deliberately — the interface set topgrelay, everything else viagrant_sender()/grant_admin().interface_version()alone carriesEXECUTEforPUBLIC, so any role with schemaUSAGEcan probe compatibility — the same convention pg_relay uses forlist_applications(). See the DBA Guide for the practical grant walkthrough. SECURITY DEFINERhygiene. Every function pinsSET search_path = pgrelay_notifier, pg_temp, closing the classic search-path-hijack vector for definer functions. Provenance (notifications.created_by) usessession_user, notcurrent_user— immune to the definer switch, so it always names who actually calledcompose(), not the function owner.- Secrets never enter the database. Profiles store only the name of a Processor-host environment variable (the
_env:VAR_NAMEconvention — see The Interface Contract). Rotation is an environment change on the Processor host, nothing else. Nothing sensitive appears inpg_dumpoutput, logs, or any table. - Content confidentiality. The Processor uses message content transiently and never logs it, at any level. Failure text (
status_detail/pgrelay.log.error) may quote a provider's response, which can reference a recipient address (an SMTP 550, for instance) — both destinations are function-gated tables, not plain logs. - The
pgrelayrole holds exactly four grants — the interface functions plus schemaUSAGE. It cannot read notifications directly, manage profiles, or send on its own initiative; it can only fetch what it's told to deliver and report the outcome.
Why SECURITY DEFINER at all¶
Every producer-facing function (compose, attach, dispatch, send, the profile-management functions) is SECURITY DEFINER. This is what makes the function-gated model work at all: a role granted EXECUTE on send() doesn't need any privileges on the underlying tables — the function runs with the extension owner's rights for the duration of the call, and the caller's own privileges never come into it beyond "was this role allowed to call this function in the first place." That question is answered entirely by GRANT/REVOKE on the function itself (and schema USAGE), which is exactly what grant_sender()/grant_admin() manage.