Streaming & Messaging AWS

deepstream.io 10.2.4 on AWS User Guide

| Product: deepstream.io 10.2.4 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the deepstream.io 10.2.4 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.

deepstream.io is an open source realtime server. Clients connect over a WebSocket and share records (JSON documents that stay in sync on every client that subscribes to them), events (publish and subscribe), RPCs (one client calls a procedure another client provides) and presence (which users are online). An HTTP API offers the same operations to servers and scripts that do not keep a connection open.

What the cloudimg image adds:

  • deepstream.io 10.2.4, the release that closes five upstream security advisories. 10.1.x and earlier could be crashed by one unauthenticated WebSocket frame, frozen with a crafted listen pattern, and let clients bypass permission rules through bulk record reads, record updates and listen responses. 10.2.4 fixes all of them.
  • TLS only. deepstream listens on 127.0.0.1:6020 and nothing else. nginx serves the WebSocket endpoint at wss://<host>/deepstream and the HTTP API at https://<host>/api on port 443; port 80 only redirects.
  • Authentication that is on, with no default account. Upstream's sample configuration accepts anyone (auth: none) and its sample user file holds the published login userA / password. This image uses file authentication only, ships an empty user list, and generates an admin login unique to your instance at first boot. Passwords are stored only as PBKDF2-HMAC-SHA512 digests (210,000 iterations, a random salt per password); the password itself never appears on a command line.
  • Least privilege for every user you add. The admin role has full access. Any other role can read public/* records, read and write its own users/<name>/* records and subscribe to public/* events, and nothing else, until you widen the rules.
  • A WebSocket Origin guard. deepstream itself accepts a WebSocket upgrade from any web page. nginx refuses an upgrade (and an HTTP API call) whose Origin is another site before it reaches deepstream, so a page on another site cannot use a visitor's browser to talk to your server. Clients that send no Origin (Node.js, Java, mobile apps, scripts) are not affected; you allow your own web application's origin with one command.
  • Exact routes, rate limits, sandboxing. deepstream matches its HTTP paths by prefix; nginx serves only the two exact paths and answers 404 for everything else, including the health check, which stays on the instance. WebSocket connections and API calls are rate limited per client address. deepstream runs as an unprivileged user with systemd sandboxing and cannot reach the instance metadata service.
  • HTTPS from the first boot. The certificate is generated on your instance for its own names and addresses. It is an end-entity certificate: it can only vouch for this instance and can never sign other certificates. Install a CA-issued certificate with one command (Step 12).
  • A pinned software supply chain. deepstream and its 136 npm dependencies are installed from a committed lockfile with every package's integrity hash checked and no install scripts run; Node.js is the official release, checked against its signed checksums. Ubuntu 24.04 LTS is fully patched at build time with unattended security upgrades on, and SSH accepts keys only.

What is on the image: deepstream.io 10.2.4 (@deepstream/server) and the official JavaScript client @deepstream/client 7.1.1 in /opt/deepstream, Node.js 26.13.0 LTS in /opt/node (also on the PATH as node and npm), nginx 1.24 (from Ubuntu 24.04), cloud-init and the AWS Systems Manager agent. No compiler toolchain is installed. deepstream keeps records in memory (no storage plugin is configured), so records live as long as the server runs.

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 TCP 22 (SSH) from your admin network and TCP 443 (WSS and HTTPS) from your clients, plus TCP 80 if you want plain HTTP requests redirected to HTTPS. There is nothing else to open: deepstream's own port (6020) listens on the loopback address only and refuses connections from other machines even if a security group allows them.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick deepstream.io 10.2.4 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large (or larger for many concurrent connections), 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=deepstream}]'

First boot takes about a minute after the instance is running: the boot service generates the admin login and the certificate, then deepstream and nginx start.

Step 2: Connect to your instance

Version SSH user
deepstream.io 10.2.4 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services and the listening set

systemctl is-active deepstream-metadata-names deepstream-firstboot deepstream nginx
sudo test -f /var/lib/cloudimg/deepstream-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 and nginx listen on network addresses; deepstream listens on 127.0.0.1:6020:

sudo ss -Hlnt | awk '{print $4}' | grep -v '^127.0.0.5' | sort
0.0.0.0:22
0.0.0.0:443
0.0.0.0:80
127.0.0.1:6020
[::]:22
[::]:443
[::]:80

Step 4: Get the URLs, the admin login and the certificate

The credentials file is readable by root only. It holds the WebSocket and HTTP API URLs, the admin user and password, and the SHA-256 fingerprint of this instance's certificate:

sudo cat /root/deepstream-credentials.txt
# deepstream.io 10.2.4 on Ubuntu 24.04 - generated on first boot by deepstream-firstboot.service.
# The admin login is unique to this instance; none was baked into the image. Keep this file secret: the admin role
# has full access to every record, event, RPC and presence. deepstream stores the password only as a PBKDF2-SHA512
# digest (/etc/deepstream/users.yml). Clients connect over TLS only:
#   WebSocket  wss://<public-ip>/deepstream      HTTP API  https://<public-ip>/api (POST, JSON)
# The certificate is self signed (minted on this instance): compare its SHA-256 fingerprint with
# DEEPSTREAM_TLS_CERT_SHA256 below and pin it in your client (Node: socketOptions.ca), or install a CA-issued
# certificate with  sudo deepstream-install-cert <chain.pem> <key.pem>
# Add users (password on stdin, stored hashed):  sudo deepstream-user add <name> <role>
# Allow a browser app's origin:  sudo deepstream-allow-origin add https://app.example.com
# Change the served name: sudo deepstream-set-hostname <dns-name>      Re-verify: sudo deepstream-selfcheck
DEEPSTREAM_URL=wss://<public-ip>/deepstream
DEEPSTREAM_HTTP_URL=https://<public-ip>/api
DEEPSTREAM_HOST=<public-ip>
DEEPSTREAM_ADMIN_USER=admin
DEEPSTREAM_ADMIN_PASSWORD=<DEEPSTREAM_ADMIN_PASSWORD>
DEEPSTREAM_TLS_CERT_SHA256=<sha256-fingerprint>
DEEPSTREAM_TLS_CERT_FILE=/etc/cloudimg/deepstream-tls.pem
# User guide: https://www.cloudimg.co.uk/guides/deepstream-io-10-2-4-on-ubuntu-24-04-aws/

The URLs, the host and the user (without the password):

sudo grep -E '^DEEPSTREAM_(URL|HTTP_URL|HOST|ADMIN_USER|TLS_CERT_FILE)=' /root/deepstream-credentials.txt
DEEPSTREAM_URL=wss://<public-ip>/deepstream
DEEPSTREAM_HTTP_URL=https://<public-ip>/api
DEEPSTREAM_HOST=<public-ip>
DEEPSTREAM_ADMIN_USER=admin
DEEPSTREAM_TLS_CERT_FILE=/etc/cloudimg/deepstream-tls.pem

To print just the password on the instance:

sudo awk -F= '$1=="DEEPSTREAM_ADMIN_PASSWORD" {print $2}' /root/deepstream-credentials.txt

The host is the instance's public IP address from the EC2 instance metadata (the private address when the instance has no public one). 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 12).

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

The certificate is self signed and unique to this instance, so no client trusts it by default; a client that has not been told about it refuses the connection. Do not add it to an operating-system or browser trust store. Instead, compare its fingerprint and then pin it in the one client that connects to this server, for that connection only.

The certificate's fingerprint, names and constraints, read on the instance:

openssl x509 -in /etc/cloudimg/deepstream-tls.pem -noout -fingerprint -sha256
openssl x509 -in /etc/cloudimg/deepstream-tls.pem -noout -ext subjectAltName,basicConstraints,keyUsage,extendedKeyUsage
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
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
    TLS Web Server Authentication

Copy the certificate file (it is public, not a secret) to the machine your client runs on and check that its fingerprint matches DEEPSTREAM_TLS_CERT_SHA256:

scp -i <your-key.pem> ubuntu@<public-ip>:/etc/cloudimg/deepstream-tls.pem ./deepstream-tls.pem
openssl x509 -in deepstream-tls.pem -noout -fingerprint -sha256

Then give that file to your client as the only certificate it trusts for this server: in Node.js the client's socketOptions: { ca } (Step 6), in other runtimes the equivalent per-connection trust setting. Browsers cannot pin a certificate for one site: for browser clients, install a CA-issued certificate for a DNS name you own (Step 12).

Step 6: Connect a Node.js client

Clients log in with a user name and password; the password should come from a file or the environment, never from the command line. This example uses the official client, logs in over wss:// with the instance certificate pinned for this connection, and then uses a record, an event, an RPC and presence. On the instance it connects to wss://localhost/deepstream (the certificate also names localhost), using the copy of the client library in /opt/deepstream:

install -d -m 0700 ~/ds-demo
( umask 077; sudo awk -F= '$1=="DEEPSTREAM_ADMIN_PASSWORD" {print $2}' /root/deepstream-credentials.txt > ~/ds-demo/admin.pw )
cat > ~/ds-demo/app.js <<'EOF'
// deepstream.io client demo: log in over wss:// with this instance's certificate pinned for this connection only,
// then use records, events, RPC and presence. The password is read from a file, never the command line.
const fs = require('fs')
const { DeepstreamClient } = require('@deepstream/client')
const url = process.env.DEEPSTREAM_URL || 'wss://localhost/deepstream'
const ca = fs.readFileSync(process.env.DEEPSTREAM_CA_FILE || '/etc/cloudimg/deepstream-tls.pem')
const password = fs.readFileSync(process.env.DEEPSTREAM_PASSWORD_FILE, 'utf8').trim()
const client = new DeepstreamClient(url, { socketOptions: { ca } })
client.on('error', (err, event) => console.error('deepstream error:', event, err))
client.login({ username: process.env.DEEPSTREAM_USER || 'admin', password }, async (ok, data) => {
  if (!ok) { console.error('login refused:', JSON.stringify(data)); process.exit(1) }
  console.log('logged in as', process.env.DEEPSTREAM_USER || 'admin', JSON.stringify(data))
  // Records: synchronised JSON documents
  const record = client.record.getRecord('demo/settings')
  await record.whenReady()
  await record.setWithAck({ theme: 'dark', updated: new Date().toISOString().slice(0, 10) })
  console.log('record demo/settings =', JSON.stringify(record.get()))
  // Events: publish / subscribe
  const got = new Promise((resolve) => client.event.subscribe('demo/news', resolve))
  client.event.emit('demo/news', { headline: 'hello from deepstream' })
  console.log('event demo/news received', JSON.stringify(await got))
  // RPC: request / response
  client.rpc.provide('demo/add', (data, response) => response.send(data.a + data.b))
  await new Promise((resolve) => setTimeout(resolve, 500))   // let the provider registration reach the server
  console.log('rpc demo/add(2, 3) =', await client.rpc.make('demo/add', { a: 2, b: 3 }))
  // Presence: who is online
  client.presence.getAll(['admin'], (err, users) => {
    console.log('presence', JSON.stringify(users))
    client.rpc.unprovide('demo/add'); record.discard(); client.close()
  })
})
EOF
cd ~/ds-demo && DEEPSTREAM_PASSWORD_FILE=~/ds-demo/admin.pw NODE_PATH=/opt/deepstream/node_modules node app.js
logged in as admin {"role":"admin"}
record demo/settings = {"theme":"dark","updated":"2026-10-29"}
event demo/news received {"headline":"hello from deepstream"}
rpc demo/add(2, 3) = 5
presence {"admin":true}

From your own machine, install the same client (npm install @deepstream/client@7.1.1), save the password in a file only you can read (chmod 600), and run the same program against the public URL with the certificate you copied in Step 5:

DEEPSTREAM_URL=wss://<public-ip>/deepstream DEEPSTREAM_CA_FILE=./deepstream-tls.pem DEEPSTREAM_PASSWORD_FILE=./admin.pw node app.js

A wrong password is refused with INVALID_AUTHENTICATION_DETAILS, a missing user name or password with missing authentication parameter. Each connection may try to log in at most three times and is closed after 20 seconds without a login.

Step 7: Records, events, RPC and presence

  • Records (client.record.getRecord(name)) are JSON documents. Every client subscribed to a record receives each change; setWithAck resolves once the server accepted the write, and client.record.snapshot(name) reads the current value once. Records are kept in deepstream's memory: configure one of deepstream's storage connectors (in /etc/deepstream/config.yml) if they must survive a restart.
  • Events (client.event.subscribe / emit) are fire-and-forget messages to every subscriber of a name.
  • RPCs (client.rpc.provide / make) route a request to one client that provides the procedure and return its answer.
  • Presence (client.presence.getAll, subscribe) reports which user names are connected.

Names are free-form strings; the / separated prefixes are what the permission rules match (Step 9).

Step 8: The HTTP API

The HTTP API runs the same operations without a WebSocket: POST a JSON body with authData and a list of operations to https://<host>/api. Write the body to a file so the password never appears on a command line, and pin the certificate with --cacert:

cd ~/ds-demo
( umask 077
  printf '{"authData":{"username":"admin","password":"%s"},"body":[{"topic":"record","action":"write","recordName":"demo/http","data":{"source":"http api"}}]}' "$(cat ~/ds-demo/admin.pw)" > write.json
  printf '{"authData":{"username":"admin","password":"%s"},"body":[{"topic":"record","action":"read","recordName":"demo/http"},{"topic":"event","action":"emit","eventName":"demo/news","data":{"headline":"via http"}}]}' "$(cat ~/ds-demo/admin.pw)" > read.json )
curl -s --cacert /etc/cloudimg/deepstream-tls.pem -H 'Content-Type: application/json' --data-binary @write.json https://localhost/api; echo
curl -s --cacert /etc/cloudimg/deepstream-tls.pem -H 'Content-Type: application/json' --data-binary @read.json https://localhost/api; echo
{"result":"SUCCESS","body":[{"success":true}]}

{"result":"SUCCESS","body":[{"version":1,"data":{"source":"http api"},"success":true},{"success":true}]}

Send each write and the read that depends on it as separate requests: operations in one request run in parallel. Without valid authData the API answers 401:

printf '{"body":[{"topic":"record","action":"read","recordName":"demo/http"}]}' > ~/ds-demo/anon.json
curl -s -w ' (HTTP %{http_code})\n' --cacert /etc/cloudimg/deepstream-tls.pem -H 'Content-Type: application/json' --data-binary @"$HOME/ds-demo/anon.json" https://localhost/api
Unsuccessful authentication attempt.

 (HTTP 401)

The API accepts only POST with a Content-Type: application/json body (other methods 405, other body types 415), and a browser request from another site is refused 403 (Step 10). From another machine use DEEPSTREAM_HTTP_URL from the credentials file with --cacert ./deepstream-tls.pem.

Step 9: Add users and scope their permissions

Users live in /etc/deepstream/users.yml, each with a PBKDF2 digest of its password and a role. Manage them with deepstream-user, which reads the password on standard input (never the command line), stores only the digest and lists names and roles only. deepstream reads the user list at start, so restart it afterwards:

( umask 077; openssl rand -hex 16 > alice.pw )
sudo deepstream-user add alice app < alice.pw
sudo systemctl restart deepstream
sudo deepstream-user list
deepstream-user: added alice (role app); restart deepstream to apply
admin role=admin
alice role=app

sudo deepstream-user remove <name> removes a user (it refuses to remove the last admin). The role decides what a user may do. /etc/deepstream/permissions.yml gives the admin role full access and every other role least privilege:

sudo sed -n '/^record:/,/^event:/p' /etc/deepstream/permissions.yml
record:
  "*":
    create: "user.data.role === 'admin'"
    write: "user.data.role === 'admin'"
    read: "user.data.role === 'admin'"
    delete: "user.data.role === 'admin'"
    listen: "user.data.role === 'admin'"
    notify: "user.data.role === 'admin'"
  "public/*":
    create: "user.data.role === 'admin'"
    write: "user.data.role === 'admin'"
    read: true
    delete: "user.data.role === 'admin'"
    listen: "user.data.role === 'admin'"
    notify: "user.data.role === 'admin'"
  "users/$owner/*":
    create: "user.id === $owner || user.data.role === 'admin'"
    write: "user.id === $owner || user.data.role === 'admin'"
    read: "user.id === $owner || user.data.role === 'admin'"
    delete: "user.id === $owner || user.data.role === 'admin'"
    listen: "user.data.role === 'admin'"
    notify: "user.data.role === 'admin'"
event:

So alice (role app) may write users/alice/... and read public/..., and everything else is denied. Logged in as alice with the same client as in Step 6 (an admin had written public/notice first):

write users/alice/profile -> ok
read public/notice -> {"text":"maintenance at 22:00"}
write public/notice -> MESSAGE_DENIED
read demo/settings -> MESSAGE_DENIED
rpc demo/add -> MESSAGE_DENIED

To grant more, add a rule for the name prefix your application uses (the most specific matching pattern decides), for example a chat/* record prefix every app user may read and write:

  "chat/*":
    create: "user.data.role === 'app' || user.data.role === 'admin'"
    write: "user.data.role === 'app' || user.data.role === 'admin'"
    read: "user.data.role === 'app' || user.data.role === 'admin'"
    delete: "user.data.role === 'admin'"
    listen: "user.data.role === 'admin'"
    notify: "user.data.role === 'admin'"

Edit the file with sudoedit /etc/deepstream/permissions.yml, then sudo systemctl restart deepstream. Rules are deepstream Valve expressions: user.id is the user name and user.data its serverData from users.yml; see the deepstream documentation for the data, oldData and path variables they can use.

Step 10: Browser applications and the Origin guard

A browser sends an Origin header with every WebSocket upgrade. nginx lets an upgrade through when there is no Origin (a server-side or mobile client) or when it is one of this instance's own https:// origins, and refuses any other site with 403 before deepstream sees it:

H="$(sudo sed -n 's/^DEEPSTREAM_HOST=//p' /root/deepstream-credentials.txt)"
ws() { curl -s -m 3 -o /dev/null -w '%{http_code}\n' --cacert /etc/cloudimg/deepstream-tls.pem --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' --resolve "$H:443:127.0.0.1" "$@" "https://$H/deepstream"; }
echo "no Origin (a server-side client): $(ws)"
echo "Origin https://$H (this instance): $(ws -H "Origin: https://$H")"
echo "Origin https://evil.example (another site): $(ws -H 'Origin: https://evil.example')"
no Origin (a server-side client): 101
Origin https://<public-ip> (this instance): 101
Origin https://evil.example (another site): 403

(101 is the WebSocket upgrade.) If your web application is served from its own site, allow its origin; the change is saved and applied at once:

sudo deepstream-allow-origin add https://app.example.com
sudo deepstream-allow-origin list
sudo deepstream-allow-origin remove https://app.example.com

Browsers do not let a page pin a certificate, so browser clients need a CA-issued certificate for a DNS name (Step 12).

Step 11: What the front door refuses

nginx serves exactly two paths. deepstream itself matches its HTTP paths by prefix (/apiX and /api/x would reach its API); nginx answers 404 for every other path, and deepstream's health check is reachable on the instance only (http://127.0.0.1:6020/health-check):

cd ~/ds-demo
for p in /health-check /apiX /api/x; do echo "$p -> $(curl -s -o /dev/null -w '%{http_code}' --cacert /etc/cloudimg/deepstream-tls.pem -H 'Content-Type: application/json' --data-binary @write.json https://localhost$p)"; done
echo "http://localhost/deepstream -> $(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' http://localhost/deepstream)"
echo "health check on the instance -> $(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:6020/health-check)"
/health-check -> 404
/apiX -> 404
/api/x -> 404
http://localhost/deepstream -> 301 https://<public-ip>/deepstream
health check on the instance -> 200

Also refused: requests for a host name the instance does not serve (the connection is dropped), ; path parameters (400), and, per client address, more than 60 new WebSocket connections a minute (a burst of 40 is allowed) or 120 HTTP API calls a minute (burst 60), which get 429. The limits are in /etc/nginx/sites-available/cloudimg-deepstream (limit_req_zone) if your clients connect through one shared address.

Step 12: Use your own domain and a CA-issued certificate

Point a DNS name at the instance (an Elastic IP keeps the address stable), then make it the served name; this issues a new self-signed certificate for it:

sudo deepstream-set-hostname deepstream.example.com

To serve a certificate from your CA (or Let's Encrypt) for that name, which browsers and clients trust without pinning, install the chain (server certificate first) and its unencrypted private key:

sudo deepstream-install-cert /path/to/fullchain.pem /path/to/privkey.pem deepstream.example.com

The boot-time renewal never replaces a certificate you installed; sudo deepstream-set-hostname <name> goes back to a self-signed one. Both commands update the URLs and the fingerprint in the credentials file.

Step 13: The instance metadata block

deepstream, nginx and the boot service may not connect to the EC2 instance metadata service (169.254.169.254) or its IPv6 and IPv4-mapped forms. Only a small boot unit reads the instance's own names and addresses for the certificate, before deepstream starts:

for u in deepstream nginx deepstream-firstboot; do printf '%s: ' "$u"; systemctl show -p IPAddressDeny --value "$u" | tr ' ' '\n' | sort | tr '\n' ' '; echo; done
deepstream: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 
nginx: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128 
deepstream-firstboot: 168.63.129.16/32 169.254.169.254/32 ::ffff:168.63.129.16/128 ::ffff:169.254.169.254/128 fd00:ec2::254/128

The self-check below proves it from inside deepstream's own service: a connection to the metadata service from there never completes, while a loopback connection does. deepstream also runs as the unprivileged deepstream user, cannot read /root, the TLS key or the boot state, and can write only its empty state directory.

Step 14: Re-verify the appliance

deepstream-selfcheck re-checks the whole appliance: the units and files, the certificate, the front door (redirect, Origin guard, refusals, exact routes), the HTTP API, the official client over wss:// with the certificate pinned (the refusal matrix and a record, event, RPC and presence round trip), the listening set and the metadata block. It reads the admin password from the credentials file, never from a command line:

sudo deepstream-selfcheck
  ok  units active; credentials file, TLS key and deepstream config present with their permissions; the config gate accepts the configuration
  ok  TLS: the served certificate is this instance's end-entity certificate (CA:FALSE, serverAuth; SHA-256 <fingerprint>...)
  ok  front door: TLS only (80 -> 301), unknown Host dropped; WebSocket no-Origin 101, own Origin 101, foreign / null / http Origin 403 (before the upgrade), non-WebSocket 426, upgrade elsewhere 403; HTTP API anonymous / userA / wrong password 401 (deepstream), admin 200 (a record written, read back and deleted), foreign Origin 403, text/plain and form 415, GET / OPTIONS / PUT 405, ';' 400; exact routes only: /apiX /api/ /api/x /api/auth /deepstreamX /deepstream/ /health-check /health-checkX / 404; CORS stripped
       admin login accepted over wss://<public-ip>/deepstream (clientData role admin; positive control)
       anonymous refused: {"error":"missing authentication parameter: username or/and password"}
       missing password refused: {"error":"missing authentication parameter: username or/and password"}
       upstream sample userA/"password" refused: "INVALID_AUTHENTICATION_DETAILS"
       wrong password refused: "INVALID_AUTHENTICATION_DETAILS"
       unknown user refused: "INVALID_AUTHENTICATION_DETAILS"
       event: connection B -> deepstream -> connection A (nonce matched)
       record: update by B delivered to A's subscription (nonce matched)
       RPC: A called the procedure B provides and got B's answer
       presence: admin reported online
  ok  client (@deepstream/client 7.1.1, certificate pinned for this connection): refusal matrix exact, admin login accepted; event, record, RPC and presence delivered between two connections
  ok  public listeners 22/80/443 only; deepstream on 127.0.0.1:6020 only (health check answers there)
  ok  from inside deepstream.service's cgroup: 169.254.169.254 unreachable (curl v4 and v4-mapped; Node.js timeout), loopback control 200
SELFCHECK OK

Step 15: Logs and service management

deepstream logs to the journal without colour codes. It logs connections and logins, never the data a client logged in with:

sudo journalctl -u deepstream -n 6 -o cat --no-pager
CLIENT_DISCONNECTED | admin
INCOMING_CONNECTION | from undefined (127.0.0.1)
AUTH_SUCCESSFUL | admin
CLIENT_DISCONNECTED | admin
INCOMING_CONNECTION | from undefined (::1)
CLIENT_DISCONNECTED | null

The configuration is /etc/deepstream/config.yml. Before deepstream starts, deepstream-config-gate checks it and refuses to start the server (the reason is in journalctl -u deepstream) if it would weaken the appliance: authentication other than hashed file authentication, a user without a digest, a bind other than 127.0.0.1:6020, another connection endpoint (text or JSON WebSocket, MQTT), logging of failed login data, the HTTP token endpoint, telemetry, a plugin directory, or ${...} environment expansion.

sudo systemctl restart deepstream
sudo systemctl status deepstream

Security notes

  • Keep /root/deepstream-credentials.txt secret and give each application its own user with the narrowest role that works (Step 9); the admin role can read and change everything.
  • deepstream stores records in memory only on this image; anything clients write is lost when deepstream restarts unless you configure a storage connector.
  • Restrict the security group's port 443 to the networks your clients connect from, and port 22 to your admin network.

What changed since the Azure image (deepstream.io 10.1.4)

The cloudimg Azure Marketplace image of deepstream.io ships 10.1.4 behind plain HTTP on port 80 with md5 password digests at 100 iterations. This AWS image runs 10.2.4 (which fixes GHSA-rv32-v8hf-rxqg, GHSA-9r5v-rrr7-v476, GHSA-g4qj-f7jp-76c6, GHSA-v448-hpx6-fm45 and GHSA-j55p-v9rg-r9f2), serves only WSS and HTTPS, hashes with PBKDF2-SHA512 at 210,000 iterations, refuses foreign-origin WebSocket upgrades, gives non-admin users least privilege and blocks the instance metadata service.

Support

cloudimg provides support for this image by email at support@cloudimg.co.uk. deepstream.io is open source software under the MIT licence; this image is not affiliated with or endorsed by the deepstream.io project.