Streaming & Messaging AWS

Apache Qpid Broker-J 10.1.1 on AWS User Guide

| Product: Apache Qpid Broker-J 10.1.1 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Apache Qpid Broker-J 10.1.1 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 and launch.

Apache Qpid Broker-J is a pure Java message broker that speaks every version of AMQP: 0-8, 0-9, 0-9-1, 0-10 and 1.0. Applications built on RabbitMQ client libraries (AMQP 0-9-1), on Qpid JMS or Apache ActiveMQ Artemis clients (AMQP 1.0) and on the older Qpid clients (AMQP 0-10) can all share one broker. It keeps durable queues and messages in a Berkeley DB store, supports exchanges and bindings, priority, LVQ and sorted queues, message groups, transactions and flow control, and ships a web management console and a REST API for everything.

What the cloudimg image adds:

  • AMQP off the instance only over TLS. The only AMQP listener reachable from other machines is AMQPS on port 5671 (TLS 1.2 or 1.3). Plain AMQP on port 5672 listens on 127.0.0.1 only, for tools on the instance itself, and refuses the clear-text PLAIN mechanism: there it accepts only SCRAM-SHA-256, so a password is never sent unencrypted.
  • No default login, no shared secret. The stock Broker-J configuration ships a guest/guest user; this image ships neither that user nor an anonymous login. The admin password, the keystore password and the TLS private key are generated on your instance at first boot. The image contains only a configuration template with placeholders, and the broker will not start until first boot has filled it in.
  • Passwords stored as SCRAM-SHA-256 hashes. The broker's single authentication provider is SCRAM-SHA-256: the admin password, and every user you add, is stored only as a salted hash (32768 iterations), never in clear.
  • HTTPS from the first boot. nginx serves the web management console and the REST API on port 443, port 80 redirects to HTTPS, and requests for host names the instance does not serve are dropped. The same certificate secures AMQPS on 5671. It is an end-entity certificate: it can only vouch for this instance's own names and can never sign other certificates. Install your own CA-issued certificate with one command (Step 10); the boot-time renewal never replaces it.
  • A guarded console. A request carrying another site's Origin is refused, so is every WebSocket upgrade, console logins are rate limited, and the few management actions Broker-J performs on a plain GET request (logging out, restarting or shutting down the broker, resetting statistics and the like) only go through when the browser proves the request comes from the console itself, so a link or hidden image on another site cannot trigger them with your login. The Prometheus /metrics endpoint, open by default in Broker-J, requires a login.
  • Fail closed. The broker starts only after the boot service has written your secrets, and a configuration check refuses to start it with a placeholder, a clear-text password, a guest user, PLAIN allowed without TLS or a plaintext AMQP port on a public address.
  • Hardened defaults. The broker, nginx and the boot service are blocked from the instance metadata service (the one small service that reads the instance's own host names is allowed to reach nothing else), the AMI requires IMDSv2, nginx hands the broker the real client address and the configured host name (never values a client sends), the broker runs under systemd sandboxing as an unprivileged user, and the JVM never writes a heap dump.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Apache Qpid Broker-J 10.1.1 (the official Apache binary release, verified against its published SHA-512 digest and its release signature), OpenJDK 17 (from Ubuntu 24.04), nginx 1.24 (from Ubuntu 24.04), python3-pika 1.2.0 (an AMQP 0-9-1 client used by the self-test and the examples), cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

The Broker-J web management console Broker tab showing the default virtual host on a BDB store and the three ports: AMQP-LOCAL on 127.0.0.1:5672, AMQPS on 5671 with SSL, and HTTP on 127.0.0.1:8080

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 TCP 22 (SSH) from your admin network, TCP 5671 (AMQPS) from the networks your applications connect from and TCP 443 (the web management console and REST API) from your admin network, plus TCP 80 if you want plain HTTP requests redirected to HTTPS. Never open 5672 or 8080: they listen on the loopback address only.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Apache Qpid Broker-J 10.1.1 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, 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=qpid-broker-j}]'

First boot takes under a minute after the instance is running: the boot service generates the secrets, the certificate and the keystore and writes the broker configuration, then the broker and nginx start.

Step 2: Connect to your instance

Version SSH user
Apache Qpid Broker-J 10.1.1 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

