Cloud-Managed Databases¶
On Amazon RDS and Aurora, Azure Database for PostgreSQL, and Google Cloud SQL, the "separate host" rule of the previous chapter is not a recommendation but a fact: you cannot run anything on the database host. The Processor therefore lives somewhere in your cloud account, and the security questions become how it reaches the database privately, how it authenticates without a long-lived password, and how the thing it runs on is locked down.
This chapter covers the security dimension only. The mechanics — which instance type, how to build a container image, the exact Cloud SQL Auth Proxy setup — are in the Cloud Setup book, and this chapter links to them rather than repeating them.
Keep the database off the internet entirely¶
Every managed provider can expose a database with a public address and can, equally, keep it private. Use the private option; pg_relay gives you no reason not to.
| Provider | The private path the Processor should use |
|---|---|
| Amazon RDS / Aurora | The instance in a private subnet with Publicly accessible: No; the Processor in the same VPC (or a peered/Transit-Gateway-connected one); a security group on the database allowing 5432 only from the Processor's security group, never from a CIDR that includes anything else |
| Azure Database for PostgreSQL | Private access (VNet integration) or a Private Endpoint, with public network access disabled; the Processor in the VNet; an NSG allowing 5432 from the Processor subnet only |
| Google Cloud SQL | Private IP with the public IP disabled, reached through the Cloud SQL Auth Proxy running on the Processor host (the proxy is the one process that legitimately sits beside the Processor — it is a local outbound tunnel, and it listens only on 127.0.0.1) |
In every case the shape is the one from the previous chapter's diagram: the database accepts a single inbound connection from a source you name, and initiates nothing.
The Processor's own host¶
The Processor needs a small compute resource in the same private network. The choice — a VM with systemd, or a container on ECS Fargate / Azure Container Instances / Cloud Run-style services — is covered in Running the Processor. From a security standpoint the requirements are the same whichever you choose:
- No public IP address, and no inbound rule. The Processor listens on nothing. On a VM, the security group / NSG has no inbound entries for pg_relay at all (SSH from a bastion or SSM Session Manager / Azure Bastion / IAP is your admin path, not a pg_relay need). On a container platform, do not attach a load balancer or public ingress — the task needs none.
- Egress through NAT to an allowlist. A private subnet with a NAT gateway (or the provider's equivalent) gives the Processor outbound access without an inbound address. Restrict the NAT's destinations, or route provider traffic through an egress proxy / firewall service (AWS Network Firewall, Azure Firewall, Google Cloud NAT + firewall rules), exactly as the on-premises chapter recommends with
HTTPS_PROXY. Direct-to-internet egress from the Processor should be the exception you consciously allow, not the default. - Private endpoints for the providers you use, where offered. Microsoft 365 / Graph, Azure Communication Services, and Google APIs can often be reached over the provider's private backbone (Azure Private Link, Google Private Google Access, AWS PrivateLink for services that offer it) so that even "external" calls never traverse the public internet. This is optional, but it turns the egress allowlist into an even shorter list.
- On a VM, everything in Hardening the Service applies unchanged — the dedicated user, the systemd sandbox,
/prochiding, core-dump suppression. A cloud VM is still a Linux host. - In a container, the equivalent hardening is the platform's: run as a non-root user in the image (the example Dockerfile in Cloud Setup does), a read-only root filesystem (the Processor writes nothing), no added capabilities, and the platform's secrets integration for the environment (next section).
Credentials without a long-lived password¶
A managed database is where identity-based authentication earns its keep, and the Processor is built to use it: it re-reads its libpq environment — including PGPASSFILE — on every connection it opens, so a credential that is rotated on disk takes effect on the next reconnect without a restart (and pgrelay.request_reload() forces one whenever you want).
| Provider | Mechanism | How it fits the Processor |
|---|---|---|
| Amazon RDS / Aurora | IAM database authentication — a 15-minute token in place of a password, obtained with the Processor host's IAM role (rds generate-db-auth-token) |
A small refresher (a systemd timer, or a sidecar) writes the current token into the .pgpass file PGPASSFILE points at; the Processor's next reconnect uses it. No password exists anywhere |
| Azure Database for PostgreSQL | Microsoft Entra authentication — an access token from the VM's or container's managed identity | Same pattern: a refresher writes the token to .pgpass; the database role is the managed identity's Entra principal |
| Google Cloud SQL | IAM database authentication via the Auth Proxy (--auto-iam-authn) |
The proxy handles the token entirely; the Processor connects to 127.0.0.1 with no password at all |
In each case the outcome is the same: there is no database password to protect, and the credential that does exist expires in minutes and is bound to the Processor host's own cloud identity — it is useless if copied elsewhere. If you cannot use identity-based auth, fall back to a strong password in .pgpass under the file-permission rules in Hardening the Service, rotated on a schedule, and never PGPASSWORD.
Provider secrets are separate from the database credential
The credentials for the external services — an SMTP password, a Graph client secret, a webhook bearer token — still arrive as environment variables referenced by _env:VAR_NAME in notifier profiles (see Keeping Secrets Out of the Database). On a VM those live in the root-only environment file; on a container platform, inject them from the platform's secrets store (AWS Secrets Manager / SSM Parameter Store into ECS task definition secrets, Azure Key Vault references, Google Secret Manager) so they never appear in a task definition or image. A running Processor's environment is fixed at start, so rotating one of these requires a restart — plan the rotation as a rolling restart of the Processor task, not as an edit to a running one.
TLS to the database is mandatory here¶
On-premises, TLS between the Processor and PostgreSQL is strongly recommended. Across a cloud network it is not negotiable: set PGSSLMODE=verify-full and point PGSSLROOTCERT at the provider's CA bundle (each provider publishes one — the RDS global bundle, Azure's DigiCert/Microsoft RSA roots, Google's server CA downloadable per instance). verify-full is the only mode that checks the certificate and that the hostname matches; require encrypts but would happily talk to an impostor. With the Cloud SQL Auth Proxy the proxy performs this verification itself, and the Processor's local hop to 127.0.0.1 is inside the host.
Two things managed platforms make easier¶
Worth stating plainly, because security reviews of cloud deployments tend to assume the opposite:
- The database host is unreachable by definition. Nobody — not you, not an attacker who compromises the Processor — can log in to the machine running PostgreSQL. The entire class of "Processor compromise leads to host compromise" risk from the on-premises chapter does not exist. What remains is exactly the database-role blast radius analysed in What a Compromised Processor Can Do, and nothing more.
- Network policy is declarative and auditable. A security group that allows 5432 from one other security group is a single reviewable object. The on-premises firewall rules exist here too, but as configuration you can diff, version, and alert on.
Continue to What a Compromised Processor Can Do.