Streaming & Messaging AWS

Centrifugo 6.10.0 on AWS User Guide

| Product: Centrifugo 6.10.0 on Ubuntu 24.04 on AWS

Overview

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

Centrifugo is an open source, scalable real-time messaging server. Your users' browsers and apps keep a WebSocket connection open to Centrifugo and subscribe to channels; your backend publishes into those channels through a simple server HTTP API, and Centrifugo pushes each message to every subscriber at once. Clients prove who they are with a JSON Web Token (JWT) your backend signs, so Centrifugo never needs access to your user database. It suits chat, live comments and notifications, multiplayer and collaborative tools, live dashboards and streaming AI responses, with a backend written in any language.

What the cloudimg image adds:

  • No default login, no shared secret. The JWT signing key, the server API key, the admin password and the admin session secret are generated on your instance at first boot and written to /etc/centrifugo/config.json and to /root/centrifugo-info.txt (root only). The image ships no configuration at all.
  • HTTPS and WSS from the first boot. nginx serves Centrifugo on port 443 with a certificate generated on your instance, port 80 redirects to HTTPS and requests for host names the instance does not serve are dropped. Centrifugo itself listens on 127.0.0.1:8000 only, and its service refuses to stay up if it ever finds itself on another address.
  • Cross-site WebSocket hijacking blocked. WebSocket connections are accepted on /connection/websocket only. A browser page from an origin you have not allowed can neither open a socket nor send a request: nginx answers 403, and Centrifugo's own client.allowed_origins carries the same list. You allow your web app with one command (Step 8). Non-browser clients (backends, mobile apps) send no origin and connect normally.
  • Admin login protected. The admin web UI needs the per-instance password and its login is rate limited by nginx, on top of Centrifugo's own throttling of failed attempts.
  • Fail closed. Centrifugo and nginx start only after the boot service has written your secrets and configuration. If it cannot finish, nothing is served.
  • Hardened defaults. Centrifugo, nginx and the boot service are blocked from the instance metadata service (Centrifugo can fetch URLs you configure, such as JWKS endpoints and proxy backends, so a misconfiguration can never reach the instance's credentials), the AMI requires IMDSv2, nginx hands Centrifugo the real client address from the connection and the configured host name (never values a client sends), and Centrifugo runs under systemd sandboxing as an unprivileged user.
  • Current release. Centrifugo 6.10.0 is the upstream release binary, verified against the project's published sha256 checksum. It fixes the security advisories that affect 6.9.1 and earlier.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

What is on the image: Centrifugo 6.10.0 (the checksum-verified upstream release), nginx 1.24 (from Ubuntu 24.04), cloud-init and the AWS Systems Manager agent.

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

The Centrifugo admin web UI status page showing one node running Centrifugo 6.10.0 with a connected client and subscription

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 and TCP 443 (WebSocket clients, the server API and the admin UI) from the networks your clients and backend use, plus TCP 80 if you want plain HTTP requests redirected to HTTPS. Never open 8000: Centrifugo listens on the loopback address only.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Centrifugo 6.10.0 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=centrifugo}]'

First boot takes a few seconds after the instance is running.

Step 2: Connect to your instance

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

Step 3: Check the services

The boot service generates the secrets, the configuration and the certificate, then Centrifugo and nginx start:

systemctl is-active centrifugo nginx centrifugo-firstboot
sudo test -f /var/lib/cloudimg/centrifugo-firstboot.done && echo FIRSTBOOT_DONE

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

active
active
active
FIRSTBOOT_DONE

Only SSH and nginx listen on network ports; Centrifugo listens 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
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"))
127.0.0.1:8000 users:(("centrifugo"))
[::]:22 users:(("sshd"),("systemd"))
[::]:443 users:(("nginx"),("nginx"),("nginx"))
[::]:80 users:(("nginx"),("nginx"),("nginx"))

Step 4: Get your URL and secrets

sudo cat /root/centrifugo-info.txt

The file is readable by root only:

# Centrifugo 6.10.0 on Ubuntu 24.04 - generated on first boot by centrifugo-firstboot.service.
# Every secret below is unique to this instance; none was baked into the image. Keep this file secret.
# Admin web UI: open CENTRIFUGO_URL and log in with CENTRIFUGO_ADMIN_PASSWORD (there is no username).
# Server HTTP API: POST ${CENTRIFUGO_URL}api/<method> with the header "X-API-Key: <CENTRIFUGO_API_KEY>".
# Client connections: wss://<host>/connection/websocket with a JWT signed (HS256) by CENTRIFUGO_TOKEN_HMAC_SECRET_KEY.
# Browser apps on another origin: sudo centrifugo-allow-origin add https://your-app.example
# The certificate is self signed (minted on this instance); install your own in /etc/nginx/ssl.
CENTRIFUGO_URL=https://<public-ip>/
CENTRIFUGO_ADMIN_PASSWORD=<CENTRIFUGO_ADMIN_PASSWORD>
CENTRIFUGO_API_KEY=<CENTRIFUGO_API_KEY>
CENTRIFUGO_TOKEN_HMAC_SECRET_KEY=<CENTRIFUGO_TOKEN_HMAC_SECRET_KEY>
CENTRIFUGO_ADMIN_SECRET=<CENTRIFUGO_ADMIN_SECRET>
# Re-verify the appliance: sudo centrifugo-selfcheck    Change the served name: sudo centrifugo-set-hostname <dns-name>
# User guide: https://www.cloudimg.co.uk/guides/centrifugo-6-10-0-on-ubuntu-24-04-aws/
Value Who uses it
CENTRIFUGO_ADMIN_PASSWORD you, to log in to the admin web UI
CENTRIFUGO_API_KEY your backend, to publish through the server HTTP API
CENTRIFUGO_TOKEN_HMAC_SECRET_KEY your backend, to sign the connection JWTs it hands to clients (never give it to a client)
CENTRIFUGO_ADMIN_SECRET Centrifugo itself, to sign admin UI sessions

The URL uses 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 and the private IP address. 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. To serve Centrifugo under your own DNS name, see Step 9.

Step 5: The admin web UI

Open https://<public-ip>/ in your browser, accept the self signed certificate and log in with CENTRIFUGO_ADMIN_PASSWORD (there is no user name).

The Centrifugo admin login page

The Status page lists the running node with its version, uptime, connected clients and subscriptions (the screenshot at the top of this guide shows one client subscribed to demo:chat). Actions runs any server API command from the browser, for example a publish into a channel:

Publishing a message into the demo:chat channel from the Actions page, with the request and the OK response

or Channels, which lists the channels that currently have subscribers:

The Channels action listing demo:chat with one client

The same login works from the command line. On the instance the commands below use https://127.0.0.1/ (the instance's own names are all served); from elsewhere use your CENTRIFUGO_URL. The password is passed on standard input, never on the command line:

CF_URL=https://127.0.0.1/
sudo sed -n 's/^CENTRIFUGO_ADMIN_PASSWORD=//p' /root/centrifugo-info.txt | tr -d '\n' \
  | curl -sk -m 10 --data-urlencode password@- "${CF_URL}admin/auth" | jq -r 'keys[]'
token

Step 6: Publish with the server API

Your backend publishes with POST <CENTRIFUGO_URL>api/<method> and the header X-API-Key. Without the key the API answers 401:

CF_URL=https://127.0.0.1/
curl -sk -m 10 -o /dev/null -w '%{http_code}\n' --data '{"channel":"demo:news","data":{"text":"no key"}}' "${CF_URL}api/publish"
401

With the key, publish into a channel, read the channel history back and look at the node. The image ships a demo namespace that keeps the last 100 messages of each channel for five minutes, so demo:news has history. The key goes to curl in a configuration read from standard input, so it never appears in the process list:

CF_URL=https://127.0.0.1/
CF_KEY=$(sudo sed -n 's/^CENTRIFUGO_API_KEY=//p' /root/centrifugo-info.txt)
printf 'header = "X-API-Key: %s"\n' "${CF_KEY}" | curl -sk -m 10 -K - --data '{"channel":"demo:news","data":{"text":"Hello from the server API"}}' "${CF_URL}api/publish"; echo
printf 'header = "X-API-Key: %s"\n' "${CF_KEY}" | curl -sk -m 10 -K - --data '{"channel":"demo:news","limit":5}' "${CF_URL}api/history" | jq -c '.result.publications[] | .data'
printf 'header = "X-API-Key: %s"\n' "${CF_KEY}" | curl -sk -m 10 -K - --data '{}' "${CF_URL}api/info" | jq -c '.result.nodes[] | {name, version, uptime}'
{"result":{"offset":1,"epoch":"ikLMATso"}}
{"text":"Hello from the server API"}
{"name":"ip-172-31-88-136_8000","version":"6.10.0 OSS","uptime":555}

The full list of server API methods (publish, broadcast, subscribe, unsubscribe, disconnect, presence, history, channels, info and more) is in the Centrifugo server API documentation.

Step 7: Connect clients over WebSocket

Clients connect to wss://<public-ip>/connection/websocket (or your DNS name) with a connection JWT signed with HS256 using CENTRIFUGO_TOKEN_HMAC_SECRET_KEY. Your backend creates the token after it has authenticated the user: sub is the user ID, exp the expiry, and the optional channels claim subscribes the connection to channels on the server side. As an illustration, this generates a one hour token for user user-42 subscribed to demo:news on the instance itself:

sudo python3 -I - <<'EOF'
import base64, hashlib, hmac, json, time
key = next(l.split("=", 1)[1].strip() for l in open("/root/centrifugo-info.txt") if l.startswith("CENTRIFUGO_TOKEN_HMAC_SECRET_KEY="))
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=").decode()
head = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
body = b64(json.dumps({"sub": "user-42", "exp": int(time.time()) + 3600, "channels": ["demo:news"]}).encode())
sig = b64(hmac.new(key.encode(), f"{head}.{body}".encode(), hashlib.sha256).digest())
print(f"{head}.{body}.{sig}")
EOF
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAidXNlci00MiIsICJleHAiOiAxNzkyNDc3MjE0LCAiY2hhbm5lbHMiOiBbImRlbW86bmV3cyJdfQ.r8NL_7Ck_kdDfSuZZM9TN-4Nhw3JDKNE6cT3QXLwt5k

In a real application use a JWT library in your backend language. In the browser, the official centrifuge-js SDK connects with that token and receives every message published into the channel:

import { Centrifuge } from 'centrifuge';

const client = new Centrifuge('wss://<public-ip>/connection/websocket', {
  token: '<connection JWT from your backend>'
});
// Publications into server-side subscribed channels (the token's "channels" claim).
client.on('publication', (ctx) => console.log(ctx.channel, ctx.data));
client.connect();

Official SDKs exist for JavaScript, Dart/Flutter, Swift, Java, Python, Go and .NET. Subscribing to channels from the client side needs either a subscription token or channel namespace permissions; see channel permissions. While the instance uses its self signed certificate, browsers only connect after you have accepted the certificate once at https://<public-ip>/; install a trusted certificate for production (Step 9).

Step 8: Allow your web app's origin

Browsers send an Origin header with WebSocket connections. The instance accepts only its own https origins until you add yours, so a page on another site cannot connect on behalf of your users. A connection from a foreign origin is refused before it reaches Centrifugo:

CF_URL=https://127.0.0.1/
curl -sk -m 10 --http1.1 -o /dev/null -w '%{http_code}\n' -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
  -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
  -H 'Origin: https://attacker.example' "${CF_URL}connection/websocket"
403

Add the origin of each web app that connects (scheme, host and optional port, no path). The command updates both the nginx guard and Centrifugo's client.allowed_origins, then restarts Centrifugo; open connections reconnect automatically:

sudo centrifugo-allow-origin add https://app.<your-domain>

