Skip to content

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 postgres over 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_PROXY in 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) with nologin shell, 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, the Protect*/Restrict* set, MemoryDenyWriteExecute=yes, SystemCallFilter=@system-service, empty CapabilityBoundingSet=, RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6, UMask=0077, LimitCORE=0.
  • [ ] systemd-analyze security pg_relay.service has been run and anything it still flags has been consciously accepted.
  • [ ] /proc is mounted with hidepid=2 (with a gid= exemption group for monitoring only), applied in /etc/fstab and verified after a reboot.
  • [ ] Core dumps are disabled at all three layers: LimitCORE=0 in the unit, Storage=none + ProcessSizeMax=0 in /etc/systemd/coredump.conf, fs.suid_dumpable=0 via sysctl — and confirmed with a deliberate crash of a test instance.
  • [ ] /etc/pg_relay/pg_relay.env is root:root 0600 (read by systemd, unreadable by pgrelay); /etc/pg_relay/.pgpass is pgrelay:pgrelay 0600; the binary is root-owned and not writable by pgrelay.
  • [ ] PGPASSWORD appears nowhere. PGPASSFILE is the only password mechanism.
  • [ ] PGSSLMODE=verify-full with PGSSLROOTCERT set — never prefer or require.
  • [ ] The journal is forwarded to central logging; --test-notify is only ever run interactively.
  • [ ] Rotation is understood and rehearsed: .pgpass + request_reload() for the database credential (live); environment file + rolling systemctl restart for provider secrets.

The database (Database-Side Controls)

  • [ ] pg_hba.conf has hostssl … pgrelay … <host>/32 … scram-sha-256 per Processor host and a final reject; password_encryption = scram-sha-256.
  • [ ] Server ssl = on with a CA the Processor's PGSSLROOTCERT trusts (or cert authentication with a client certificate instead of a password).
  • [ ] pgrelay is LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS NOREPLICATION with a sized CONNECTION LIMIT.
  • [ ] pgrelay holds no table privileges outside the pgrelay schema (verified with the information_schema.table_privileges query).
  • [ ] PUBLIC holds no table privileges the Processor should not have; on a shared database, pgrelay.notify() is granted per role rather than to PUBLIC if not every role should fire events.
  • [ ] GRANT … TO pgrelay is empty, or holds only narrow per-job maintenance roles, and the list is under change control (pg_auth_members query).
  • [ ] grant_user() recipients are named, scoped roles — never PUBLIC, never pgrelay.
  • [ ] log_connections/log_disconnections are on and pgrelay sessions are watched for unexpected client_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_NOTIFY and PG_RELAY_OUTAGE_NOTIFY are 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 → review log_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 uses verify-full with 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.