Streaming & Messaging AWS

PostgreSQL 18 with PGMQ on AWS User Guide

| Product: PostgreSQL 18 with PGMQ on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the PostgreSQL 18 with PGMQ on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg streaming and messaging listing on AWS Marketplace; you pick it as the software version when you subscribe.

PGMQ is an open source (PostgreSQL licence) message queue that lives inside PostgreSQL. A queue is a pair of ordinary tables, and you use it with SQL functions in the style of Amazon SQS: pgmq.send puts a JSON message on a queue, pgmq.read hands it to one consumer and hides it from the others for a visibility timeout, and the consumer then calls pgmq.delete or pgmq.archive when it has finished. A message that is never deleted becomes visible again when the timeout runs out, so a crashed consumer does not lose work. Because the queue is in your database, a message can be sent in the same transaction as the data it describes, and backups, replication and monitoring are the PostgreSQL ones you already use.

What the cloudimg image adds:

  • No database in the image. The image contains no PostgreSQL cluster, so it carries no data, role, password or key from the build. First boot creates a fresh cluster (with data checksums), the queuedb database with the PGMQ extension, the application role queueapp and a password for each of queueapp and postgres, unique to your instance.
  • TLS only. Every TCP connection to port 5432, loopback included, must use TLS 1.2 or later. A connection without TLS is refused by pg_hba.conf (Step 10). Passwords are stored as SCRAM-SHA-256 verifiers.
  • A certificate made on your instance. At first boot the server gets a key and an end-entity certificate for this instance's own names and addresses. It can only vouch for this server and can never sign other certificates. Your clients trust exactly that certificate with sslmode=verify-ca (Step 5).
  • The superuser stays local. postgres logs in on the instance only (over the local socket, or TLS on loopback). From any other address it is refused even with the right password. Applications use queueapp or the least-privilege roles you add (Step 7).
  • A guard that refuses to serve unsafely. On every boot pgmq-credguard proves the per-instance passwords work and that none of 13 published or example passwords (postgres, changeme, queueapp, ...) logs in as either role. If one does, or the outbound block is not loaded, PostgreSQL is stopped (Step 11). If first boot cannot finish, nothing listens.
  • No access to the instance metadata service and no outbound network. PostgreSQL cannot reach the EC2 instance metadata service, and the postgres account may only answer connections that clients open to it (Step 12).
  • A pinned supply chain. PostgreSQL 18.6 comes from the official PostgreSQL (PGDG) apt repository with its signing key pinned. PGMQ 1.13.1 is the upstream release, with the SHA-256 of its tarball and licence pinned. PGMQ is pure SQL here, so no compiled code is added to the server. Ubuntu 24.04 LTS is fully patched at build time, and unattended upgrades cover the PGDG archive as well as Ubuntu, so later PostgreSQL 18 releases keep arriving through apt.

What is on the image: PostgreSQL 18.6 (postgresql@18-main.service; configuration in /etc/postgresql/18/main/, data in /var/lib/postgresql/18/main), PGMQ 1.13.1 in the pgmq schema of queuedb, the certificate in /etc/cloudimg/pgmq/tls/, and the cloudimg services pgmq-metadata-names, pgmq-firstboot, pgmq-egress-guard and pgmq-credguard.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large · Root volume: 12 GiB

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet (or a private route you use for management).
  • A security group that allows:
Port Protocol Source Purpose
22 TCP your admin network SSH
5432 TCP your application's security group, or your VPC CIDR PostgreSQL over TLS

Never open 5432 to 0.0.0.0/0. The listing's default security group opens port 22 only, so add the 5432 rule yourself for the hosts that run your producers and consumers. Nothing else listens on the network (Step 3).

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick PostgreSQL 18 with PGMQ on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger for many connections or a high message rate), your key pair and a security group as above.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=pgmq}]'

The image requires IMDSv2 (session tokens). First boot takes under a minute after the instance is running: pgmq-metadata-names reads the instance's EC2 names for the certificate, pgmq-firstboot creates the cluster, the certificate and the passwords, then PostgreSQL starts and the guard checks it.

