Skip to content

MQTTS, NTP, and firmware on the server

FluxMQ serves three ports: plain MQTT 3.1.1 (MQTT_PORT), plain MQTT 5 (MQTT5_PORT), and MQTTS (MQTTS_PORT, one TLS port for MQTT 3.1.1 and 5). MQTTS is optional and activates itself when the certificate and key files exist; without them only plain MQTT runs.

Put fullchain.pem and privkey.pem in ${DATA_DIR}/tls (the folder is created by data-init, mode 700, owned by root; copy with sudo).

The certificate must be for the name clients use, with the full chain (fullchain.pem). Open MQTTS_PORT in the firewall.

FluxMQ reads the certificate once at startup and does not reload it. So the container entrypoint checks the contents of the certificate and key files every MQTTS_RELOAD_CHECK_S seconds (300 by default) and restarts FluxMQ when they change. No certbot hook or Docker socket access is needed.

  • No files: MQTTS is off, plain runs. Files appear: TLS activates itself within the check interval.
  • Files replaced: FluxMQ is restarted. All MQTT connections drop for a few seconds and then reconnect by themselves.
  • A corrupt or mismatched file: the container falls back to no TLS (plain MQTT stays alive) and tries again when the files change.
Terminal window
docker compose logs fluxmq | grep run.sh # "MQTTS active" or "MQTTS inactive"
echo | openssl s_client -connect celestian.example.com:8883 -servername celestian.example.com 2>/dev/null \
| openssl x509 -noout -subject -enddate
mosquitto_pub -h celestian.example.com -p 8883 -V mqttv5 -u <ID> -P <key> -i <ID>-test \
-t m/<ws>/c/<ch>/test -m x -d # add --cafile when using your own CA

MQTTS_BIND sets the bind address of the MQTTS port only. If some clients connect from the internet and some through a VPN: MQTTS_BIND=0.0.0.0 and leave MQTT_BIND at 127.0.0.1 or the VPN IP, so only 8883 is open to the public and plain MQTT is not.

SenFlat v2 data times come from the device clock, which is synchronized to the gateway or to this server. So the server must have a correct clock and, if used as an upstream, serve NTP. Install chrony on the server host, not in Docker.

Terminal window
sudo apt install chrony
sudo cp deploy/host/chrony-celestian.conf /etc/chrony/conf.d/celestian.conf
sudo nano /etc/chrony/conf.d/celestian.conf # replace the `allow` line with the gateway address or subnet
sudo systemctl restart chrony
sudo ufw allow from <gateway-address> to any port 123 proto udp
  • Restrict allow and the firewall to the gateway. An open NTP server can be abused for amplification attacks.
  • Verify: chronyc tracking (Stratum 2–3), chronyc sources -v (a ^* source exists), chronyc serverstats (incoming packets rise). From the gateway: chronyc -h <server-address> tracking.
  • Skip it if the server already has an NTP server that serves clients; point the gateway at that address.

Firmware is uploaded in Device Management → Firmware (OTA) (guide). What needs to be prepared:

  • SECRETS_KEY is set (the signed download addresses are derived from it; without it a rollout answers 503).
  • Files live in ${DATA_DIR}/firmware. Back this folder up: a workspace backup does not contain firmware files.
  • The upload limit is FIRMWARE_MAX_BYTES (8 MiB by default). The reverse proxy must allow a body of that size (client_max_body_size); raise it if you raise the limit.
  • The public route GET /api/fw/<token>/<file> must be reachable from outside, because devices or gateways download through it. No special rule is needed if the whole web tier is already forwarded.

A node that cannot reach Celestian fetches the firmware from the gateway (a relative fw_url /fw/... means http://<gateway>:8080/fw/...). Make the gateway forward /fw/ to Celestian in the gateway’s nginx:

proxy_cache_path /var/cache/nginx/fw levels=1:2 keys_zone=fw:10m max_size=512m inactive=7d use_temp_path=off;
server {
listen 80;
location /fw/ {
# /fw/<token>/<file> → https://<celestian>/api/fw/<token>/<file>
proxy_pass https://celestian.example.com/api/fw/;
proxy_ssl_server_name on;
proxy_set_header Host celestian.example.com;
proxy_http_version 1.1;
proxy_cache fw;
proxy_cache_key $uri;
proxy_cache_valid 200 7d;
proxy_cache_valid 403 404 1m;
}
}

Test from the gateway: curl -sI http://127.0.0.1:8080/fw/abc/x.bin must answer 403 from Celestian (an invalid token), not 404 or a connection error. Rules on Device also need a gateway bridge that forwards +/rules (down) and +/rules/reported and +/rules/current (up).