Lock down Postgres/EMQX/API ports, make Tailscale actually exclusive - #220
Merged
Conversation
Postgres (5432) and every EMQX port (1883/8883/8083/8084/8081/18083/61613)
now bind to 127.0.0.1 only in docker-compose.yml — nothing external ever
needs them; hummingbot-api reaches them over the emqx-bridge docker network,
and bot containers (network_mode=host) reach 127.0.0.1 just as well as
0.0.0.0.
hummingbot-api's own port 8000 is now bound via ${API_BIND_HOST:-0.0.0.0},
defaulting to today's behavior. setup.sh sets API_BIND_HOST=127.0.0.1
whenever Tailscale is enabled, and the Tailscale sidecar (docker-compose.
tailscale.yml) gets a declarative TS_SERVE_CONFIG (tailscale-serve.json)
that forwards the tailnet's :8000 to 127.0.0.1:8000 — closing the gap where
a Tailscale deploy still had port 8000 published on every host interface
at the same time. `make tailscale-status` now also prints `tailscale serve
status` so the proxy (not just tailnet membership) is visible.
README updated to match, plus a stale /health -> / fix (no such endpoint
exists in main.py).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
david-hummingbot
marked this pull request as ready for review
August 21, 2026 13:53
Greptile SummaryThe PR binds PostgreSQL and EMQX host ports to loopback, adds Tailscale Serve forwarding for the API, and introduces deployment diagnostics.
Confidence Score: 3/5The PR is not yet safe to merge because upgraded Tailscale deployments can still leave the trading API reachable through public interfaces. Existing Files Needing Attention: docker-compose.yml, setup.sh, Makefile
|
| Filename | Overview |
|---|---|
| docker-compose.yml | Adds configurable API binding and loopback-only PostgreSQL/EMQX ports, but the API fallback remains public when older Tailscale configurations lack the new variable. |
| setup.sh | Fresh installations receive the appropriate API bind host, while the existing-configuration early exit leaves older Tailscale installations unchanged. |
| docker-compose.tailscale.yml | Adds the host-network Tailscale sidecar Serve configuration and mounts its static forwarding configuration. |
| tailscale-serve.json | Forwards tailnet TCP port 8000 to the API's loopback listener. |
| doctor.sh | Adds read-only diagnostics that detect insecure Tailscale binding and other deployment problems, but does not gate deployment. |
| Makefile | Adds diagnostics and Serve status reporting, while the deploy target still does not enforce the Tailscale-only API bind. |
| README.md | Documents the revised local and Tailscale exposure model and operational diagnostics. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart LR
Public[Public interfaces] -. blocked when loopback bind is active .-> API[API port 8000]
Tailnet[Tailscale device] --> Serve[Tailscale Serve :8000]
Serve --> Loopback[127.0.0.1:8000]
Loopback --> API
API --> Network[emqx-bridge]
Network --> EMQX[EMQX]
Network --> Postgres[PostgreSQL]
Reviews (4): Last reviewed commit: "Setup: mask the password prompts, stop h..." | Re-trigger Greptile
cardosofede
approved these changes
Aug 21, 2026
…ients The Configuration section's example .env showed TAILSCALE_ENABLED=true directly above API_BIND_HOST=0.0.0.0 -- contradicting the comment right above it, which correctly says 127.0.0.1 is what gets set when Tailscale is enabled. Fixed the example value to match. Also clarified the "same machine" callout at the top: a client that's co-located with this API (the concrete case being Condor deploying its own hummingbot-api locally) reaches it over localhost regardless of Tailscale, and doesn't need this API to have its own tailnet node for that -- only for *other*, non-co-located devices to reach it directly. This is exactly the redundancy Condor's own install wizard just stopped creating by default (it used to spin up a second tailnet node for a co-located hummingbot-api even though Condor never needed it). No functional changes -- setup.sh/docker-compose.yml/docker-compose. tailscale.yml/tailscale-serve.json were already correct and already consistent with what Condor's wizard now produces; this is a docs-only pass after cross-checking this PR against that work. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Condor ships one; this side had nothing equivalent, so the two halves of the same install could only be checked from one end. Same report layout, so they read as one tool when run back to back. Checks dependencies (Docker, Compose, curl), .env -- including credentials still left at well-known defaults -- the hummingbot-api / hummingbot-broker / hummingbot-postgres containers, which ports are sitting on a public interface, Tailscale's tailnet AND serve status, and whether the API actually answers an authenticated request. The last one also confirms an *un*authenticated request is rejected: a 200 there would mean the API is answering anyone who can reach the port. Two of those are worth calling out because `docker compose ps` cannot show them. A stack deployed before the port lockdown still has Postgres and EMQX on every interface until the containers are recreated, and nothing about a running stack makes that visible. And with API_BIND_HOST=127.0.0.1, joining the tailnet is not the same as being reachable on it -- `tailscale serve` is the only thing forwarding port 8000, so the doctor reports tailnet membership and the serve forward separately. Plain bash, no conda: the Docker deploy path never creates the conda env, and a doctor that cannot run on the most common install is not much of a doctor. Read-only, and exits non-zero only on a real failure -- warnings are advice. Container names are matched exactly rather than by substring, because emqx runs as hummingbot-broker and postgres as hummingbot-postgres, which `docker ps | grep emqx` would never find. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The API password and config password were read with plain `read -r`, so they were typed in cleartext -- left on screen, in scrollback, and in any recorded install log. They are masked now, and asked twice: a mistyped password that is never echoed is otherwise only discovered when the API refuses to authenticate. Inner whitespace is rejected, since .env is read by three parsers that disagree on quoting. GATEWAY_PASSPHRASE was written as a literal `admin` regardless of what the operator chose. It unlocks Gateway's DEX wallet keys, and it is the first thing anyone who reaches the port will try. It now reuses CONFIG_PASSWORD -- which is what Condor's install wizard has been writing here all along, so the two generators finally agree. .env is chmod 600. It holds the API password, the config/Gateway passphrase and (when set) a Tailscale auth key, and the usual 644 umask makes all of that readable by every other account on the box. The "`.env` file already exists. Skipping setup." path exited with no way forward, which is a dead end for anyone re-running setup to change something. It now names the three ways out: edit and redeploy, `make reset`, or `make doctor`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
cardosofede
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
127.0.0.1only indocker-compose.yml. Nothing external ever needs these — hummingbot-api reaches them over theemqx-bridgedocker network, and bot containers (network_mode=host) reach127.0.0.1just as well as0.0.0.0. This closes a real gap: Docker publishes ports on all interfaces via its own iptables rules, ahead of a plainufw/host firewall, so these were reachable externally on any VPS deploy regardless of firewall config.${API_BIND_HOST:-0.0.0.0}(default unchanged, so non-Tailscale deploys aren't affected).setup.shsetsAPI_BIND_HOST=127.0.0.1whenever Tailscale is enabled.docker-compose.tailscale.ymlnow setsTS_SERVE_CONFIG(a static, committedtailscale-serve.json) so the sidecar forwards the tailnet IP's:8000to127.0.0.1:8000(reachable directly since the sidecar shares the host's network namespace vianetwork_mode: host).make tailscale-statusnow also printstailscale serve statusso the proxy — not just tailnet membership — is visible.Configurationblock), plus a stale/health→/fix (no/healthendpoint exists inmain.py; confirmed via grep).This is the hummingbot-api-side counterpart to hummingbot/condor#213, which does the equivalent (127.0.0.1 bind +
tailscale serve) for Condor's own web dashboard.Test plan
docker compose config -q— valid,hummingbot-api:8000defaults to0.0.0.0(unchanged), Postgres/EMQX resolve to127.0.0.1API_BIND_HOST=127.0.0.1 docker compose config— confirms the override bindshummingbot-api:8000to loopbackdocker compose -f docker-compose.yml -f docker-compose.tailscale.yml config -q— valid with the sidecar overlay +TS_SERVE_CONFIGmountbash -n setup.sh— syntax check passesmake -n tailscale-status— Makefile syntax check passestailscale serve statusshows port 8000 forwarded and the API is reachable athttp://hummingbot-api:8000from another tailnet device whilenmap/curlfrom outside the tailnet times out on all of 8000/5432/1883/etc. (not testable from this dev environment — no live tailnet/second machine available)🤖 Generated with Claude Code