Step 2: Connect to your instance

Version SSH user
PostgreSQL 18 with PGMQ on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

The login banner shows the database, the certificate fingerprint and where your credentials are.

Step 3: Check the services and the listening set

systemctl is-active pgmq-egress-guard pgmq-firstboot postgresql@18-main pgmq-credguard
sudo ss -tlnup | grep -E ':(22|5432) '
active
active
active
active
tcp   LISTEN 0      4096              0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=883,fd=3),("systemd",pid=1,fd=251))
tcp   LISTEN 0      200               0.0.0.0:5432      0.0.0.0:*    users:(("postgres",pid=1422,fd=6))
tcp   LISTEN 0      4096                 [::]:22           [::]:*    users:(("sshd",pid=883,fd=4),("systemd",pid=1,fd=252))
tcp   LISTEN 0      200                  [::]:5432         [::]:*    users:(("postgres",pid=1422,fd=7))

pgmq-egress-guard holds the outbound block (Step 12), pgmq-firstboot set the instance up, postgresql@18-main is the server and pgmq-credguard checked it. PostgreSQL listens on 5432 on every address, IPv4 and IPv6, and the only other network listener is SSH. The UDP sockets on the instance are the operating system's own: the DNS stub resolver and the time service on loopback, and the DHCP client.

Step 4: Your credentials

Everything generated for your instance is in /root/pgmq-credentials.txt, readable by root only. This prints it with the two passwords hidden:

sudo sed -E 's/^([A-Z_]+_PASSWORD)=.*/\1=<hidden>/' /root/pgmq-credentials.txt
# PostgreSQL 18.6 with PGMQ 1.13.1 (cloudimg AWS Marketplace image)
# Generated on THIS instance's first boot by pgmq-firstboot.service; these passwords exist nowhere else.
# Rotate a password (never on a command line) with:  sudo -u postgres psql   then   \password queueapp
# and update this file to match (pgmq-credguard reads it at boot).

POSTGRES_SUPERUSER_PASSWORD=<hidden>
QUEUEAPP_PASSWORD=<hidden>

# Database 'queuedb' (owner queueapp) with the PGMQ extension (schema pgmq) on port 5432.
# The postgres superuser is for local administration only: remote hosts are refused for it.
#
# TLS: every TCP connection must use TLS. This server's certificate (public; copy it to your clients):
#   /etc/cloudimg/pgmq/tls/server.crt
#   SHA-256 fingerprint 77:E9:EF:ED:D7:A1:C2:63:0C:CF:B0:B2:AD:AD:A3:4B:C9:CD:DE:64:36:23:26:60:0A:AC:3A:61:90:4E:85:31
# Connect on this instance:
#   psql "host=127.0.0.1 dbname=queuedb user=queueapp sslmode=verify-ca sslrootcert=/etc/cloudimg/pgmq/tls/server.crt"
# Private address of this instance: 172.31.95.135
#
# NETWORK ACCESS: port 5432 is CLOSED in the default security group (SSH 22 only). Open it to your application's
# security group or your VPC CIDR only (never 0.0.0.0/0); remote clients use sslmode=verify-ca with the certificate above.
#
# User guide: https://www.cloudimg.co.uk/guides/postgresql-18-with-pgmq-on-ubuntu-24-04-aws/

POSTGRES_SUPERUSER_PASSWORD is for the postgres superuser and QUEUEAPP_PASSWORD for the application role queueapp, which owns queuedb, may create queues and is not a superuser. The psql line in the file works as root (sudo); as ubuntu, use the copy of the certificate you make in Step 5. Keep passwords in files with mode 0600, never on a command line (a command line is visible to every user of a machine through ps) and never in a connection string in a configuration file.

Step 5: Connect with TLS and a password file

The server's certificate is self-signed, made on your instance:

sudo openssl x509 -in /etc/cloudimg/pgmq/tls/server.crt -noout -subject -enddate -ext subjectAltName,basicConstraints
sudo openssl x509 -in /etc/cloudimg/pgmq/tls/server.crt -noout -fingerprint -sha256
subject=CN = ec2-18-233-161-75.compute-1.amazonaws.com, O = cloudimg
notAfter=Nov  5 10:42:35 2036 GMT
X509v3 Subject Alternative Name:
    DNS:ec2-18-233-161-75.compute-1.amazonaws.com, DNS:localhost, DNS:ip-172-31-95-135.ec2.internal, IP Address:127.0.0.1, IP Address:172.31.95.135, IP Address:18.233.161.75
X509v3 Basic Constraints: critical
    CA:FALSE
sha256 Fingerprint=77:E9:EF:ED:D7:A1:C2:63:0C:CF:B0:B2:AD:AD:A3:4B:C9:CD:DE:64:36:23:26:60:0A:AC:3A:61:90:4E:85:31

CA:FALSE means it is an end-entity certificate that cannot sign anything else. The fingerprint is the one in your credentials file and the login banner. The certificate covers the instance's EC2 names, its private and public IPv4 addresses, localhost and 127.0.0.1.

The certificate's directory is readable by root and PostgreSQL only, so copy the certificate (it is public; the private key stays where it is) to your home directory, and write the queueapp password into a password file that only you can read. Then connect with sslmode=verify-ca, which checks that the server presents exactly this certificate:

sudo cat /etc/cloudimg/pgmq/tls/server.crt > ~/pgmq-server.crt
( umask 077; sudo sed -n 's/^QUEUEAPP_PASSWORD=/127.0.0.1:5432:queuedb:queueapp:/p' /root/pgmq-credentials.txt > ~/.pgpass-queuedb )
export PGPASSFILE=~/.pgpass-queuedb
psql "host=127.0.0.1 dbname=queuedb user=queueapp sslmode=verify-ca sslrootcert=$HOME/pgmq-server.crt" \
  -c "SELECT current_user, ssl, version AS tls, (SELECT extversion FROM pg_extension WHERE extname = 'pgmq') AS pgmq FROM pg_stat_ssl WHERE pid = pg_backend_pid();"
 current_user | ssl |   tls   |  pgmq
--------------+-----+---------+--------
 queueapp     | t   | TLSv1.3 | 1.13.1
(1 row)

The password goes from the root-only credentials file straight into ~/.pgpass-queuedb (mode 0600) without appearing on a command line or in your shell history, and psql reads it from the file named by PGPASSFILE. The format is host:port:database:user:password, one line per connection.

Step 6: PGMQ quick start

Create a queue, send two messages, read one (it is then hidden from other readers for 30 seconds), archive it, delete the other, and check the queue:

export PGPASSFILE=~/.pgpass-queuedb
psql "host=127.0.0.1 dbname=queuedb user=queueapp sslmode=verify-ca sslrootcert=$HOME/pgmq-server.crt" -X -v ON_ERROR_STOP=1 <<'SQL'
SELECT pgmq.create('orders');
SELECT * FROM pgmq.send('orders', '{"order_id": 1001, "item": "widget"}');
SELECT * FROM pgmq.send('orders', '{"order_id": 1002, "item": "gadget"}');
-- read 1 message and hide it from other readers for 30 seconds
SELECT msg_id, read_ct, message FROM pgmq.read('orders', 30, 1);
-- a second reader now gets the next message, not the hidden one
SELECT msg_id, read_ct, message FROM pgmq.read('orders', 30, 1);
-- done with 1001: keep it in the archive table; done with 1002: delete it
SELECT pgmq.archive('orders', 1);
SELECT pgmq.delete('orders', 2);
SELECT msg_id, message FROM pgmq.a_orders;
SELECT queue_name, queue_length, total_messages FROM pgmq.metrics('orders');
SQL
 create
--------

(1 row)

 send
------
    1
(1 row)

 send
------
    2
(1 row)

 msg_id | read_ct |               message
--------+---------+--------------------------------------
      1 |       1 | {"item": "widget", "order_id": 1001}
(1 row)

 msg_id | read_ct |               message
--------+---------+--------------------------------------
      2 |       1 | {"item": "gadget", "order_id": 1002}
(1 row)

 archive
---------
 t
(1 row)

 delete
