(fix) require authentication on the MQTT broker and stop exposing it on 0.0.0.0 - #222
Conversation
Follow-up: an incident review found this broker was actively abusedA read-only forensic review of one deployed server found the broker had been reached from the internet and exploited — ~112 MB of live bot traffic ( The original three commits hardened the broker but left three real gaps. Four more commits close them.
|
pub/sub hbot/#, hummingbot-api/response/# |
allowed — API reconnects and serves normally |
#, +/#, $SYS/# |
denied |
| arbitrary topics | denied |
ssrf/probe |
denied — the backdoor's trigger topic no longer fires |
cd350f8 — make emqx-audit
The broker was hardened without first checking whether something was already living in it. This prints listeners, published ports, auth, authorization, the ACL, and any rules/actions/connectors/bridges in full, including target URLs and Authorization headers.
Verified in both directions: a clean broker reports none; a broker with a replica of the reported backdoor prints the credential, target and trigger under a REVIEW banner. On a stock broker it also surfaces authentication = [] and the 0.0.0.0 ssl/ws listeners. EMQX_CONTAINER=<name> audits another deployment.
Worth noting how cheap the backdoor was to install in testing: three API calls, authenticated with nothing but admin/public.
Note on the firewall approach
The review recommends moving port rules from INPUT to DOCKER-USER, since published container ports traverse FORWARD/DOCKER-USER and INPUT rules silently do nothing. Correct — but this PR sidesteps it for these ports by unpublishing them rather than filtering them. Docker never creates a DNAT rule for a port it doesn't publish, so there is no chain to get wrong. Unpublish beats firewall wherever the port isn't genuinely needed.
Still outside this repo
Deleting the live backdoor (rule 4715e59c / connector hbapi_ssrf), the host firewall, CUPS, authorized_keys, and exchange key rotation are all server-side. Nothing in this PR touches the compromised host.
Greptile SummaryThe PR secures EMQX by restricting published ports, requiring broker authentication, applying deny-by-default topic authorization, and provisioning credentials for managed bots. It also separates dashboard credentials, adds reset and audit operations, strengthens fresh-install defaults, and updates deployment diagnostics and documentation.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported issues are addressed by the current setup pipeline handling and bootstrap credential validation.
|
| Filename | Overview |
|---|---|
| Makefile | Generates the validated EMQX bootstrap file and adds broker reset and audit operations; the previously reported CSV field-corruption path is addressed. |
| setup.sh | Generates separate alphanumeric MQTT and dashboard credentials without allowing the expected pipe SIGPIPE to abort setup; the previously reported setup failure is addressed. |
| docker-compose.yml | Enables EMQX authentication and authorization, mounts bootstrap and ACL files, separates dashboard credentials, and narrows published service bindings. |
| emqx/acl.conf | Introduces deny-by-default authorization while allowing the shared account only on required Hummingbot topic prefixes. |
| services/docker_service.py | Seeds managed bot MQTT configurations with the broker credentials required by the new authentication boundary. |
| doctor.sh | Updates bind diagnostics for the renamed API setting and recognizes direct Tailscale-IP binding. |
| README.md | Documents the secured broker topology, credential separation, rotation procedure, audit command, and loopback defaults. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Env[".env credentials"] --> Bootstrap["make emqx-auth"]
Bootstrap --> CSV["EMQX bootstrap CSV"]
CSV --> Broker["EMQX broker"]
Env --> API["Hummingbot API"]
API --> BotConfig["Managed bot conf_client.yml"]
BotConfig --> Bots["Bot containers"]
API -->|"Authenticated MQTT"| Broker
Bots -->|"Authenticated MQTT via loopback"| Broker
ACL["Deny-by-default ACL"] --> Broker
Reviews (2): Last reviewed commit: "(fix) require authentication on the MQTT..." | Re-trigger Greptile
…on 0.0.0.0 The EMQX broker accepted anonymous connections on 0.0.0.0, and a forensic review of one deployed server found it had been reached from the internet and exploited: ~112 MB of live bot traffic exfiltrated, plus a rule-engine backdoor issuing authenticated requests into the internal API. - EMQX requires password_based auth (bootstrap file from `make emqx-auth`, seeded from BROKER_USERNAME/BROKER_PASSWORD, is_superuser=false), and denies every topic outside hbot/# and hummingbot-api/response/# via a deny-by-default ACL (emqx/acl.conf) instead of the stock allow-all one. - EMQX, hummingbot-api and Postgres all bind to 127.0.0.1 by default (API_BIND / DB_BIND override the API and Postgres binds; the EMQX ports that aren't used by this project are dropped entirely rather than just loopback-bound). - Bot instances get their own MQTT credentials seeded into conf_client.yml instead of connecting with the empty defaults the broker now rejects. - BROKER_DASHBOARD_PASSWORD is a separate credential from BROKER_PASSWORD: the latter is distributed to every bot instance, so reusing it for the EMQX dashboard would mean a single leaked bot config grants full broker admin (rules, connectors) rather than just the scoped MQTT access the ACL intends. The dashboard's well-known admin/public default is gone either way, and setup.sh no longer writes the literal string "password" as a fresh-install default for either credential. - `make emqx-audit` prints the broker's listeners, auth, authorization, ACL and any rule-engine rules/connectors/bridges in full — a rule nobody added is exactly how the backdoor above worked, and it survives restarts in cluster.hocon. `make emqx-auth-reset` rotates credentials by wiping the EMQX state volume (fixed to match Compose's actual, lowercased project name when filtering which volume to remove). - doctor.sh and the README are updated for the API_BIND rename this introduces, including validating it against Tailscale's 100.64.0.0/10 range rather than accepting any non-loopback value as the documented tailscale-IP override. Verified against a live broker: anonymous subscribe/publish is refused, a credentialed client connects normally, `docker port` shows only the loopback binds, and the dashboard's old default credentials no longer work. The Tailscale sidecar's TS_SERVE_CONFIG forward (tailnet IP and MagicDNS both reaching a loopback-bound port with no manual `tailscale serve` call) was verified directly against a real tailnet. Upgrading an existing deployment: EMQX only imports the bootstrap file and dashboard password for accounts/state it doesn't already have, so changing BROKER_PASSWORD or BROKER_DASHBOARD_PASSWORD in .env does nothing until `make emqx-auth-reset` recreates the state volume. Bot instances with empty MQTT credentials baked into conf_client.yml need redeploying to reconnect. Deleting the live backdoor and rotating exchange keys on an already-compromised host is outside this repo. Co-Authored-By: david-hummingbot <michaeld@hummingbot.org> Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
f9b9125 to
45448a0
Compare
Problem
The EMQX broker was accepting anonymous connections, published on
0.0.0.0.emqx ctl conf show authenticationreturnedauthentication = []. EMQX ships that way and nothing in the compose file overrode it, so theBROKER_USERNAME/BROKER_PASSWORDpair in.envwas never actually enforced — it was only ever passed by our own clients, never checked by the broker.Anyone able to reach port 1883 could:
subscribe hbot/#— read every bot's logs and performance, and enumerate instance IDspublish hbot/<instance_id>/stop— halt a live trading botNo credential required at any step. Seven ports were published on
0.0.0.0(1883, 8883, 8083, 8084, 8081, 18083, 61613), and with the Tailscale overlay enabled that means reachable from the whole tailnet. The dashboard on 18083 still had the well-knownadmin/publicdefault.Changes
Four commits, ordered so no intermediate state breaks a running deployment:
1b6a1f5127.0.0.13f52c3ac3246871b2fde4Ports. Bot containers run with
network_mode: hostand reach the broker at127.0.0.1:1883;hummingbot-apireaches it in-network asemqx:1883. Neither path ever needed a routable interface. Only1883and the dashboard on18083remain, both on loopback —conf_client.ymlsetsmqtt_ssl: falseand the API speaks plain TCP MQTT, so the SSL/WS/management/STOMP listeners were pure attack surface.Authentication.
password_basedauth against EMQX's built-in database, seeded from a bootstrap file thatmake emqx-authgenerates out of.envbefore everyrun/deploy. The file holds the password in plaintext, so it is written0600and gitignored.Bot side. The credential templates under
bots/credentials/ship with an emptymqtt_username/mqtt_password, which the broker now rejects — socreate_hummingbot_instanceseeds each instance'sconf_client.ymlfrom the API's own broker credentials.mqtt_hostis deliberately left alone: the instance container is on the host network and must use the host loopback, not the compose DNS name the API uses.Weak defaults. The dashboard password now comes from
BROKER_PASSWORDinstead ofadmin/public, andsetup.shgenerates a 32-character broker password on fresh installs rather than writing the literal stringpassword.Verification
Run against a live broker, not inferred:
subscribe hbot/#Connection Refused: bad user name or passwordpublish .../stopmqtt_connected: truedocker port0.0.0.0127.0.0.1:1883,127.0.0.1:18083admin/publicAuth failedUpgrade note for existing deployments
EMQX imports the bootstrap file only for accounts it does not already have, so editing
BROKER_PASSWORDin.envdoes nothing on its own.make emqx-auth-resetdrops the EMQX state volume and re-seeds. Existing deployments running the placeholderpasswordshould rotate.EMQX_DASHBOARD__DEFAULT_PASSWORDlikewise only applies on first init — an existing volume keepsadmin/publicuntil the state volume is recreated.Any already-deployed bot instances have empty MQTT credentials baked into their
conf_client.ymland will fail to reconnect after this change; they need to be redeployed to pick up credentials.Follow-up (not in this PR)
This hardens the broker rather than questioning it. Separately worth discussing: on a single-host deployment the broker has exactly one subscriber (the API) and all publishers are containers on the same machine, and four of its eight subscribed channels land in empty handlers. Retiring EMQX in favour of an embedded broker, then the direct channels the repo already uses elsewhere, is written up separately.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DUBD1gt2FbshzLd1pevxuy