Where to Run the Processor¶
This chapter is about on-premises and self-managed deployments — anywhere you control the PostgreSQL host yourself. For managed cloud databases, where the host is not yours to run anything on, go to Cloud-Managed Databases; the network principles here still apply there.
Three deployment tiers¶
| Tier | Where the Processor runs | Use it for |
|---|---|---|
| Sandbox / development | On the PostgreSQL server itself, as the postgres user, over the local Unix socket |
Trying pg_relay out; a developer's own machine; a throwaway test database |
| Production, on-premises | On its own VM or server, in a restricted network zone, connecting to the database over TCP with TLS | Everything that is not the row above |
| Production, cloud-managed | On its own VM or container in your cloud account, reaching the database over private networking | Any RDS / Aurora / Azure / Cloud SQL deployment — see the next chapter |
The shipped deploy/pg_relay.service and deploy/pg_relay.env.example describe the first tier: User=postgres, PGHOST=/var/run/postgresql. That is deliberate — it is the configuration that works with nothing else set up — and it is the configuration you should leave behind the moment a deployment matters. The second and third tiers use deploy/pg_relay-hardened.service and deploy/pg_relay-hardened.env.example, covered in Hardening the Service.
Why not the database server?¶
Running the Processor beside PostgreSQL feels natural — it is the database's helper, after all — and it is precisely what a security review should refuse. Three separate problems, any one of which is enough:
1. The Processor's OS user becomes the database superuser's OS user. The convenient co-located setup runs the Processor as postgres so that it can connect over the Unix socket with peer authentication. But postgres is the OS account that owns the data directory and that PostgreSQL's own pg_hba.conf typically trusts as the superuser over that same socket. Anyone who compromises the Processor process is now the postgres user on the database host: they can read the data files directly, connect as superuser, and change pg_hba.conf. Every one of pg_relay's careful database-role limits — the whole point of the Security Model — is bypassed, not by pg_relay, but by the host account it was given.
2. The host that holds your data now has internet egress. The Processor's job is to talk to SMTP servers, Microsoft Graph, Google, webhooks, metrics receivers. That means the host it runs on needs outbound access to those destinations. If that host is the database server, you have just given the machine holding your data a path to the outside world — the exact thing a private database is supposed not to have. Move the Processor off, and the database host's egress rules can stay at "none."
3. Lifecycle coupling. Patching, rebooting, upgrading, or resizing the Processor should never touch the database host, and vice versa. On separate hosts, a Processor upgrade is a five-second service restart nobody else notices; the database host runs only PostgreSQL and can be locked down and change-controlled as such.
A separate host removes all three at once. The Processor is a small Go binary that idles at almost no CPU and a few tens of megabytes of memory; the smallest VM your platform offers is more than enough, and one such VM can host several Processor instances ([email protected]) for high availability.
The network shape¶
Because the Processor initiates every connection and accepts none, the ideal placement is a restricted zone with no inbound access and allowlisted egress — the classic "DMZ-ish" tier that sits between the private database network and the outside:
┌──────────────────────────────┐
│ Database zone (private) │
│ PostgreSQL — no egress, │
│ inbound 5432 from the │
│ Processor hosts only │
└──────────────▲───────────────┘
│ 5432/TCP, TLS (verify-full)
│ initiated by the Processor
┌──────────────┴───────────────┐
NO INBOUND ──▶│ Processor zone (restricted) │
(nothing │ pg_relay — no listening │
listens) │ port, no state on disk │
└──────────────┬───────────────┘
│ egress ONLY, allowlisted
▼
SMTP smart host · HTTPS forward proxy
│ │
▼ ▼
the internet (mail, Graph, ACS, Gmail, webhooks, OTLP)
The firewall rules this needs are short enough to write on one hand:
| Direction | Rule |
|---|---|
| Processor zone → Database zone, TCP 5432 | Allow, from the Processor hosts' addresses only |
| Processor zone → egress | Allow to an allowlist — your SMTP smart host and/or HTTPS proxy, or the specific external endpoints if you allow direct egress |
| Anything → Processor zone | Deny. Nothing listens; there is nothing to reach. Management access (SSH) should come from a bastion or your usual admin path, and is not a pg_relay requirement |
| Database zone → anywhere | Deny. The database initiates nothing on pg_relay's behalf |
That last row is the one to show a security reviewer: pg_relay adds no outbound requirement to the database host, and exactly one inbound rule — from hosts you name, on a port it already serves, as a role that can do very little.
Egress: proxy and smart host¶
For most organisations, "allowlisted egress" is easier to enforce at a single choke point than as a list of provider IP ranges that change without notice. The Processor cooperates with both standard choke points:
- HTTPS forward proxy. Every HTTP-based transport (Microsoft 365, Azure Communication Services, Gmail, webhooks, OTLP) uses Go's standard HTTP client, which honours the conventional
HTTPS_PROXY/HTTP_PROXY/NO_PROXYenvironment variables. Set them in the Processor's environment file and every provider call goes through your proxy, where the destination allowlist and any TLS inspection policy live. The direct route can then be closed entirely. - SMTP smart host. SMTP is a raw TCP protocol and does not use an HTTP proxy. The standard answer — the same one every mail-sending application uses — is an internal SMTP relay ("smart host") in the Processor zone. The Processor talks to it on a private address, typically with
"auth": "none"and"security": "none"or"starttls"inside your own network. The smart host owns the actual internet hop and authenticates to the upstream provider with credentials the Processor never holds. It also gives you one place to apply outbound mail policy (DKIM signing, rate limits, recipient restrictions).
With those two in place, the Processor host's egress rule can be reduced to two internal addresses — the proxy and the smart host — plus the database. Nothing on the Processor host can reach the internet directly at all.
The Processor still needs DNS
Even behind a proxy, PGHOST must resolve, and if you use a private DNS zone for the database and the proxy, the Processor host needs to reach that resolver. Include it in the egress allowlist; it is easy to forget when everything else is locked down.
One Processor host or several?¶
Running two or more Processor instances is how pg_relay achieves high availability — work is partitioned automatically, and instances need no coordination. From a security point of view, two small identical hosts in the same restricted zone are better than one: a compromise of one is contained by the same rules, and you can patch them one at a time. If you use only one host, [email protected] still lets you run several instances on it for throughput.
Whatever the count, the rules in this chapter apply per host, identically. The Processor has no notion of a "primary" instance, so there is nothing special to protect.
What this chapter did not need¶
It is worth noticing what does not appear above, because it is the list a security team usually expects to see. There is no inbound port to justify, no TLS certificate for the Processor to present, no reverse proxy, no WAF, no callback URL to register with a provider, and no shared secret a provider must hold to reach you. pg_relay is outbound-only by design. That design is what makes a strict zone placement a matter of three firewall rules rather than a project.
Continue to Cloud-Managed Databases.