--------
 t
(1 row)

 msg_id |               message
--------+--------------------------------------
      1 | {"item": "widget", "order_id": 1001}
(1 row)

 queue_name | queue_length | total_messages
------------+--------------+----------------
 orders     |            0 |              2
(1 row)

The functions you will use most:

Function What it does
pgmq.create('q') creates queue q (the tables pgmq.q_q and pgmq.a_q); safe to run again
pgmq.send('q', '{...}'::jsonb [, delay_seconds]) adds a message and returns its msg_id; with a delay it stays invisible until then
pgmq.send_batch('q', ARRAY['{...}', '{...}']::jsonb[]) adds several messages in one call
pgmq.read('q', vt_seconds, qty) returns up to qty visible messages and hides them for vt_seconds
pgmq.read_with_poll('q', vt_seconds, qty, max_poll_seconds) like read, but waits up to max_poll_seconds for a message
pgmq.pop('q') reads and deletes in one step (at most once delivery)
pgmq.delete('q', msg_id) / pgmq.archive('q', msg_id) finishes a message: removes it, or moves it to the archive table
pgmq.set_vt('q', msg_id, vt_seconds) extends (or shortens) a message's visibility timeout
pgmq.metrics('q') / pgmq.list_queues() queue length, age of the oldest message, totals
pgmq.purge_queue('q') / pgmq.drop_queue('q') empties a queue / removes it

The archive table is an ordinary table: delete old rows from it (for example DELETE FROM pgmq.a_orders WHERE archived_at < now() - interval '30 days') when you no longer need them.

Step 7: Least-privilege producer and consumer roles

queueapp can do everything with every queue. Give each application only what it needs instead. PGMQ's functions run with the privileges of the role that calls them, so ordinary table privileges on a queue's tables decide who may do what:

  • a producer needs INSERT on the queue table, plus SELECT on its msg_id column (so send can return the new id);
  • a consumer needs SELECT, UPDATE and DELETE on the queue table (to read, hide and delete), plus INSERT and SELECT (msg_id) on the archive table if it archives;
  • both need CONNECT on queuedb (the image revokes it from PUBLIC) and USAGE on the pgmq schema. Neither gets CREATE, so neither can create or drop queues.

This creates the two roles for the orders queue as the local superuser, then shows what each may do (SET ROLE switches to the role in the same session):

cd /tmp && sudo -u postgres psql -X -d queuedb <<'SQL'
\set VERBOSITY terse
CREATE ROLE orders_producer NOLOGIN;
CREATE ROLE orders_consumer NOLOGIN;
GRANT CONNECT ON DATABASE queuedb TO orders_producer, orders_consumer;
GRANT USAGE ON SCHEMA pgmq TO orders_producer, orders_consumer;
GRANT INSERT, SELECT (msg_id) ON pgmq.q_orders TO orders_producer;
GRANT SELECT, UPDATE, DELETE ON pgmq.q_orders TO orders_consumer;
GRANT INSERT, SELECT (msg_id) ON pgmq.a_orders TO orders_consumer;

SET ROLE orders_producer;
SELECT * FROM pgmq.send('orders', '{"order_id": 1003}');
SELECT msg_id FROM pgmq.read('orders', 30, 1);
RESET ROLE;

SET ROLE orders_consumer;
SELECT msg_id, message FROM pgmq.read('orders', 30, 1);
SELECT pgmq.archive('orders', 3);
SELECT * FROM pgmq.send('orders', '{"order_id": 1004}');
SELECT pgmq.create('another_queue');
RESET ROLE;
SQL
CREATE ROLE
CREATE ROLE
GRANT
GRANT
GRANT
GRANT
GRANT
SET
 send
------
    3
(1 row)

ERROR:  permission denied for table q_orders
RESET
SET
 msg_id |      message
--------+--------------------
      3 | {"order_id": 1003}
(1 row)

 archive
---------
 t
(1 row)

ERROR:  permission denied for table q_orders
ERROR:  permission denied for schema pgmq at character 33
RESET