systemctl is-active qpid-metadata-names qpid-broker-j-firstboot qpid nginx
sudo test -f /var/lib/cloudimg/qpid-broker-j-firstboot.done && echo FIRSTBOOT_DONE

All four units are active and the first boot marker is present:

active
active
active
active
FIRSTBOOT_DONE

Only SSH, nginx and AMQPS listen on network ports; plain AMQP (5672) and the broker's own management port (8080) listen on the loopback address:

sudo ss -lntp | awk 'NR>1 {print $4, $6}' | grep -v '127.0.0.5' | sed -E 's/,pid=[0-9]+,fd=[0-9]+//g' | sort
*:5671 users:(("java"))
0.0.0.0:22 users:(("sshd"),("systemd"))
0.0.0.0:443 users:(("nginx"),("nginx"),("nginx"))
0.0.0.0:80 users:(("nginx"),("nginx"),("nginx"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))
[::ffff:127.0.0.1]:5672 users:(("java"))
[::ffff:127.0.0.1]:8080 users:(("java"))

Step 4: Get your URLs, admin password and certificate fingerprint

sudo cat /root/qpid-broker-j-credentials.txt

The file is readable by root only:

# Apache Qpid Broker-J 10.1.1 on Ubuntu 24.04 - generated on first boot by qpid-broker-j-firstboot.service.
# The admin password is unique to this instance; none was baked into the image. Keep this file secret.
# The broker stores it only as a salted SCRAM-SHA-256 hash. Clients: AMQPS (TLS) on port 5671 with SCRAM-SHA-256 or
# PLAIN; the web management console and REST API on https port 443. The certificate is self signed (minted on this
# instance): check that the SHA-256 fingerprint your client sees matches QPID_TLS_CERT_SHA256 below, then pin
# /etc/cloudimg/qpid-broker.pem in that client's OWN truststore - or install a CA-issued certificate with
#   sudo qpid-install-cert <chain.pem> <key.pem>
# Change the served name: sudo qpid-broker-j-set-hostname <dns-name>      Re-verify: sudo qpid-broker-j-selftest
QPID_ADMIN_USER=admin
QPID_ADMIN_PASSWORD=<QPID_ADMIN_PASSWORD>
QPID_CONSOLE_URL=https://<public-ip>/
QPID_AMQPS_URL=amqps://<public-ip>:5671
QPID_AMQPS_HOST=<public-ip>
QPID_AMQPS_PORT=5671
QPID_VIRTUALHOST=default
QPID_TLS_CERT_SHA256=<sha256-fingerprint>
QPID_TLS_CERT_FILE=/etc/cloudimg/qpid-broker.pem
# AMQP 127.0.0.1:5672 (plain TCP, on this instance only) accepts SCRAM-SHA-256 only, never PLAIN.
# curl config with the admin login for the REST API: sudo curl -K /root/qpid-curl.conf https://<host>/api/latest/broker
# User guide: https://www.cloudimg.co.uk/guides/apache-qpid-broker-j-10-1-1-on-ubuntu-24-04-aws/

The host is the instance's public IP address from the EC2 instance metadata (the private address when the instance has no public one). The certificate also names the public DNS name, the private IP address and the private DNS name. If you stop and start the instance without an Elastic IP, the boot service picks up the new address and issues a new certificate for it (unless you installed your own, Step 10). To use your own DNS name, see Step 10.

Print the certificate's SHA-256 fingerprint and its names:

sudo openssl x509 -in /etc/cloudimg/qpid-broker.pem -noout -fingerprint -sha256
openssl x509 -in /etc/cloudimg/qpid-broker.pem -noout -ext subjectAltName,basicConstraints
sha256 Fingerprint=<sha256-fingerprint>
X509v3 Subject Alternative Name: 
    IP Address:<public-ip>, DNS:ec2-<public-ip-dashed>.compute-1.amazonaws.com, IP Address:<private-ip>, DNS:ip-<private-ip-dashed>.ec2.internal, IP Address:127.0.0.1, DNS:localhost
X509v3 Basic Constraints: critical
    CA:FALSE

How to trust this certificate. The certificate is self signed and unique to this instance. Before a client trusts it, verify the SHA-256 fingerprint that client sees against QPID_TLS_CERT_SHA256 in the credentials file (read over SSH, as above). Then pin it in that client's own truststore only (a CA file or truststore file that only this application uses, as in Step 7). Never add it to an operating-system, browser or JVM-wide trust store. For clients you cannot configure that way, install a certificate from a certificate authority instead (Step 10).

