M1
Streaming & Messaging AWS

Mercure 1.1.6 Real-time Hub on AWS User Guide

| Product: Mercure 1.1.6 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Mercure 1.1.6 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.

Mercure is an open source (AGPL-3.0) real-time hub written in Go on top of the Caddy web server. Your application publishes an update to a topic with an HTTP POST, and every browser or service subscribed to that topic receives it at once over a standard server-sent events (SSE) stream, which every browser supports through EventSource. It is used for live dashboards, notifications, chat, collaborative editing and pushing API changes to clients, without running a WebSocket server yourself.

What the cloudimg image adds:

  • HTTPS only, on 443. nginx serves TLS 1.3 and TLS 1.2 (AEAD cipher suites only) on port 443 and passes requests to the hub, which listens on 127.0.0.1:3000 only. Port 80 is closed and the Caddy admin API is off (Step 11).
  • A certificate made on your instance. At first boot the instance gets a TLS key and an end-entity certificate for its own names and addresses. It can only vouch for this server and can never sign other certificates. You pin it in your clients (Step 5), or replace it with one from your own CA or Let's Encrypt (Step 13).
  • Keys generated for your instance, and a token on every request. First boot generates a publisher key and a separate subscriber key (256 random bits each) for this instance. Every publish needs a token signed with the publisher key and every subscription needs a token signed with the subscriber key, so a subscriber can never publish. There are no anonymous subscribers, no debugger and no UI playground, and a token is only accepted for this hub's own address (its audience).
  • A hub that refuses to run unsafely. Before every start a gate refuses a missing, short, placeholder or shared key, and a configuration that turns on the admin API or anonymous access, listens beyond 127.0.0.1:3000, writes a key into the Caddyfile or adds another handler. After every start a guard stops a hub that answers a tokenless or wrong-key request with anything but 401 (Step 11).
  • A sandboxed hub. The hub runs as the unprivileged user mercure with no capabilities, NoNewPrivileges, a read-only system (only /var/lib/mercure is writable), a private /tmp and no access to home directories.
  • No instance metadata. The hub, nginx and the first-boot service run with the EC2 instance metadata addresses blocked, so nothing the hub does can obtain the instance's IAM role credentials (Step 12). The hub has no telemetry.
  • Rate and size limits. nginx allows 20 requests per second per client address (bursts of 40) and 200 open connections per address (each subscriber holds one), refuses a body over 1 MB and answers nothing to a request for an unknown host name.
  • A pinned, rebuilt supply chain. The mercure binary is a cloudimg build of the Mercure project's tag v1.1.6 source, with only its Go dependency golang.org/x/crypto raised and no source change, because the project's own v1.1.6 release binary links a version of that module with a published security advisory. The build is reproducible, checked against its SHA-256 digest, and every shipped file is recorded in /usr/share/doc/mercure/PROVENANCE (Step 14). Ubuntu 24.04 LTS is fully patched at build time.

What is on the image: Mercure 1.1.6 (/usr/local/bin/mercure; its Caddyfile in /etc/mercure/Caddyfile, keys in /etc/mercure/mercure.env, state in /var/lib/mercure), nginx from Ubuntu 24.04 as the TLS front, and the cloudimg helpers mercure-mint-token, mercure-selfcheck, mercure-rotate-keys, mercure-set-hostname, mercure-install-cert, mercure-config-gate and mercure-env-check.

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

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
443 TCP the networks your publishers and subscribers (application servers, browsers) connect from The hub over HTTPS

Nothing else needs to be open. The hub's own port 3000 is bound to 127.0.0.1 and port 80 is not served, so opening them achieves nothing.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Mercure 1.1.6 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (a smaller instance is enough for a few hundred subscribers), 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=mercure}]'

The image requires IMDSv2 (session tokens). First boot takes under a minute after the instance is running: a boot service reads the instance's EC2 names, creates the keys and the certificate, then the hub and nginx start and the guard checks them.

Step 2: Connect to your instance

Version SSH user
Mercure 1.1.6 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

The login banner shows where the keys and the certificate are:

  Mercure 1.1.6 hub on Ubuntu 24.04 (cloudimg)
  Keys, endpoint, certificate fingerprint:  sudo cat /root/mercure-credentials.txt
  Hub: https://<this host>/.well-known/mercure (443 only; curl --cacert /etc/mercure/tls/cert.pem)
  Tokens: sudo mercure-mint-token publish|subscribe <topic>   Logs: journalctl -u mercure   Verify: sudo mercure-selfcheck
  User guide: https://www.cloudimg.co.uk/guides/mercure-1-1-6-on-ubuntu-24-04-aws/