The producer could send but not read, and the consumer could read and archive but not send or create a queue. Repeat the table grants for each queue a role should use. To let a monitoring role call pgmq.metrics, grant it SELECT on the queue table and on its msg_id sequence (pgmq.q_orders_msg_id_seq).

Step 8: Add login roles safely

The roles above cannot log in; they hold privileges. Give each application its own login role that is a member of one of them, and set its password with psql's \password command, which prompts for it and sends only a SCRAM verifier to the server, so the password never appears in a command line, your shell history or the server log:

sudo -u postgres psql -d queuedb
queuedb=# CREATE ROLE orders_api LOGIN IN ROLE orders_producer;
queuedb=# CREATE ROLE orders_worker LOGIN IN ROLE orders_consumer CONNECTION LIMIT 20;
queuedb=# \password orders_api
queuedb=# \password orders_worker
queuedb=# \q

A new login role can connect from any address your security group lets reach port 5432, over TLS only. To limit a role to particular networks, add a line for it to /etc/postgresql/18/main/pg_hba.conf above the two hostssl all all lines, followed by a reject for that role from everywhere else, then reload:

hostssl  queuedb  orders_worker  10.0.2.0/24  scram-sha-256
hostssl  all      orders_worker  0.0.0.0/0    reject
sudo systemctl reload postgresql@18-main

Never add host (non-TLS), trust, password or md5 lines, and keep the two postgres ... reject lines.

Changing the shipped passwords. You can change the queueapp or postgres password with \password queueapp (or \password postgres) in the same way. Then update the matching line of /root/pgmq-credentials.txt (for example with sudo nano /root/pgmq-credentials.txt): pgmq-credguard logs in with the passwords in that file at every boot, and stops PostgreSQL if they no longer work.

Step 9: Connect from your application

Allow port 5432 from your application's security group (Prerequisites). On the application host, copy the server's certificate (after Step 5 it is in the ubuntu home directory) and check its fingerprint against your credentials file:

client-host$ scp -i <your-key.pem> ubuntu@<public-ip>:pgmq-server.crt .
client-host$ openssl x509 -in pgmq-server.crt -noout -fingerprint -sha256

Give your client only this file as its root certificate (sslrootcert) with sslmode=verify-ca. Do not add it to the operating system's trust store. Because the certificate also names the instance's private address and EC2 DNS names, sslmode=verify-full works too when you connect by one of them. Put the password in a password file only you can read, for the private address you connect to:

client-host$ umask 077
client-host$ ssh -i <your-key.pem> ubuntu@<public-ip> "sudo sed -n 's/^QUEUEAPP_PASSWORD=/<private-ip>:5432:queuedb:queueapp:/p' /root/pgmq-credentials.txt" > ~/.pgpass-queuedb
client-host$ PGPASSFILE=~/.pgpass-queuedb psql "host=<private-ip> dbname=queuedb user=queueapp sslmode=verify-ca sslrootcert=pgmq-server.crt" \
  -c "SELECT * FROM pgmq.send('orders', '{\"from\": \"client-host\"}')"

In application code, pass the same settings to your PostgreSQL driver. For example with Python and psycopg 3, a worker that reads, processes and archives messages:

# pgmq_worker.py - read messages from a cloudimg PGMQ queue over TLS (psycopg 3).
import os
import psycopg

conn = psycopg.connect(
    host=os.environ["PGMQ_HOST"],            # the instance's private IP or DNS name
    dbname="queuedb",
    user="orders_worker",                     # a login role from Step 8
    sslmode="verify-ca",
    sslrootcert="pgmq-server.crt",
    passfile=os.path.expanduser("~/.pgpass-queuedb"),
    autocommit=True,
)
while True:
    rows = conn.execute(
        "SELECT msg_id, message FROM pgmq.read_with_poll('orders', 30, 10, 5)").fetchall()
    for msg_id, message in rows:
        print("processing", msg_id, message)   # your work here
        conn.execute("SELECT pgmq.archive('orders', %s)", (msg_id,))