sudo centrifugo-allow-origin list shows the origins you have added and sudo centrifugo-allow-origin remove <origin> takes one away. The list is kept in /etc/cloudimg/centrifugo-allowed-origins.

Step 9: Your own DNS name and certificate

Point a DNS record at the instance, then tell Centrifugo to serve that name. This updates nginx, issues a new self signed certificate that names it, adds https://<name> to the allowed origins and updates the URL in the credentials file:

sudo centrifugo-set-hostname centrifugo.<your-domain>

The name is saved in /etc/cloudimg/centrifugo-hostname and survives reboots. To use a certificate from your own CA or from Let's Encrypt, replace /etc/nginx/ssl/centrifugo.crt and /etc/nginx/ssl/centrifugo.key (keep the key mode 0600) and run sudo systemctl reload nginx.

Step 10: Configuration and rotating secrets

Centrifugo reads /etc/centrifugo/config.json (owned by root, readable by the centrifugo group). Everything except the secrets:

sudo jq 'del(.client.token, .http_api, .admin.password, .admin.secret)' /etc/centrifugo/config.json
{
  "http_server": {
    "address": "127.0.0.1",
    "port": 8000
  },
  "log": {
    "level": "info"
  },
  "client": {
    "allowed_origins": [
      "https://<public-ip>",
      "https://ec2-<public-ip-dashed>.compute-1.amazonaws.com",
      "https://<private-ip>",
      "https://ip-<private-ip-dashed>.ec2.internal"
    ]
  },
  "admin": {
    "enabled": true
  },
  "channel": {
    "namespaces": [
      {
        "name": "demo",
        "history_size": 100,
        "history_ttl": "300s"
      }
    ]
  }
}

Add your own channel namespaces, proxy settings or engine (for example Redis for several nodes) as described in the configuration documentation, keep http_server.address at 127.0.0.1, check the file with sudo centrifugo checkconfig --config /etc/centrifugo/config.json and restart with sudo systemctl restart centrifugo. Manage allowed_origins with centrifugo-allow-origin rather than by hand.

To rotate a secret, write a new value with jq and restart Centrifugo, then update the credentials file. For example, a new admin password:

NEW=<new-password>
sudo jq --arg p "$NEW" '.admin.password = $p' /etc/centrifugo/config.json | sudo tee /etc/centrifugo/config.new.json >/dev/null
sudo chown root:centrifugo /etc/centrifugo/config.new.json && sudo chmod 0640 /etc/centrifugo/config.new.json
sudo mv /etc/centrifugo/config.new.json /etc/centrifugo/config.json && sudo systemctl restart centrifugo
sudo sed -i "s/^CENTRIFUGO_ADMIN_PASSWORD=.*/CENTRIFUGO_ADMIN_PASSWORD=$NEW/" /root/centrifugo-info.txt

The same works for .http_api.key (the API key) and .client.token.hmac_secret_key (the JWT signing key; tokens signed with the old key stop working).

Step 11: Security checks

centrifugo-selfcheck re-proves the whole appliance with this instance's secrets: HTTPS, the admin login, the API key, a publish and history round trip, a WebSocket connection with a JWT that receives a message published through the API, refusal of foreign origins, foreign-key tokens and other host names, the loopback-only port and the metadata block inside every service:

sudo centrifugo-selfcheck
Centrifugo appliance self-check (https://<public-ip>/)
  [ok] HTTPS admin web UI -> 200
  [ok] admin login with the per-instance password -> token
  [ok] wrong admin password refused (400)
  [ok] server API without a key / with a wrong key -> 401
  [ok] server API with the per-instance key -> 200, node version 6.10.0 OSS
  [ok] publish -> history round trip on demo:history (cloudimg-hist-1792473018281393963)
  [ok] cross-origin WebSocket upgrade -> 403 (nginx Origin guard)
  [ok] WebSocket upgrade outside /connection/websocket -> 403
  [ok] JWT (per-instance HMAC key) connects over wss; an API publication reached the subscriber (cloudimg-ws-1792473018-pkBoMKMP)
  [ok] same-origin page (https://<public-ip>) connects
  [ok] JWT signed with a foreign key refused ({"close": 3500})
  [ok] Centrifugo's own client.allowed_origins refuses a foreign Origin (403 on 127.0.0.1:8000)
  [ok] port 80 -> 301 https://<public-ip>/; unknown Host -> 444; CONNECT refused; cross-origin POST -> 403
  [ok] Centrifugo (8000) answers on 127.0.0.1 only
  [ok] instance metadata refused inside the centrifugo and nginx cgroups (loopback control 200)
SELFCHECK OK - HTTPS, per-instance secrets, WebSocket round trip, origin guard and metadata block verified.

The metadata block is a systemd IPAddressDeny rule on each service; you can see it in effect:

for u in centrifugo nginx centrifugo-firstboot; do printf '%s: ' $u; systemctl show -p IPAddressDeny --value $u.service; done
centrifugo: 169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
nginx: 169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32
centrifugo-firstboot: 169.254.169.254/32 fd00:ec2::254/128 168.63.129.16/32

Step 12: Versions, provenance and file locations

centrifugo version
sed -n '1,6p' /usr/share/doc/centrifugo/BUILD-PROVENANCE.txt
Centrifugo v6.10.0 (Go version: go1.27.2)
Centrifugo 6.10.0 - official upstream release binary (not rebuilt)
source        https://github.com/centrifugal/centrifugo/releases/download/v6.10.0/centrifugo_6.10.0_linux_amd64.tar.gz
sha256        6453d58c17f54bf2e1fed0d4e3db0b79779979b304387e89e6753408a8c6e8ba  (upstream centrifugo_6.10.0_checksums.txt)
licence       Apache License 2.0 (LICENSE from the release tarball)
version       Centrifugo v6.10.0 (Go version: go1.27.2)
Path What it is
/etc/centrifugo/config.json Centrifugo configuration and secrets (written at first boot)
/root/centrifugo-info.txt URL and secrets (root only)
/etc/nginx/sites-available/cloudimg-centrifugo the HTTPS front door; certificate and key in /etc/nginx/ssl/
/etc/cloudimg/ your served host name and added origins (when set)
/usr/share/doc/centrifugo/ LICENSE, README, CHANGELOG, BUILD-PROVENANCE.txt
journalctl -u centrifugo Centrifugo's log (also nginx, centrifugo-firstboot)

Step 13: Backup and maintenance

  • The built-in memory engine keeps channel history and presence in RAM only: a restart clears them, and there is nothing to back up except /etc/centrifugo, /etc/cloudimg and /etc/nginx/ssl. For persistence or several nodes, configure a Redis engine.
  • Ubuntu security updates install automatically (unattended-upgrades). Centrifugo is a single binary in /usr/local/bin; to upgrade, launch the newer version from the listing and move your configuration across.
  • Centrifugo is light: an m5.large holds tens of thousands of idle connections. Raise the instance size for heavy publish rates.

Step 14: Security recommendations

  • Limit TCP 443 in the security group to the networks your clients and backend use, and TCP 22 to your admin network. If only your backend needs the admin UI and server API, consider a separate private load balancer or VPN for them.
  • Serve Centrifugo under your own DNS name with a certificate from a trusted CA (Step 9) so clients can verify it.
  • Keep CENTRIFUGO_TOKEN_HMAC_SECRET_KEY and CENTRIFUGO_API_KEY on your backend only; issue short-lived connection tokens.
  • Allow only the origins of your own web apps (Step 8); never add *.
  • Keep Centrifugo on 127.0.0.1; publish nothing but nginx.

Support and licensing

cloudimg provides 24/7 technical support for this image by email and live chat: support@cloudimg.co.uk. Centrifugo is licensed under the Apache License 2.0; its LICENSE file is on the instance in /usr/share/doc/centrifugo. Centrifugo is a trademark of its respective owner; cloudimg is not affiliated with or endorsed by the Centrifugo project.