Step 3: Check the services and the listening set

systemctl is-active mercure-metadata mercure-firstboot mercure nginx
sudo ss -tlnp | grep -E ':(22|80|443|2019|3000) '
active
active
active
active
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=14695,fd=5),("nginx",pid=14694,fd=5),("nginx",pid=14569,fd=5))
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*    users:(("sshd",pid=893,fd=3),("systemd",pid=1,fd=122))
LISTEN 0      4096       127.0.0.1:3000      0.0.0.0:*    users:(("mercure",pid=14880,fd=6))
LISTEN 0      511             [::]:443          [::]:*    users:(("nginx",pid=14695,fd=6),("nginx",pid=14694,fd=6),("nginx",pid=14569,fd=6))
LISTEN 0      4096            [::]:22           [::]:*    users:(("sshd",pid=893,fd=4),("systemd",pid=1,fd=123))

mercure-metadata reads this instance's EC2 names, mercure-firstboot prepares the keys and the certificate, mercure is the hub and nginx is the TLS front. Only nginx listens on the network (443, IPv4 and IPv6). The hub listens on 127.0.0.1:3000, and nothing listens on port 80 or on the Caddy admin port 2019.

Step 4: Your endpoint and keys

Everything generated for your instance is in /root/mercure-credentials.txt (readable by root only). View it all with sudo cat /root/mercure-credentials.txt. The lines that are not secret are:

sudo grep -E '^MERCURE_(HOST|HUB_URL|PRIVATE_IP|ISSUER|TLS_CA_FILE|TLS_CERT_SHA256|TLS_PUBKEY_SHA256)=' /root/mercure-credentials.txt
MERCURE_HOST=3.88.217.81
MERCURE_HUB_URL=https://3.88.217.81/.well-known/mercure
MERCURE_PRIVATE_IP=172.31.93.224
MERCURE_ISSUER=urn:cloudimg:mercure
MERCURE_TLS_CA_FILE=/etc/mercure/tls/cert.pem
MERCURE_TLS_CERT_SHA256=B0:70:65:B9:D9:91:2A:58:45:13:F3:3F:20:BD:47:17:EC:8D:29:70:F4:F2:08:5C:D7:27:8E:AA:68:6B:53:B0
MERCURE_TLS_PUBKEY_SHA256=47a7ccfe13873e4baf2fd7ffbc9927555bd6c150b0efe35b682253b1464f3973

The file also holds MERCURE_PUBLISHER_JWT_KEY and MERCURE_SUBSCRIBER_JWT_KEY, the two keys the hub checks tokens with. The hub reads the same keys from /etc/mercure/mercure.env (root:mercure 0640). Only your application backend needs them, to sign tokens (Step 8); never put a key in a browser, a mobile app or a command line.

Step 5: Check the certificate and pin it in your client

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

sudo openssl x509 -in /etc/mercure/tls/cert.pem -noout -subject -enddate -ext subjectAltName,basicConstraints,extendedKeyUsage
sudo openssl x509 -in /etc/mercure/tls/cert.pem -noout -fingerprint -sha256
subject=CN = mercure, O = cloudimg
notAfter=Feb 12 18:20:49 2029 GMT
X509v3 Subject Alternative Name:
    IP Address:3.88.217.81, DNS:ec2-3-88-217-81.compute-1.amazonaws.com, IP Address:172.31.93.224, DNS:ip-172-31-93-224.ec2.internal, IP Address:127.0.0.1, DNS:localhost
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Extended Key Usage:
    TLS Web Server Authentication
sha256 Fingerprint=B0:70:65:B9:D9:91:2A:58:45:13:F3:3F:20:BD:47:17:EC:8D:29:70:F4:F2:08:5C:D7:27:8E:AA:68:6B:53:B0

CA:FALSE means it is an end-entity certificate that cannot sign anything else. The SHA-256 fingerprint is the MERCURE_TLS_CERT_SHA256 value in your credentials file. The certificate covers the instance's public and private IPv4 addresses and EC2 DNS names, localhost and 127.0.0.1. An EC2 stop and start gives the instance a new public IP. At the next boot the certificate is re-issued for the new names with the same key, and the hub's address in the credentials file follows: clients that pin the public key (MERCURE_TLS_PUBKEY_SHA256) keep working, while clients that pin the certificate file need the new copy.