Step 5: The web management console

Open https://<public-ip>/ in your browser. It warns that the certificate is self signed: open the certificate details, check that its SHA-256 fingerprint matches the one in the credentials file, and only then continue to the site. Log in as admin with QPID_ADMIN_PASSWORD. The console logs in with SCRAM-SHA-256, so your password never crosses the wire even inside TLS.

The Broker-J console login page

The Broker tab (shown at the top of this guide) lists the virtual host default and the three ports. Double-click the virtual host to manage its exchanges, queues, connections and access control. Add Queue and Add Exchange create them; select a queue and use Clear Queue or Delete Queue:

The virtual host page listing the orders.events exchange and the invoices and orders queues with their depth

Double-click a queue for its attributes, statistics, bindings and consumers, and to browse, move or delete its messages:

The orders queue page showing 10 messages enqueued, 3 delivered, a depth of 7 and the order.created binding

Under authenticationproviders, scramProvider lists the users. Add User creates one; its password is stored as a SCRAM-SHA-256 hash:

The scramProvider authentication provider of type SCRAM-SHA-256 with the users admin and app1

Step 6: The REST API

Everything in the console is available through the REST API under /api/latest, with HTTP Basic authentication over HTTPS. Without a login it answers 401. On the instance, /root/qpid-curl.conf holds the admin login for curl (-K), so the password never appears on a command line, and --cacert pins this broker's certificate:

curl -s -o /dev/null -w '%{http_code}\n' --cacert /etc/cloudimg/qpid-broker.pem https://127.0.0.1/api/latest/broker
sudo curl -s --cacert /etc/cloudimg/qpid-broker.pem -K /root/qpid-curl.conf https://127.0.0.1/api/latest/broker \
  | jq -r '"\(.name) \(.productVersion) model \(.modelVersion)"'
sudo curl -s --cacert /etc/cloudimg/qpid-broker.pem -K /root/qpid-curl.conf "https://127.0.0.1/api/latest/port?depth=0" \
  | jq -r '.[] | "\(.name) port=\(.port) bind=\(.bindingAddress // "*") transports=\(.transports | join(","))"'
sudo curl -s --cacert /etc/cloudimg/qpid-broker.pem -K /root/qpid-curl.conf https://127.0.0.1/api/latest/authenticationprovider \
  | jq -r '.[] | "\(.name) type=\(.type) TLS-only=\(.secureOnlyMechanisms | join(","))"'
401
cloudimg 10.1.1 model 9.1
AMQPS port=5671 bind=* transports=SSL
HTTP port=8080 bind=127.0.0.1 transports=TCP
AMQP-LOCAL port=5672 bind=127.0.0.1 transports=TCP
scramProvider type=SCRAM-SHA-256 TLS-only=PLAIN

TLS-only=PLAIN means the PLAIN mechanism is offered only on TLS connections. Create a durable queue, a direct exchange and a binding between them (201 created, 200 for the bind operation):