From another host the server refuses, with these errors, a wrong password (FATAL: password authentication failed for user "queueapp"), a connection without TLS (FATAL: pg_hba.conf rejects connection for host "...", user "queueapp", database "queuedb", no encryption), the postgres superuser even with its right password (FATAL: pg_hba.conf rejects connection for host "...", user "postgres", database "queuedb", SSL encryption) and a client that trusts a different certificate (SSL error: certificate verify failed).

Step 10: Why only 5432 over TLS is open

PostgreSQL listens on every address so that your application hosts can reach it, and pg_hba.conf decides who may log in. This image's rules are:

sudo grep -vE '^(#|$)' /etc/postgresql/18/main/pg_hba.conf
sudo -u postgres psql -X -d postgres -tAc "SELECT name || ' = ' || setting FROM pg_settings WHERE name IN ('listen_addresses', 'ssl', 'ssl_min_protocol_version', 'password_encryption') ORDER BY name" 2>/dev/null
local      all       postgres                peer
local      all       all                     scram-sha-256
hostssl    all       all       127.0.0.1/32  scram-sha-256
hostssl    all       all       ::1/128       scram-sha-256
hostssl    all       postgres  0.0.0.0/0     reject
hostssl    all       postgres  ::/0          reject
hostssl    all       all       0.0.0.0/0     scram-sha-256
hostssl    all       all       ::/0          scram-sha-256
hostnossl  all       all       0.0.0.0/0     reject
hostnossl  all       all       ::/0          reject
listen_addresses = *
password_encryption = scram-sha-256
ssl = on
ssl_min_protocol_version = TLSv1.2
  • Only hostssl lines accept TCP, with scram-sha-256; the hostnossl ... reject lines refuse every TCP connection without TLS, loopback included.
  • The postgres superuser is refused from every address except the instance itself.
  • There is no trust, password or md5 method and no plain host line.

A connection without TLS fails before any password is sent:

PGPASSFILE=~/.pgpass-queuedb psql "host=127.0.0.1 dbname=queuedb user=queueapp sslmode=disable" -c 'SELECT 1' 2>&1 | sed -n 's/.*FATAL: */FATAL: /p' || true
FATAL: pg_hba.conf rejects connection for host "127.0.0.1", user "queueapp", database "queuedb", no encryption

The security group is the first gate, so keep 5432 open only to the hosts that need it. Nothing else listens: there is no HTTP interface, no replication or pooler port and no other database port.

Step 11: The credential guard

pgmq-credguard runs at every boot after PostgreSQL starts. It logs in over TLS with each password from the credentials file (both must work), then tries 13 published and example passwords against both postgres and queueapp (none may work), and checks that the outbound block (Step 12) is loaded exactly as shipped. If any check fails it stops PostgreSQL. Its last result:

systemctl show -p ActiveState -p SubState -p Result pgmq-credguard
sudo tail -n 1 /var/log/cloudimg-pgmq-credguard.log
Result=success
ActiveState=active
SubState=exited
[2026-11-08T10:42:43+0000] pgmq-credguard: PASS: egress table loaded as shipped; per-VM credentials authenticate; 13 forbidden literals rejected for both roles

Its failed test logins appear in the PostgreSQL log at boot as password authentication failed from 127.0.0.1. That is expected.

Step 12: The instance metadata and outbound block

The EC2 instance metadata service (169.254.169.254) hands out the instance's IAM role credentials to any process that asks. A database superuser can run programs as the postgres account (COPY ... FROM PROGRAM), so the server must not be able to reach it, nor use the instance as a stepping stone into your VPC. Two layers keep it in:

  • postgresql@18-main, pgmq-firstboot and pgmq-credguard run with the metadata addresses blocked by systemd (IPAddressDeny, IPv4 and IPv6). Only pgmq-metadata-names, which reads the instance's names for the certificate before first boot, may reach the metadata service, and it may reach nothing else.
  • The postgres account has no outbound network at all. pgmq-egress-guard loads an nftables table that lets that account answer the connections clients open to it, and reach its own port 5432 on loopback, and refuses everything else, over IPv4 and IPv6: your VPC's addresses, the metadata service, the reserved ranges and the internet. PostgreSQL will not start without the table.