Step 6: Publish and subscribe on the instance

A token names what its holder may do and on which topics. mercure-mint-token signs one with this instance's keys (by default for the hub's public address and valid for an hour). On the instance itself the hub is https://localhost/.well-known/mercure, so the tokens below are minted for that address with --aud. They are written to header files that only you can read, so a token never appears on a command line:

umask 077
TOPIC='https://example.com/books/1'
HUB=https://localhost/.well-known/mercure
printf 'Authorization: Bearer %s\n' "$(sudo mercure-mint-token subscribe "$TOPIC" --aud "$HUB")" > ~/mercure-sub.h
printf 'Authorization: Bearer %s\n' "$(sudo mercure-mint-token publish "$TOPIC" --aud "$HUB")" > ~/mercure-pub.h
curl -sS -N -m 6 --cacert /etc/mercure/tls/cert.pem -H @"$HOME/mercure-sub.h" -G --data-urlencode "match=$TOPIC" "$HUB" > ~/mercure-stream.txt &
sleep 2
curl -sS --cacert /etc/mercure/tls/cert.pem -H @"$HOME/mercure-pub.h" --data-urlencode "topic=$TOPIC" --data-urlencode 'data={"title":"Mercure on AWS","status":"in stock"}' "$HUB"; echo
curl -sS --cacert /etc/mercure/tls/cert.pem -H @"$HOME/mercure-pub.h" --data-urlencode "topic=$TOPIC" --data-urlencode 'data={"status":"sold out"}' -d private=on "$HUB"; echo
wait
cat ~/mercure-stream.txt
urn:uuid:01a1c6b1-c0ca-76c3-b493-7ae64e199249
urn:uuid:01a1c6b1-c0d8-74fb-9f1f-8d457104e074
curl: (28) Operation timed out after 6002 milliseconds with 185 bytes received
:
id: urn:uuid:01a1c6b1-c0ca-76c3-b493-7ae64e199249
data: {"title":"Mercure on AWS","status":"in stock"}

id: urn:uuid:01a1c6b1-c0d8-74fb-9f1f-8d457104e074
data: {"status":"sold out"}