Q=(--cacert /etc/cloudimg/qpid-broker.pem -K /root/qpid-curl.conf -s -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json')
B=https://127.0.0.1/api/latest
sudo curl "${Q[@]}" -X PUT -d '{"durable": true}' "$B/queue/default/default/orders"
sudo curl "${Q[@]}" -X PUT -d '{"type": "direct", "durable": true}' "$B/exchange/default/default/orders.events"
sudo curl "${Q[@]}" -X POST -d '{"destination": "orders", "bindingKey": "order.created"}' "$B/exchange/default/default/orders.events/bind"
201
201
200

Create an application user app1 with a password generated on the instance. The password goes into a file readable by root only and reaches the API in a request body file, never on a command line:

T=$(mktemp -d -p /dev/shm); chmod 700 "$T"
openssl rand -hex 16 | tr -d '\n' > "$T/pw"
python3 -c 'import json,sys; print(json.dumps({"name": "app1", "password": open(sys.argv[1]).read()}))' "$T/pw" > "$T/user.json"
sudo curl --cacert /etc/cloudimg/qpid-broker.pem -K /root/qpid-curl.conf -s -o /dev/null -w '%{http_code}\n' \
  -H 'Content-Type: application/json' -X POST -d @"$T/user.json" https://127.0.0.1/api/latest/user/scramProvider
sudo install -m 0600 "$T/pw" /root/qpid-app1-password.txt
rm -rf "$T"
sudo curl -s --cacert /etc/cloudimg/qpid-broker.pem -K /root/qpid-curl.conf https://127.0.0.1/api/latest/user/scramProvider | jq -r '.[].name'
201
admin
app1

Notes:

  • From another machine, use https://<public-ip>/api/latest/... with your own login (curl -u puts the password on the command line, so prefer a curl config file such as /root/qpid-curl.conf), and pin the certificate with --cacert qpid-broker.pem.
  • State-changing management calls that Broker-J accepts as a plain GET (for example /api/latest/broker/restart, /api/latest/broker/performGC, /api/latest/port/<name>/updateTLS, the BDB *Statistics?reset=true operations and /logout) need the header Sec-Fetch-Site: same-origin when you call them outside a browser. Use POST for operations wherever the API allows it.
  • HTTP Basic requests are limited to 120 a minute per client address (burst 60) and console logins to 10 a minute; a request beyond that is answered with 429.
  • nginx refuses (400) a URL whose path contains ; or an encoded /, ., \ or NUL. A queue or exchange whose name contains / cannot be addressed through the console or REST API; use other separators such as ..

To change the admin password, run the block below. It reads the new password without echoing it, sets it through the REST API (the broker stores its SCRAM-SHA-256 hash) and writes it into the credentials file and the curl config, all through standard input, never the command line:

read -rsp 'New Broker-J admin password: ' P; echo
printf '%s' "$P" | sudo python3 -I -c '
import base64, json, re, ssl, sys, urllib.request
new = sys.stdin.read()
cf = "/root/qpid-broker-j-credentials.txt"; c = open(cf).read()
old = re.search(r"(?m)^QPID_ADMIN_PASSWORD=(.*)$", c).group(1)
req = urllib.request.Request("https://127.0.0.1/api/latest/user/scramProvider/admin", method="PUT",
      data=json.dumps({"password": new}).encode(),
      headers={"Content-Type": "application/json", "Authorization": "Basic " + base64.b64encode(("admin:" + old).encode()).decode()})
urllib.request.urlopen(req, context=ssl.create_default_context(cafile="/etc/cloudimg/qpid-broker.pem"))
open(cf, "w").write(re.sub(r"(?m)^QPID_ADMIN_PASSWORD=.*$", lambda m: "QPID_ADMIN_PASSWORD=" + new, c))
k = "/root/qpid-curl.conf"; kc = open(k).read()
open(k, "w").write(re.sub(r"(?m)^user = .*$", lambda m: "user = \"admin:" + new + "\"", kc))
print("admin password changed")'
unset P

Step 7: Connect an AMQP client over AMQPS

On the instance, python3-pika is installed. This example publishes three JSON messages to the orders.events exchange and consumes them from the orders queue over AMQPS. It reads the password from the credentials file (never the command line), trusts only this broker's certificate and checks the host name:

sudo python3 - <<'PY'
import json, ssl, pika
creds = dict(l.strip().split("=", 1) for l in open("/root/qpid-broker-j-credentials.txt") if "=" in l and not l.startswith("#"))
host = creds["QPID_AMQPS_HOST"]
tls = ssl.create_default_context(cafile=creds["QPID_TLS_CERT_FILE"])  # trust THIS broker's certificate only
params = pika.ConnectionParameters("127.0.0.1", 5671, "default", pika.PlainCredentials("admin", creds["QPID_ADMIN_PASSWORD"]),
                                   ssl_options=pika.SSLOptions(tls, host))
conn = pika.BlockingConnection(params)
ch = conn.channel()
for i in range(1, 4):
    ch.basic_publish("orders.events", "order.created", json.dumps({"order": 1000 + i, "status": "CREATED"}).encode(),
                     pika.BasicProperties(content_type="application/json", delivery_mode=2))
print("published 3 messages to exchange orders.events (routing key order.created)")
while True:
    method, props, body = ch.basic_get("orders", auto_ack=True)
    if method is None:
        break
    print("consumed from orders:", body.decode())
conn.close()
PY
published 3 messages to exchange orders.events (routing key order.created)
consumed from orders: {"order": 1001, "status": "CREATED"}
consumed from orders: {"order": 1002, "status": "CREATED"}
consumed from orders: {"order": 1003, "status": "CREATED"}

The new user app1 connects over AMQPS, and the same password with PLAIN on the plaintext loopback port is refused, because the broker offers PLAIN only inside TLS:

sudo python3 - <<'PY'
import ssl, pika
creds = dict(l.strip().split("=", 1) for l in open("/root/qpid-broker-j-credentials.txt") if "=" in l and not l.startswith("#"))
host, pw = creds["QPID_AMQPS_HOST"], open("/root/qpid-app1-password.txt").read().strip()
tls = ssl.create_default_context(cafile=creds["QPID_TLS_CERT_FILE"])
conn = pika.BlockingConnection(pika.ConnectionParameters("127.0.0.1", 5671, "default", pika.PlainCredentials("app1", pw), ssl_options=pika.SSLOptions(tls, host)))
print("app1 over AMQPS 5671:", "connected" if conn.is_open else "closed"); conn.close()
try:
    pika.BlockingConnection(pika.ConnectionParameters("127.0.0.1", 5672, "default", pika.PlainCredentials("app1", pw)))
except pika.exceptions.AuthenticationError as e:
    print("app1 with PLAIN on plaintext 127.0.0.1:5672: refused (" + repr(e).split(": ", 1)[1] + ")")
PY
app1 over AMQPS 5671: connected
app1 with PLAIN on plaintext 127.0.0.1:5672: refused (Server and client could not negotiate use of the PLAIN authentication mechanism)

From your own machine. Copy the public certificate over SSH, then check its fingerprint against QPID_TLS_CERT_SHA256 from the credentials file before you trust it:

scp -i <your-key.pem> ubuntu@<public-ip>:/etc/cloudimg/qpid-broker.pem ./qpid-broker.pem
openssl x509 -in qpid-broker.pem -noout -fingerprint -sha256
openssl s_client -connect <public-ip>:5671 -servername <public-ip> </dev/null 2>/dev/null | openssl x509 -noout -fingerprint -sha256

Both fingerprints must equal QPID_TLS_CERT_SHA256. Then pin the file in the client's own trust configuration:

  • Python, AMQP 0-9-1 (pika): ssl.create_default_context(cafile="qpid-broker.pem") and pika.SSLOptions(context, "<public-ip>"), connecting to <public-ip>:5671, virtual host default, as in the example above.
  • Python, AMQP 1.0 (python-qpid-proton): SSLDomain(SSLDomain.MODE_CLIENT) with set_trusted_ca_db("qpid-broker.pem") and set_peer_authentication(SSLDomain.VERIFY_PEER_NAME), URL amqps://<public-dns-name>:5671. Use the instance's public DNS name (it is in the certificate): proton's host name check does not match IP addresses.
  • Java, Qpid JMS (AMQP 1.0): import the certificate into a truststore file used by that application only, then reference it in the connection URL. Read the truststore password from your secret store rather than typing it on the command line:
keytool -importcert -noprompt -alias qpid-broker -file qpid-broker.pem \
  -keystore qpid-client-truststore.p12 -storetype PKCS12 -storepass:env QPID_TRUSTSTORE_PASSWORD
amqps://<public-dns-name>:5671?transport.trustStoreLocation=/path/to/qpid-client-truststore.p12&transport.trustStorePassword=${QPID_TRUSTSTORE_PASSWORD}&amqp.vhost=default

Qpid JMS logs in with SCRAM-SHA-256 when the broker offers it. The truststore pins this broker's certificate for that application only; never import it into the JVM's cacerts. With a CA-issued certificate (Step 10) none of this is needed.

Step 8: Security checks

qpid-broker-j-selftest re-proves the whole appliance with this instance's credentials: the exact listening set, an AMQPS round trip with the pinned certificate, refusal of a wrong password, refusal of PLAIN on the plaintext loopback port and the mechanisms each port offers, the REST API's refusal of anonymous requests and wrong passwords, /metrics requiring a login, the redirect, refusal of other host names, cross-origin requests, WebSocket upgrades and ; path parameters, the metadata block inside the broker and nginx, that the certificate served on 443 and 5671 is this instance's end-entity certificate, and the cross-site guard on the state-changing GET routes (a same-origin request passes; cross-site, same-site, links opened from outside the browser and requests with no browser signal at all are refused):

sudo qpid-broker-j-selftest
Apache Qpid Broker-J appliance self-test (https://<public-ip>/ amqps://<public-ip>:5671)
  [ok] listening set: 22, 80, 443, 5671 public; AMQP 5672 and management 8080 on 127.0.0.1 only
  [ok] AMQPS_RT_OK queue cloudimg.selftest.84edb42e7b message cloudimg-rt-1acad857 via TLS 127.0.0.1:5671 (certificate pinned, hostname <public-ip> checked)
  [ok] AMQPS_WRONGPW_REFUSED (broker reply code 530: the authentication was refused)
  [ok] PLAIN_REFUSED_ON_5672 (the broker offers no PLAIN on a non-TLS port)
  [ok] MECHS_5672 SCRAM-SHA-256 (127.0.0.1:5672)
  [ok] MECHS_5671 PLAIN SCRAM-SHA-256 (TLS)
  [ok] HTTPS console/REST: anonymous 401, wrong password 401, admin 200 (Broker-J 10.1.1, certificate pinned); /metrics 401; console login SCRAM-SHA-256
  [ok] front door: plain HTTP -> 301 https://<public-ip>/; unknown Host -> 444; foreign Origin 403 (own origin 200); WebSocket 403; ';' 400; CONNECT refused
  [ok] instance metadata refused inside the qpid and nginx cgroups (loopback control 200)
  [ok] TLS certificate: end-entity (CA:FALSE, serverAuth, no certificate signing), names <public-ip>; served on 443 and 5671; SHA-256 <sha256-fingerprint>
  [ok] cross-site GET guard: 13/13 (/logout)
  [ok] cross-site GET guard: 13/13 (/api/latest/broker/performGC)
  [ok] cross-site GET guard: 13/13 (/api/latest/broker/compactMemory)
  [ok] cross-site GET guard: 13/13 (/api/latest/broker/getThreadStackTraces)
  [ok] cross-site GET guard: 13/13 (/api/latest/port/AMQPS/updateTLS)
  [ok] cross-site GET guard: 13/13 (/api/latest/virtualhost/default/default/environmentStatistics)
  [ok] cross-site GET guard: 13/13 (/api/latest/virtualhost/default/default/transactionStatistics)
SELFTEST OK - AMQPS round trip with the pinned certificate, wrong password and plaintext PLAIN refused, HTTPS console/REST auth wall, origin and cross-site guards, exact listening set, metadata block verified.

Step 9: The instance metadata block

The broker, nginx and the boot service are denied the instance metadata service (169.254.169.254) by a systemd IPAddressDeny rule, so nothing a client sends to the broker or the console can ever reach the instance's credentials. The only service allowed to reach the metadata service is qpid-metadata-names, which runs once at boot, reads the instance's four host names and addresses (no credentials) for the certificate, and may reach nothing else:

for u in qpid nginx qpid-broker-j-firstboot qpid-metadata-names; do printf '%s: deny=%s allow=%s\n' $u "$(systemctl show -p IPAddressDeny --value $u.service)" "$(systemctl show -p IPAddressAllow --value $u.service)"; done
qpid: deny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 allow=
nginx: deny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 allow=
qpid-broker-j-firstboot: deny=168.63.129.16/32 fd00:ec2::254/128 169.254.169.254/32 allow=
qpid-metadata-names: deny=0.0.0.0/0 ::/0 allow=fd00:ec2::254/128 169.254.169.254/32

Consequence: the broker cannot use the instance's IAM role. Broker-J does not need it for anything in this image.

Step 10: Your own DNS name and a CA-issued certificate

Point a DNS record at the instance, then tell the appliance to serve that name. This updates nginx, issues a new self signed end-entity certificate naming it (and rebuilds the broker's keystore, so AMQPS on 5671 presents it too), accepts https://<name> as the console's origin and updates the URLs and the fingerprint in the credentials file. The broker restarts to pick up the new certificate:

sudo qpid-broker-j-set-hostname qpid.<your-domain>

The name is saved in /etc/cloudimg/qpid-hostname and survives reboots. For a certificate clients trust without pinning, install one from a certificate authority (your own CA, or Let's Encrypt). The chain file holds the server certificate first, then any intermediates; the key is PEM and unencrypted:

sudo qpid-install-cert /path/to/fullchain.pem /path/to/privkey.pem qpid.<your-domain>

qpid-install-cert checks that the key matches the certificate and that the certificate covers the name, installs it for nginx (443) and rebuilds the broker's PKCS#12 keystore from it (5671) with this instance's keystore password, then restarts the broker and reloads nginx. The boot-time renewal never replaces a certificate installed this way, even if the instance's address changes. Running qpid-broker-j-set-hostname again goes back to a self signed certificate. Renew a CA certificate by running qpid-install-cert again with the new files.

Step 11: Versions, configuration and file locations

ls /opt/qpid/qpid-broker
sha256sum /opt/qpid/current/LICENSE
/usr/lib/jvm/java-17-openjdk-amd64/bin/java -version 2>&1 | head -1
grep -o 'QPID_JAVA_MEM=.*' /etc/default/qpid
10.1.1
ec0fe3860f124961a235cf05710947277a97de6c18243e867b3b18501317a3f0  /opt/qpid/current/LICENSE
openjdk version "17.0.20.1" 2026-08-18
QPID_JAVA_MEM="-Xms256m -Xmx1536m -XX:MaxDirectMemorySize=1536m"
Path What it is
/var/lib/qpid/config.json the broker's live configuration (users as SCRAM-SHA-256 hashes, ports, virtual hosts); change it through the console or REST API, not by hand
/var/lib/qpid/default/ the BDB message store of the default virtual host
/var/lib/qpid/etc/initial-config.json the configuration first boot rendered from the template (used only to create config.json)
/var/lib/qpid/etc/keystore.p12, /etc/qpid/keystore-password the broker's TLS keystore for AMQPS and its password (root only)
/root/qpid-broker-j-credentials.txt, /root/qpid-curl.conf URLs, admin password and fingerprint; curl login (root only)
/etc/cloudimg/qpid-broker.pem the public certificate for clients to pin
/etc/default/qpid Java heap and direct memory settings
/etc/nginx/sites-available/cloudimg-qpid the HTTPS front door; certificate and key in /etc/nginx/ssl/
/var/lib/qpid/log/qpid.log, journalctl -u qpid the broker's log (also nginx, qpid-broker-j-firstboot)

Step 12: Backup and maintenance

  • Back up /var/lib/qpid (configuration, users and the message store) with the broker stopped (sudo systemctl stop qpid), plus /etc/qpid, /etc/cloudimg and /etc/nginx/ssl. The keystore password in /etc/qpid/keystore-password must stay with the keystore.
  • Ubuntu security updates install automatically (unattended-upgrades), including the OpenJDK runtime. Broker-J itself lives in /opt/qpid; to upgrade, launch the newer version from the listing and move your queues and configuration across (the console's Download on the virtual host exports its configuration).
  • The broker runs with a 1.5 GB Java heap and 1.5 GB of direct memory, sized for an m5.large. On a larger instance raise QPID_JAVA_MEM in /etc/default/qpid and run sudo systemctl restart qpid.

Step 13: Security recommendations

  • Limit TCP 5671 in the security group to the networks your applications connect from, and TCP 443 and 22 to your admin network. The console can create, purge and delete queues and users; treat it as an administrative interface.
  • Give each application its own user (Step 6) instead of sharing admin, and restrict what each may do with a virtual host access control provider in the console.
  • Serve the broker under your own DNS name with a certificate from a trusted CA (Step 10). Until then, check the self signed certificate's fingerprint (Step 4) and pin it per client (Step 7) rather than adding it to a trust store.
  • Change the admin password if you share the credentials file (the block at the end of Step 6), and give other people their own users.
  • Keep plain AMQP on 127.0.0.1. If you add a port in the console, make it TLS (transport SSL with brokerKeystore).

Support and licensing

cloudimg provides 24/7 technical support for this image by email and live chat: support@cloudimg.co.uk. Apache Qpid Broker-J is licensed under the Apache License 2.0 (its LICENSE and NOTICE files are on the instance in /opt/qpid/current), OpenJDK under the GNU General Public License version 2 with the Classpath Exception, nginx under the 2-clause BSD licence and pika under the BSD 3-clause licence. Apache, Apache Qpid and Qpid are trademarks of the Apache Software Foundation; cloudimg is not affiliated with or endorsed by the Apache Software Foundation.