Checklist¶
Everything in this book, as a list to work through — split by deployment tier, because the sandbox tier is allowed to skip almost all of it and the two production tiers are not. Each item links back to the chapter that explains it.
Sandbox / development only¶
The shipped deploy/ files describe this tier. It is fine for what it is, as long as everyone knows what it is:
- [ ] The Processor runs on the database host as
postgresover the Unix socket — and this host holds no data anyone cares about. (Where to Run) - [ ] Nobody promotes this configuration to a shared or production database by copying it. The moment a deployment matters, it moves to the tier below.
Production — on-premises / self-managed¶
Placement and network (Where to Run the Processor)¶
- [ ] The Processor runs on its own VM or server, not the PostgreSQL host.
- [ ] That host sits in a restricted zone with no inbound firewall rules for pg_relay (admin access via your usual bastion path only).
- [ ] The database zone allows TCP 5432 inbound from the Processor hosts' addresses only, and has no egress on pg_relay's behalf.
- [ ] Processor egress is allowlisted: an internal HTTPS forward proxy (
HTTPS_PROXY/NO_PROXYin the environment file) and/or an internal SMTP smart host, plus the DNS resolver — and nothing direct to the internet unless consciously excepted. - [ ] If more than one Processor is run, each host meets every item here identically.
The service (Hardening the Service)¶
- [ ] A dedicated system user (
pgrelay) withnologinshell, no home directory, used by nothing else. - [ ] The hardened systemd unit is installed:
NoNewPrivileges=yes,ProtectHome=yes,PrivateTmp=yes,ProtectSystem=strict,ProtectProc=invisible+ProcSubset=pid, theProtect*/Restrict*set,MemoryDenyWriteExecute=yes,SystemCallFilter=@system-service, emptyCapabilityBoundingSet=,RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6,UMask=0077,LimitCORE=0. - [ ]
systemd-analyze security pg_relay.servicehas been run and anything it still flags has been consciously accepted. - [ ]
/procis mounted withhidepid=2(with agid=exemption group for monitoring only), applied in/etc/fstaband verified after a reboot. - [ ] Core dumps are disabled at all three layers:
LimitCORE=0in the unit,Storage=none+ProcessSizeMax=0in/etc/systemd/coredump.conf,fs.suid_dumpable=0via sysctl — and confirmed with a deliberate crash of a test instance. - [ ]
/etc/pg_relay/pg_relay.envisroot:root 0600(read by systemd, unreadable bypgrelay);/etc/pg_relay/.pgpassispgrelay:pgrelay 0600; the binary is root-owned and not writable bypgrelay. - [ ]
PGPASSWORDappears nowhere.PGPASSFILEis the only password mechanism. - [ ]
PGSSLMODE=verify-fullwithPGSSLROOTCERTset — neverpreferorrequire. - [ ] The journal is forwarded to central logging;
--test-notifyis only ever run interactively. - [ ] Rotation is understood and rehearsed:
.pgpass+request_reload()for the database credential (live); environment file + rollingsystemctl restartfor provider secrets.
The database (Database-Side Controls)¶
- [ ]
pg_hba.confhashostssl … pgrelay … <host>/32 … scram-sha-256per Processor host and a finalreject;password_encryption = scram-sha-256. - [ ] Server
ssl = onwith a CA the Processor'sPGSSLROOTCERTtrusts (orcertauthentication with a client certificate instead of a password). - [ ]
pgrelayisLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATIONwith a sizedCONNECTION LIMIT. - [ ]
pgrelayholds no table privileges outside thepgrelayschema (verified with theinformation_schema.table_privilegesquery). - [ ]
PUBLICholds no table privileges the Processor should not have; on a shared database,pgrelay.notify()is granted per role rather than toPUBLICif not every role should fire events. - [ ]
GRANT … TO pgrelayis empty, or holds only narrow per-job maintenance roles, and the list is under change control (pg_auth_membersquery). - [ ]
grant_user()recipients are named, scoped roles — neverPUBLIC, neverpgrelay. - [ ]
log_connections/log_disconnectionsare on andpgrelaysessions are watched for unexpectedclient_addr.
Provider secrets and detection (What a Compromised Processor Can Do)¶
- [ ] Every provider credential is scoped as narrowly as the provider allows: a send-only mailbox, an app registration with only
Mail.Send, a webhook token good for one endpoint, a metrics token with write-only scope. - [ ]
PG_RELAY_STOP_NOTIFYandPG_RELAY_OUTAGE_NOTIFYare configured, so an unexpected exit or lockout announces itself. - [ ] Someone watches
log_report()/queue_stats()for silent channels and implausible outcomes. - [ ] The containment runbook is written down:
ALTER ROLE pgrelay NOLOGIN+ terminate backends → rotate the database credential → rotate every provider secret → reviewlog_report()/queue_pending()→ rebuild the host → re-enable.
Production — cloud-managed database¶
Everything in the on-premises list applies, with these substitutions and additions (Cloud-Managed Databases):
- [ ] The database has no public address: private subnet / VNet integration or Private Endpoint / Private IP, with public access disabled.
- [ ] The database's security group / NSG / authorised network allows 5432 from the Processor's own security group or subnet only.
- [ ] The Processor host (VM or container task) has no public IP and no inbound rule; egress goes through NAT to an allowlist or an egress firewall / proxy.
- [ ] Identity-based database authentication is used where available (RDS/Aurora IAM auth, Entra auth on Azure, IAM auth through the Cloud SQL Auth Proxy) — with a token refresher writing
.pgpass, or the proxy handling it — so no long-lived database password exists. - [ ] Provider TLS enforcement is on (
rds.force_ssl,require_secure_transport, SSL-only / Auth Proxy) and the Processor usesverify-fullwith the provider's CA bundle. - [ ] On a VM: the full hardened-service section above. In a container: non-root UID, read-only root filesystem, all capabilities dropped, no-new-privileges, secrets injected from the platform's secrets store, and the node's core-dump settings checked.
- [ ] Provider-secret rotation is planned as a rolling restart of the Processor task.
That is the whole book. For the why behind the database role's limits, the Technical Guide's Security Model chapter is the companion read; for the mechanics of each cloud platform, the Cloud Setup book.