The first curl subscribes: it holds an SSE stream open on the topic named by match (an exact topic; match_urlpattern takes a URL pattern such as https://example.com/books/:id for a family of topics). Each publish returns the new update's id, and both updates arrive on the stream as an id: and a data: line. The second is private (private=on): it is delivered only to subscribers whose token grants that topic, while a public update reaches every subscriber of the topic. In a browser the subscription is new EventSource(url, {withCredentials: true}) with the token in a cookie set by your backend, or a library that sends the Authorization header.

Requests without a valid token, with a token for the other role, plain HTTP, port 80 and the hub's own port are all refused. /healthz answers without a token so a load balancer can check the instance:

TOPIC='https://example.com/books/1'
HUB=https://localhost/.well-known/mercure
curl -s -o /dev/null -w 'subscribe without a token: %{http_code}\n' --cacert /etc/mercure/tls/cert.pem -G --data-urlencode "match=$TOPIC" "$HUB"
curl -s -o /dev/null -w 'publish without a token: %{http_code}\n' --cacert /etc/mercure/tls/cert.pem --data-urlencode "topic=$TOPIC" --data-urlencode 'data=x' "$HUB"
curl -s -o /dev/null -w 'publish with a subscriber token: %{http_code}\n' --cacert /etc/mercure/tls/cert.pem -H @"$HOME/mercure-sub.h" --data-urlencode "topic=$TOPIC" --data-urlencode 'data=x' "$HUB"
curl -s -o /dev/null -w 'plain HTTP on 443: %{http_code}\n' http://localhost:443/.well-known/mercure || true
curl -s -m 5 -o /dev/null -w 'port 80: %{http_code} %{errormsg}\n' http://localhost/ || true
curl -s -m 5 -o /dev/null -w '127.0.0.1:3000 without a token: %{http_code}\n' -G --data-urlencode "match=$TOPIC" http://127.0.0.1:3000/.well-known/mercure || true
curl -s -o /dev/null -w 'healthz without a token: %{http_code}\n' --cacert /etc/mercure/tls/cert.pem https://localhost/healthz
rm -f ~/mercure-sub.h ~/mercure-pub.h ~/mercure-stream.txt
subscribe without a token: 401
publish without a token: 401
publish with a subscriber token: 401
plain HTTP on 443: 400
port 80: 000 Failed to connect to localhost port 80 after 0 ms: Couldn't connect to server
127.0.0.1:3000 without a token: 401
healthz without a token: 200

Step 7: Use the hub from another machine

Copy the certificate (it is public; the private key never leaves the instance) to the machine that publishes or subscribes, and compare its fingerprint with MERCURE_TLS_CERT_SHA256 before you trust it:

client-host$ ssh -i <your-key.pem> ubuntu@<public-ip> 'sudo cat /etc/mercure/tls/cert.pem' > mercure-cert.pem
client-host$ openssl x509 -in mercure-cert.pem -noout -fingerprint -sha256

Tokens minted on the instance without --aud are for its public address, MERCURE_HUB_URL. Mint one, copy it to a file with mode 0600 on the client machine (printf 'Authorization: Bearer %s\n' '<token>' > sub.h), then subscribe:

sudo mercure-mint-token subscribe 'https://example.com/books/1' --ttl 1h
client-host$ curl -sS -N --cacert mercure-cert.pem -H @sub.h -G --data-urlencode 'match=https://example.com/books/1' https://<public-ip>/.well-known/mercure

Give your client only this file as its CA file (--cacert for curl, verify= in Python requests, ca in Node.js), with host name checking on. Do not add it to the operating system's trust store and never turn verification off (curl -k): the client should trust this one server, so it refuses any other. A browser does not accept a self-signed certificate for EventSource, so for browser subscribers give the instance a DNS name and a certificate from a public CA (Step 13).

From another machine, a request with no token or a wrong one gets 401, plain HTTP sent to 443 gets 400, a request for a host name the instance does not serve gets no answer, port 80 and port 3000 refuse the connection even inside a security group that allows all traffic, and a client that sends more than about 40 requests at once, or more than 20 a second, gets 429.

Step 8: Sign tokens in your application

In production your application backend signs short-lived tokens itself: a publisher token for the code that publishes, and a subscriber token per user, granting only the topics that user may receive. A token is an RFC 9068 JWT signed with HS256 and carrying an RFC 9396 authorization_details claim, which uses the same match and match_urlpattern vocabulary as the subscription URL. See the claims of a token minted on the instance:

sudo mercure-mint-token subscribe 'https://example.com/books/1' --ttl 5m | cut -d. -f2 | python3 -c 'import sys,base64,json; s=sys.stdin.read().strip(); print(json.dumps(json.loads(base64.urlsafe_b64decode(s + "=" * (-len(s) % 4))), indent=1))'
{
 "aud": "https://3.88.217.81/.well-known/mercure",
 "authorization_details": [
  {
   "type": "https://mercure.rocks/authorization-detail",
   "actions": [
    "subscribe"
   ],
   "topics": [
    {
     "match": "https://example.com/books/1"
    }
   ]
  }
 ],
 "client_id": "urn:cloudimg:mercure",
 "exp": 1794335204,
 "iat": 1794334904,
 "iss": "urn:cloudimg:mercure",
 "jti": "urn:uuid:6a64424f-2e65-42b0-8b04-4ebd34f66a2b",
 "sub": "urn:uuid:7510e8e8-21e3-4460-bb33-e652e240e591"
}

iss must be MERCURE_ISSUER (urn:cloudimg:mercure), aud must be the hub address your clients use (https://<host>/.well-known/mercure) and exp must be in the future; actions is publish or subscribe. This Python example signs a publisher token with nothing but the standard library (the key is read from the credentials file here; in your backend keep it in your secret store):

sudo python3 - <<'EOF' > ~/mercure-backend.out
import base64, hashlib, hmac, json, time, uuid
creds = dict(l.split("=", 1) for l in open("/root/mercure-credentials.txt").read().splitlines() if "=" in l and not l.startswith("#"))
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
now = int(time.time())
claims = {"iss": creds["MERCURE_ISSUER"], "aud": "https://localhost/.well-known/mercure", "sub": "my-backend",
          "client_id": "my-backend", "jti": "urn:uuid:" + str(uuid.uuid4()), "iat": now, "exp": now + 300,
          "authorization_details": [{"type": "https://mercure.rocks/authorization-detail", "actions": ["publish"],
                                     "topics": [{"match": "https://example.com/books/1"}]}]}
head = b64(json.dumps({"alg": "HS256", "typ": "at+jwt"}).encode()) + "." + b64(json.dumps(claims).encode())
sig = hmac.new(creds["MERCURE_PUBLISHER_JWT_KEY"].encode(), head.encode(), hashlib.sha256).digest()
print("Authorization: Bearer " + head + "." + b64(sig))
EOF
chmod 0600 ~/mercure-backend.out
curl -sS -o /dev/null -w 'publish with the backend token: %{http_code}\n' --cacert /etc/mercure/tls/cert.pem -H @"$HOME/mercure-backend.out" --data-urlencode 'topic=https://example.com/books/1' --data-urlencode 'data=from the backend' https://localhost/.well-known/mercure
rm -f ~/mercure-backend.out
publish with the backend token: 200

Any JWT library works the same way (for example PyJWT, firebase/php-jwt or jose for Node.js). Keep subscriber tokens short-lived and scoped to what each user may see, and never sign tokens in a browser.

Step 9: Rotate the keys

mercure-rotate-keys generates two new keys, writes them to /etc/mercure/mercure.env and /root/mercure-credentials.txt and restarts the hub. Every token signed with the old keys stops working at once, so update your backend's keys straight after:

sudo mercure-rotate-keys
sudo mercure-selfcheck
mercure-rotate-keys: new publisher and subscriber keys are in /root/mercure-credentials.txt; tokens signed with the old keys are refused
SELFCHECK OK: Mercure 1.1.6 hub behind nginx 443 (TLS, certificate B0:70:65:B9:D9:91:2A:58...); private update published and received over SSE; no token / wrong-role token 401; 127.0.0.1:3000 needs a token; ports 80 and 2019 closed

To set keys of your own instead (at least 32 characters each, random, and different from each other), edit /etc/mercure/mercure.env with sudoedit and sudo systemctl restart mercure. The gate refuses a short, repeated, placeholder or shared key, and the hub then stays stopped until the file is fixed.

Step 10: Change the hub's settings

The hub's settings are in its Caddyfile, /etc/mercure/Caddyfile (root 0644). You can set the allowed browser origins (cors_origins, publish_origins), the authorization cookie name (cookie_name), the heartbeat interval (heartbeat), the write timeout and the subscriptions API (subscriptions) there, then sudo systemctl restart mercure. The gate checks the file before every start and refuses anonymous access, the debugger and playground, another transport, the admin API, a listener beyond 127.0.0.1:3000, a key written into the file (keys belong in /etc/mercure/mercure.env), * as an origin and any other handler or app. Check a change before you restart (the output names the issuers it found, or the reason it refuses the file):

sudo mercure-config-gate --env /etc/mercure/mercure.env --caddyfile /etc/mercure/Caddyfile

Step 11: The hub refuses to run unsafely

Upstream, Mercure's Caddy can serve plain HTTP on port 80, obtain its own certificates, expose the Caddy admin API on port 2019 and accept anonymous subscribers. On a cloud instance each of those exposes your updates, so this image fixes them. mercure.service runs mercure-config-gate before every start and mercure-guard after it, and nginx has its own gate, run before every start and reload. Check them, and the whole service as a client sees it:

sudo journalctl -u mercure -b --no-pager -o cat | grep -E 'mercure-(config-gate|guard): OK' | tail -n 2
sudo mercure-selfcheck
mercure-config-gate: OK (unit): keys root:mercure 0640, 2 distinct; issuers urn:cloudimg:mercure (publisher+subscriber); admin off; listen 127.0.0.1:3000; cors none (same origin)
mercure-guard: OK (start): sockets tcp:127.0.0.1:3000 only; admin 2019 closed; /healthz 200 empty; no-token and wrong-key publish/subscribe refused (401)
SELFCHECK OK: Mercure 1.1.6 hub behind nginx 443 (TLS, certificate B0:70:65:B9:D9:91:2A:58...); private update published and received over SSE; no token / wrong-role token 401; 127.0.0.1:3000 needs a token; ports 80 and 2019 closed

If a change is refused, the hub (or nginx) does not start and journalctl -u mercure (or journalctl -u nginx) shows the reason. If the running hub ever listens anywhere else or answers a request without a valid token, the guard stops it. If first boot cannot finish, neither service starts.

Step 12: The instance metadata block

The EC2 instance metadata service (169.254.169.254) hands out the instance's IAM role credentials to any process that asks. mercure, nginx and mercure-firstboot run with the metadata addresses blocked by systemd (IPAddressDeny). Only mercure-metadata, which reads the instance's names, may reach the metadata service, and it may reach nothing else:

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

Step 13: Use your own DNS name or a certificate from your CA

Point a DNS name at the instance (an Elastic IP keeps the address across stop and start), then add the name to the certificate:

sudo mercure-set-hostname <dns-name>

This re-issues the self-signed certificate with the same key to cover the name, makes https://<dns-name>/.well-known/mercure the hub address in /root/mercure-credentials.txt (the audience mercure-mint-token uses from then on) and reloads nginx. Tokens must carry the address your clients use, so update aud in your backend too.

To use a certificate from your CA (your own, or a public one such as Let's Encrypt, which browsers need for EventSource), copy the chain (server certificate first) and its unencrypted PEM key to the instance, then run:

sudo mercure-install-cert <certificate-chain.pem> <private-key.pem> <dns-name>

Port 443 then serves your certificate, and clients trust your CA instead of pinning this instance. The boot-time renewal never replaces an installed certificate. sudo mercure-set-hostname <name> goes back to a new self-signed certificate.

Step 14: Verify how Mercure was built

The Mercure project's v1.1.6 release binary is built with golang.org/x/crypto v0.58.0, which has a published security advisory (GO-2026-5932). This image therefore ships a cloudimg build of the same v1.1.6 source in which only that module is raised, to v0.58.1-0.20261009181718-2f1f834330b2. No Mercure source file is changed. Check the binary on your instance:

sha256sum /usr/local/bin/mercure
grep -E '^(source|toolchain|raised) ' /usr/share/doc/mercure/PROVENANCE
/usr/local/bin/mercure version | head -n 1
b923978b3f4aefc724ec76eb1c6ad35b116b718b6f794ed29b017879df2ef368  /usr/local/bin/mercure
source https://github.com/dunglas/mercure/archive/refs/tags/v1.1.6.tar.gz sha256 91a3116ca6c06428e6ff074a39144c76820f54f023d01c74b11423e022cb4218 (tag v1.1.6 = commit 6fc7d227fbc2c7ee8cf57c33b0caceb9bca989fc); built by cloudimg from this source
toolchain go1.27.3 linux-amd64 sha256 65efbc80fdc1fa2cd2631cad5b481777a68e8725f34e89f50fc6ac714e379b40; CGO_ENABLED=0 -trimpath -buildvcs=false -mod=readonly; upstream .goreleaser.yml tags and ldflags
raised golang.org/x/crypto v0.58.0 -> v0.58.1-0.20261009181718-2f1f834330b2 (caddy/go.mod 2b3f040c7263 -> 10496bf4a966, caddy/go.sum d9ae6e8f9f6f -> e1d347dcdb29); no source patch
Mercure.rocks 1.1.6 Caddy v2.11.8 h1:E2pv1BoZ5bJaWqFkU6QvLa21iGAX8bDzlEQG4dDQ/Vc=

/usr/share/doc/mercure/ holds the AGPL-3.0 LICENSE and COPYRIGHT exactly as published in the tag, SOURCE (where the exact source is and how this binary was built from it, as the licence's section 13 asks), the raised caddy-go.mod and caddy-go.sum and their diff, go-mod-raise.diff. The build is reproducible: following SOURCE gives byte-for-byte the binary above. cloudimg rebuilds the image when Mercure publishes a new release or when one of its modules or Go itself gets a security fix.

Logs, files and limits

  • Hub log: journalctl -u mercure (starts, refused tokens and errors; never a key or a token). nginx access and error logs: /var/log/nginx/ (paths without their query string, so topics stay out of the log). First boot: journalctl -u mercure-firstboot.
  • Configuration: /etc/mercure/Caddyfile (checked by the gate on every start). Keys: /etc/mercure/mercure.env. Certificate and key: /etc/mercure/tls/. Hub state (event history): /var/lib/mercure.
  • Limits: 1 MB request body, 20 requests a second per client address (bursts of 40), 200 connections per address.

Support

cloudimg provides support for the image: launch problems, the first-boot setup, the TLS and token configuration and the cloudimg helpers. Contact support@cloudimg.co.uk. Mercure itself is documented at mercure.rocks.