for u in postgresql@18-main pgmq-firstboot pgmq-credguard pgmq-metadata-names; do printf '%-20s deny=%s allow=%s\n' "$u" "$(systemctl show -p IPAddressDeny --value "$u")" "$(systemctl show -p IPAddressAllow --value "$u")"; done
sudo nft list table inet cloudimg_pgmq_egress | grep -o 'comment "[a-z0-9-]*"'
sudo -u postgres curl -s -m 5 -o /dev/null -w 'internet as postgres: %{http_code}\n' https://www.postgresql.org/ || true
sudo -u postgres curl -s -m 5 -o /dev/null -w 'metadata as postgres: %{http_code}\n' -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token || true
postgresql@18-main   deny=::ffff:169.254.169.254/128 fd00:ec2::254/128 168.63.129.16/32 ::ffff:168.63.129.16/128 169.254.169.254/32 allow=
pgmq-firstboot       deny=fd00:ec2::254/128 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 169.254.169.254/32 168.63.129.16/32 allow=
pgmq-credguard       deny=::ffff:168.63.129.16/128 168.63.129.16/32 fd00:ec2::254/128 ::ffff:169.254.169.254/128 169.254.169.254/32 allow=
pgmq-metadata-names  deny=0.0.0.0/0 ::/0 allow=169.254.169.254/32 fd00:ec2::254/128
comment "cloudimg-pgmq-backstop4"
comment "cloudimg-pgmq-mcast4"
comment "cloudimg-pgmq-mcast6"
comment "cloudimg-pgmq-replies"
comment "cloudimg-pgmq-lo4-pg"
comment "cloudimg-pgmq-lo6-pg"
comment "cloudimg-pgmq-deny-tcp"
comment "cloudimg-pgmq-deny"
internet as postgres: 000
metadata as postgres: 000

The 000 results mean no connection was made. This image needs no outbound connection from the server: it has no replica, no postgres_fdw or dblink peer and no WAL archive target. If you add one, allow exactly that address and port in /usr/local/share/cloudimg/pgmq/pgmq-egress.nft (a rule before cloudimg-pgmq-deny-tcp) and update /usr/local/sbin/pgmq-credguard, which checks the table's rules, before you restart pgmq-egress-guard.

Maintenance

  • Updates. Unattended upgrades install Ubuntu security updates and PostgreSQL 18 minor releases from the PGDG archive. A PostgreSQL update restarts the server. Run sudo apt update && sudo apt full-upgrade for everything else. PGMQ is installed from its release files (/usr/share/postgresql/18/extension/pgmq*), not from a package.
  • Backups. Use your usual PostgreSQL tools on the instance, for example sudo -u postgres pg_dump -Fc queuedb > queuedb.dump, or EBS snapshots of the root volume. The queue and archive tables are included like any other table.
  • Restart. sudo systemctl restart postgresql@18-main (the guard runs again at the next boot).

Logs and files

  • PostgreSQL log: /var/log/postgresql/postgresql-18-main.log (connections, disconnections and errors, with the client address; never a password). First boot: /var/log/cloudimg-pgmq-firstboot.log; the guard: /var/log/cloudimg-pgmq-credguard.log.
  • Configuration: /etc/postgresql/18/main/postgresql.conf, with the cloudimg settings in conf.d/10-cloudimg.conf (listen address, SCRAM, connection logging) and conf.d/20-cloudimg-tls.conf (the certificate); pg_hba.conf beside them.
  • Certificate and key: /etc/cloudimg/pgmq/tls/ (the key is readable by root and PostgreSQL only). The outbound rules: /usr/local/share/cloudimg/pgmq/pgmq-egress.nft. Build provenance (versions and checksums): /usr/share/doc/cloudimg-pgmq/PROVENANCE.
  • PostgreSQL runs with core dumps off (a core dump can hold a password).

Support

cloudimg provides support for the image: launch problems, the first-boot setup, the TLS configuration and the cloudimg services. Contact support@cloudimg.co.uk. PGMQ is documented at github.com/pgmq/pgmq and PostgreSQL at postgresql.org/docs/18.