Skip to content

Security Model

  • Function-gated everything. All table and sequence privileges are revoked from PUBLIC; every function's default PUBLIC EXECUTE is revoked; access is re-granted deliberately — the interface set to pgrelay, everything else via grant_sender()/grant_admin(). interface_version() alone carries EXECUTE for PUBLIC, so any role with schema USAGE can probe compatibility — the same convention pg_relay uses for list_applications(). See the DBA Guide for the practical grant walkthrough.
  • SECURITY DEFINER hygiene. Every function pins SET search_path = pgrelay_notifier, pg_temp, closing the classic search-path-hijack vector for definer functions. Provenance (notifications.created_by) uses session_user, not current_user — immune to the definer switch, so it always names who actually called compose(), not the function owner.
  • Secrets never enter the database. Profiles store only the name of a Processor-host environment variable (the _env:VAR_NAME convention — see The Interface Contract). Rotation is an environment change on the Processor host, nothing else. Nothing sensitive appears in pg_dump output, 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 pgrelay role holds exactly four grants — the interface functions plus schema USAGE. 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.