Pact Broker 2.121.2 on AWS User Guide
Overview
This guide covers launching and verifying Pact Broker 2.121.2 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened Apache Subversion (SVN) & CI/CD Tools AMIs listing on AWS Marketplace; you pick it as the software version when you launch.
Pact Broker is the open source contract testing hub for the Pact ecosystem (MIT licence). Consumer test runs publish their contracts to it, providers fetch those contracts and publish their verification results back, and can-i-deploy answers the question a release pipeline needs answered: is this version safe to deploy, given everything it talks to. It keeps the history of every contract, the matrix of who verified what against which version, tags, branches and deployments, and a web interface for reading all of it. Webhooks let it start the provider build when a consumer publishes a new contract.
What is on the image:
- Pact Broker 2.121.2 from the official upstream image
pactfoundation/pact-broker:3.0.0-pactbroker2.121.2-debian, pinned by its linux/amd64 manifest digest, with one cloudimg layer on top that applies the Debian security updates (cloudimg/pact-broker:2.121.2). The container runs as an unprivileged user and systemd starts it by image ID with--pull=never, so the instance never fetches a different build - PostgreSQL 16 from the Ubuntu archive, patched by the same updates as the rest of the instance, with no TCP listener at all: the broker reaches it over a Unix socket
- Docker CE from Docker's official repository, configured so that no container can reach instance metadata
- nginx serving HTTPS on port 443 with a certificate created on your instance. There is no port 80
- Two accounts, both required: a write credential for your pipeline and a separate read-only credential for people and dashboards, each with a password created on first boot
pact-broker-set-host(serve the broker under your own DNS name) andpact-broker-selftest(an end-to-end check that publishes a contract, verifies it, askscan-i-deployboth ways and removes everything it created)
Default user: ubuntu · Architecture: x86_64 · Root volume: 20 GiB gp3
Security posture
- No default login and no anonymous access. Upstream Pact Broker runs with no authentication unless credentials are configured; on a public instance that would let anyone read and rewrite your contracts. Here every route, including the dashboard, the API, the API browser, the status badges,
/metricsand the heartbeat, answers401without a credential. First boot creates the write accountpactbrokerand the read-only accountpactbroker-readonly, each with a 128-bit random password, written only to/root/pact-broker-credentials.txt(mode 0600, root). The read-only account is refused401on every create, change and delete. - Per-instance secrets. Both broker passwords, the PostgreSQL password, the TLS key, the SSH host keys and the machine ID are created on your instance; no two instances share one. The secrets reach the container through a root-only environment file, never on a command line, and never in a log.
- The database is private. PostgreSQL listens on no TCP port, has SSL off because there is no network path for it to protect, and accepts the broker's own role only with its password over the socket. The broker's own port 9292 listens on the loopback interface only, so nginx is the only way in. From outside the instance exactly two ports answer: 22 and 443.
- HTTPS from first boot. TLS 1.2 and 1.3 only and
Strict-Transport-Security. The host name comes from configuration, never from a request: first boot reads the instance's public DNS name from EC2 instance metadata (IMDSv2) and pins nginx and the broker's base URL to it, so every link the broker returns ishttpson that name. Requests for any other host name have their connection closed. Browsers re-send cached Basic credentials, so cross-origin state-changing requests are refused403, no CORS grant is ever returned, and forgedX-Forwarded-ForandForwardedheaders never reach the broker. Requests are rate limited per client address. - Webhooks: HTTPS and POST only, refused instance metadata. A Pact Broker webhook makes an outbound request to a URL that a write-credential holder chooses. On this image webhooks may only use
httpsandPOST, and a rule in Docker'sDOCKER-USERchain, applied on every boot before any container starts (the broker does not start without it), refuses 169.254.169.254, fd00:ec2::254 and 168.63.129.16, so a webhook cannot read the instance's IAM role credentials. Webhooks can reach your VPC and the internet. To limit them, list the hosts they may call in/etc/cloudimg/pact-broker-webhook-hosts.env(Step 8) and keep the security group's outbound rules restrictive. - Verified build. The image digest was scanned for known vulnerabilities and its licences, including every Ruby gem in it, were checked before the build. SSH allows keys only, no root login and no X11 forwarding; the AMI requires IMDSv2; the Docker daemon has no TCP socket and the
dockergroup is empty.
Trusted users only. Anyone holding the write credential can publish and delete contracts and create webhooks that call out from this instance. Keep it in your CI secret store, give people the read-only credential, and keep 443 restricted to the networks your pipelines and users work from.
Prerequisites
- An AWS account and permission to launch EC2 instances
- An EC2 key pair in the Region you launch in
Recommended instance type: m5.large (2 vCPUs, 8 GiB). The broker, PostgreSQL and nginx use well under 1 GiB at rest; a busy broker with many integrations and webhooks is comfortable on this size.
Connecting to your instance
| Version | Operating system | SSH user |
|---|---|---|
| Pact Broker 2.121.2 on Ubuntu 24.04 | Ubuntu 24.04 LTS | ubuntu |
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened Apache Subversion (SVN) & CI/CD Tools AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once the subscription is active, choose Launch your software.
- Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Pact Broker 2.121.2 on Ubuntu 24.04, then choose your Region.
- Pick an instance type (
m5.largerecommended) and your key pair. - Security group: open SSH (22) to your own IP address and HTTPS (443) to the networks your CI runners and users work from. Nothing else is needed; never open 9292 or 5432. Keep the group's outbound rules as narrow as your webhooks allow (Step 8).
- Choose your VPC and subnet. Under Advanced details, leave Metadata version at V2 only (the image already requires it) and set Metadata response hop limit to
1: containers then cannot reach instance metadata even without the image's own rule. - Choose Launch.
Option B: AWS CLI
Run this on your own workstation. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and CIDRs with your own:
SG=$(aws ec2 create-security-group --group-name pact-broker \
--description "Pact Broker" --vpc-id vpc-0123456789abcdef0 \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 443 --cidr <your-mgmt-cidr>
aws ec2 run-instances \
--image-id ami-0123456789abcdef0 \
--instance-type m5.large \
--key-name my-key-pair \
--security-group-ids "$SG" \
--subnet-id subnet-0123456789abcdef0 \
--metadata-options HttpTokens=required,HttpPutResponseHopLimit=1,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=pact-broker}]'
Associate an Elastic IP if you want the address to survive a stop and start, and see Step 9 to serve the broker under your own name.
Step 2: Connect and find your login
Connect as ubuntu with the key pair you chose:
ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>
First boot takes about a minute after the instance starts. It sets the name the broker is served as (the instance's public DNS name when it has one), creates the TLS certificate and this instance's passwords, creates the broker's database and role, starts the broker (which creates its schema) and only then opens nginx. The login is in a root-only file; this shows the address, the user names and the field names without printing any password:
sudo grep -E '^PACT_BROKER_(URL|HOST|WRITE_USER|READ_ONLY_USER|DATABASE_USER)=' /root/pact-broker-credentials.txt
sudo grep -oE '^PACT_BROKER_[A-Z_]+=' /root/pact-broker-credentials.txt
sudo stat -c '%a %U:%G %n' /root/pact-broker-credentials.txt
PACT_BROKER_URL=https://ec2-203-0-113-25.compute-1.amazonaws.com
PACT_BROKER_HOST=ec2-203-0-113-25.compute-1.amazonaws.com
PACT_BROKER_WRITE_USER=pactbroker
PACT_BROKER_READ_ONLY_USER=pactbroker-readonly
PACT_BROKER_DATABASE_USER=pact_broker
PACT_BROKER_URL=
PACT_BROKER_HOST=
PACT_BROKER_WRITE_USER=
PACT_BROKER_WRITE_PASSWORD=
PACT_BROKER_READ_ONLY_USER=
PACT_BROKER_READ_ONLY_PASSWORD=
PACT_BROKER_DATABASE_USER=
PACT_BROKER_DATABASE_PASSWORD=
PACT_BROKER_TLS_CERT=
600 root:root /root/pact-broker-credentials.txt
| Value | What it is for |
|---|---|
PACT_BROKER_URL / PACT_BROKER_HOST |
The address of this instance's broker: the web interface and the API base URL |
PACT_BROKER_WRITE_USER / PACT_BROKER_WRITE_PASSWORD |
The write account: publish contracts, record verification results and deployments, create webhooks, delete. Put it in CI and nowhere else |
PACT_BROKER_READ_ONLY_USER / PACT_BROKER_READ_ONLY_PASSWORD |
The read-only account: browse the interface, read the matrix, run can-i-deploy. Give it to people and dashboards |
PACT_BROKER_DATABASE_USER / PACT_BROKER_DATABASE_PASSWORD |
The broker's PostgreSQL role, for database administration |
PACT_BROKER_TLS_CERT |
This instance's certificate, for clients that verify it |
To read the passwords, run sudo cat /root/pact-broker-credentials.txt in your own session and store them in your password manager or CI secret store.
Step 3: Confirm the broker is running
The first-boot unit has finished; PostgreSQL, Docker, the broker and nginx are active; exactly one container runs, published on the loopback interface only. The only listening sockets reachable from outside are sshd on 22 and nginx on 443:
systemctl is-active pact-broker-firstboot postgresql@16-main docker pact-broker nginx
sudo docker ps --format '{{.Names}} {{.Image}} {{.Ports}}'
sudo /usr/local/sbin/pact-broker-port-check.sh
active
active
active
active
active
pact-broker 4df3b296bfe0 127.0.0.1:9292->9292/tcp
off-box TCP exactly [22 443]; broker 9292 loopback-only; nothing on 80; no TCP 5432 anywhere
PACT_BROKER_PORTS_OK
The container's image is shown by ID because systemd starts it by ID. pact-broker-port-check.sh fails if anything else ever listens off the instance, if the broker leaves the loopback interface, or if PostgreSQL gains a TCP port. There is deliberately no port 80: every URL this broker publishes is https from the first character.
Step 4: Prove the door refuses everything but your credentials
Pact Broker has no authentication of its own unless credentials are supplied, so the question that matters most on this product is whether the instance you received refuses everyone except you. This script proves it against the real HTTPS front door:
sudo /usr/local/sbin/pact-broker-verify-auth.sh
anonymous: UI, HAL API, heartbeat, dependencies, metrics, HAL browser, badge, matrix and webhooks all REJECTED 401
guesses: weak and product-shaped passwords (and the read-only user with the write password) all REJECTED 401
write credential accepted; read-only credential reads but is REJECTED 401 on create and delete
HAL links carry https://ec2-203-0-113-25.compute-1.amazonaws.com/ and no plaintext link; x-pact-broker-version 2.121.2; TLS names ec2-203-0-113-25.compute-1.amazonaws.com and refuses an unnamed address
PACT_BROKER_AUTH_OK
It checks that the dashboard, the API, the heartbeat, /metrics, the API browser, a status badge, the matrix and the webhooks list all answer 401 without a credential; that weak and product-shaped guesses are refused; that the write credential is accepted; that the read-only credential can read and is refused when it tries to create or delete; that every link the broker returns is https on this instance's name; and that the running broker reports version 2.121.2. It prints nothing secret.
Step 5: Sign in to the web interface
Open PACT_BROKER_URL in a browser. The broker uses HTTP Basic authentication, so there is no login form: the browser asks for a username and password. Use the read-only account for day to day viewing. The certificate is created on your instance, so the browser warns about it until you install your own (Step 9).

Each row on the dashboard is an integration between a consumer and a provider. The last-verified cell is the headline: green means the provider has verified the consumer's latest contract, amber that a newer contract has not been verified yet, red that verification failed.
Step 6: Publish a contract from CI
The official pact-broker CLI, the pact-cli container and every Pact client library read the broker address and credential from three environment variables, so a pipeline needs no other configuration. Set them from your CI secret store:
PACT_BROKER_BASE_URL=https://<PACT_BROKER_HOST>
PACT_BROKER_USERNAME=pactbroker
PACT_BROKER_PASSWORD=<the write password, from your CI secret store>
To see a publish work end to end on the instance itself, this publishes a small example contract with the write credential. The credential reaches curl through a configuration read from a process substitution, so it never appears on a command line:
sudo bash -c 'set -euo pipefail
CF=/root/pact-broker-credentials.txt
H=$(sed -n "s/^PACT_BROKER_HOST=//p" $CF)
auth() { printf "user = \"%s:%s\"\n" "$(sed -n "s/^PACT_BROKER_WRITE_USER=//p" $CF)" "$(sed -n "s/^PACT_BROKER_WRITE_PASSWORD=//p" $CF)"; }
curl -sS --cacert /etc/ssl/pact-broker/pact-broker.crt --resolve "$H:443:127.0.0.1" -K <(auth) \
-o /dev/null -w "publish -> HTTP %{http_code}\n" -X PUT -H "Content-Type: application/json" \
-d "{\"consumer\":{\"name\":\"example-consumer\"},\"provider\":{\"name\":\"example-provider\"},\"interactions\":[{\"description\":\"a request for the example resource\",\"request\":{\"method\":\"GET\",\"path\":\"/example\"},\"response\":{\"status\":200}}],\"metadata\":{\"pactSpecification\":{\"version\":\"2.0.0\"}}}" \
"https://$H/pacts/provider/example-provider/consumer/example-consumer/version/1.0.0"'
publish -> HTTP 201
A 201 means the contract is stored. The consumer and the provider are created the first time they appear in a contract, so there is nothing to register in advance. The contract now shows on the dashboard. When you have finished with the example, remove it; deleting a consumer or provider removes its contracts, versions and verification results with it:
sudo bash -c 'set -euo pipefail
CF=/root/pact-broker-credentials.txt
H=$(sed -n "s/^PACT_BROKER_HOST=//p" $CF)
auth() { printf "user = \"%s:%s\"\n" "$(sed -n "s/^PACT_BROKER_WRITE_USER=//p" $CF)" "$(sed -n "s/^PACT_BROKER_WRITE_PASSWORD=//p" $CF)"; }
for n in example-consumer example-provider; do
curl -sS --cacert /etc/ssl/pact-broker/pact-broker.crt --resolve "$H:443:127.0.0.1" -K <(auth) \
-o /dev/null -w "delete $n -> HTTP %{http_code}\n" -X DELETE "https://$H/pacticipants/$n"
done'
delete example-consumer -> HTTP 204
delete example-provider -> HTTP 204
Step 7: Record a verification and ask can-i-deploy
The provider's build fetches the contract, runs it against the real provider and publishes the outcome back; can-i-deploy reads the matrix those results build up. The on-instance round trip exercises both halves and both answers:
sudo /usr/local/sbin/pact-broker-roundtrip.sh
published a consumer contract with the write credential and read it back with the read-only one
recorded a verification result and tagged the provider; can-i-deploy answered [true] for the verified version
and [null] for one whose contract was never verified; the emptiness checker REJECTED the broker while contracts were present
the broker is empty again
PACT_BROKER_ROUNDTRIP_OK
It publishes a contract with the write credential, reads it back with the read-only one, records a verification result, and asks can-i-deploy twice: once for the verified version, which is allowed (true), and once for a version whose contract was never verified, which is not (null: the broker has no verification result to allow it on). Then it deletes everything it created and confirms the broker is empty again. In your own pipeline, a deploy gate needs only the read-only credential, so the job that asks the question never holds the credential that could change the answer.
The matrix is the full picture can-i-deploy consults: which consumer version was verified against which provider version, with their branches and tags. It is reachable from the grid icon on any dashboard row:

Opening a contract shows its interactions as the provider will be asked to honour them:

Step 8: Webhooks and what they can reach
A webhook makes the broker send a request when something happens, for example a POST to your CI server when a consumer publishes a changed contract. On this image webhooks may only use https and POST, and they are refused instance metadata. They are not otherwise limited: webhooks can reach your VPC and the internet, to whatever host a write-credential holder names. Check the controls that are in force:
sudo docker inspect pact-broker --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -E '^PACT_BROKER_WEBHOOK_'
sudo /usr/local/sbin/cloudimg-docker-imds-deny --check
sudo iptables -S DOCKER-USER | grep -E '169\.254\.169\.254|168\.63\.129\.16'
grep -vE '^[[:space:]]*(#|$)' /etc/cloudimg/pact-broker-webhook-hosts.env || echo "no host allow-list set: webhooks may call any host"
PACT_BROKER_WEBHOOK_SCHEME_WHITELIST=https
PACT_BROKER_WEBHOOK_HOST_WHITELIST
PACT_BROKER_WEBHOOK_HTTP_METHOD_WHITELIST=POST
cloudimg-docker-imds-deny: all metadata rules present
-A DOCKER-USER -d 168.63.129.16/32 -j REJECT --reject-with icmp-admin-prohibited
-A DOCKER-USER -d 169.254.169.254/32 -j REJECT --reject-with icmp-admin-prohibited
no host allow-list set: webhooks may call any host
To limit webhooks to the hosts you use, set the allow-list in /etc/cloudimg/pact-broker-webhook-hosts.env and restart the broker. Names, /regular expressions/ and CIDR ranges such as 10.0.0.0/16 are accepted, separated by spaces; that one variable is the only setting the file may contain, and the broker refuses to start if it holds anything else. For example:
echo 'PACT_BROKER_WEBHOOK_HOST_WHITELIST="ci.example.com api.github.com"' | sudo tee -a /etc/cloudimg/pact-broker-webhook-hosts.env
sudo systemctl restart pact-broker
The broker then refuses to create or run a webhook for any other host. Keep the security group's outbound rules restrictive as well, so that even an allowed webhook can only reach the destinations you expect.
Step 9: Serve the broker under your own DNS name
Point a DNS record at the instance (an Elastic IP keeps the address stable), then set the name. This creates a new certificate for it, updates nginx and the broker's base URL (so every link it returns uses the new name) and restarts the broker:
sudo pact-broker-set-host pact.example.com
The name is kept across reboots. To use a certificate from your own CA or from Let's Encrypt, replace the pair after setting the name, keeping the same paths and modes, and reload nginx:
sudo install -m 0644 fullchain.pem /etc/ssl/pact-broker/pact-broker.crt
sudo install -m 0600 privkey.pem /etc/ssl/pact-broker/pact-broker.key
sudo systemctl reload nginx
Running pact-broker-set-host again creates a new self-signed certificate, so install your own pair again afterwards.
Step 10: Back up the broker
Every contract, verification result, tag, branch, deployment and webhook lives in one PostgreSQL database, so a backup is one dump over the local socket:
sudo -u postgres pg_dump -Fc pact_broker | sudo tee /var/tmp/pact-broker-backup.dump > /dev/null && sudo ls -l /var/tmp/pact-broker-backup.dump
Copy the file off the instance and keep it somewhere durable, or snapshot the root volume. The broker passwords are not in the dump: they live in /etc/pact-broker/pact-broker.env, so restoring a dump onto a new instance keeps that instance's own passwords.
Step 11: Rotate a password
Either broker password can be changed at any time. This sets a new random write password in the broker's environment file and in the credentials file, passing it through the environment so it never appears on a command line, restarts the broker and proves the gate again:
sudo bash -c 'set -euo pipefail
export NEW="$(openssl rand -hex 16)"
perl -pi -e "s/^(PACT_BROKER_BASIC_AUTH_PASSWORD=).*/\${1}\$ENV{NEW}/" /etc/pact-broker/pact-broker.env
perl -pi -e "s/^(PACT_BROKER_WRITE_PASSWORD=).*/\${1}\$ENV{NEW}/" /root/pact-broker-credentials.txt
unset NEW
systemctl restart pact-broker
/usr/local/sbin/pact-broker-wait-ready.sh >/dev/null
/usr/local/sbin/pact-broker-verify-auth.sh | tail -1'
PACT_BROKER_AUTH_OK
Update the password in your CI secret store. Rotate the read-only password the same way with PACT_BROKER_BASIC_AUTH_READ_ONLY_PASSWORD and PACT_BROKER_READ_ONLY_PASSWORD.
Maintenance
- Operating system updates: unattended security upgrades are on, and nginx, PostgreSQL and Docker are Ubuntu or Docker packages that they cover. Reboot when
/var/run/reboot-requiredexists; the metadata rule, PostgreSQL, the broker and nginx start again by themselves. - Pact Broker updates: the broker is a pinned container image, so
aptnever updates it. To move to a newer release, launch the newer version of this image when the listing offers it and restore your Step 10 backup into it. Do not rundocker image prune -a:pact-broker.servicestarts the pinned image only and never pulls. - Logs:
sudo docker logs pact-brokerfor the broker,/var/log/nginx/for web requests,sudo journalctl -u postgresql@16-mainfor the database andsudo journalctl -u pact-broker-firstbootfor first boot. - The end-to-end check:
sudo /usr/local/sbin/pact-broker-selftestchecks the services, the listening ports, the authentication gate, the contract round trip and the running container's settings, and printsPACT_BROKER_SELFTEST_OKwhen everything holds.
Troubleshooting
- The page does not load right after launch: first boot is still running.
sudo journalctl -u pact-broker-firstboot -fshows its progress; the broker is held back until first boot has finished, because a broker without credentials would have no authentication. - Browser shows "connection closed": nginx answers only the configured name and the instance's own addresses. Use
PACT_BROKER_URL, or runsudo pact-broker-set-host <name>for a new name. - Everything answers 401: that is the intended answer without a credential. Reads accept either account; writes accept only the write account.
- A write from a browser page answers 403: cross-origin state-changing requests are refused so that cached Basic credentials cannot be replayed from another site. CI clients and the
pacttools send noOriginheader and are not affected. - A webhook is refused when you create it: the URL must be
https, the methodPOST, and, if you set an allow-list, the host must be on it. - A webhook reports "No route to host" for a metadata address: that is the metadata rule working. Webhooks are for your own services.
- The broker will not start after editing the allow-list file: the file may set
PACT_BROKER_WEBHOOK_HOST_WHITELISTand nothing else, and must stay root-owned.sudo journalctl -u pact-broker -n 20shows the reason. - 502 Bad Gateway: the broker is starting (it migrates its schema at start) or has stopped.
sudo systemctl status pact-brokerandsudo docker logs --tail 50 pact-brokershow why.
Licensing
Pact Broker is licensed under the MIT License; its licence and the third-party notice for the container image are on the instance in /usr/share/doc/pact-broker/. PostgreSQL is licensed under the PostgreSQL Licence and nginx under the 2-clause BSD licence. Pact and Pact Broker are names of the Pact Foundation project; cloudimg is not affiliated with, sponsored by or endorsed by the Pact Foundation, and the names identify the software this image contains.
Support
cloudimg provides 24/7 technical support for this image by email and live chat: deployment, first login, HTTPS and DNS setup, CI and pipeline integration, webhooks and their reach, backups, upgrades and troubleshooting. Contact support@cloudimg.co.uk.