Skip to content

Lock down Postgres/EMQX/API ports, make Tailscale actually exclusive - #220

Merged
cardosofede merged 4 commits into
mainfrom
feature/lockdown-ports-tailscale-serve
Aug 31, 2026
Merged

Lock down Postgres/EMQX/API ports, make Tailscale actually exclusive#220
cardosofede merged 4 commits into
mainfrom
feature/lockdown-ports-tailscale-serve

Conversation

@david-hummingbot

Copy link
Copy Markdown
Contributor

Summary

  • 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 these — 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. This closes a real gap: Docker publishes ports on all interfaces via its own iptables rules, ahead of a plain ufw/host firewall, so these were reachable externally on any VPS deploy regardless of firewall config.
  • hummingbot-api's port 8000 is now bound via ${API_BIND_HOST:-0.0.0.0} (default unchanged, so non-Tailscale deploys aren't affected). setup.sh sets API_BIND_HOST=127.0.0.1 whenever Tailscale is enabled.
  • The Tailscale sidecar actually proxies port 8000 now. Previously, enabling Tailscale joined the tailnet but port 8000 stayed published on every host interface at the same time — the README's "not exposed publicly" claim wasn't actually enforced. docker-compose.tailscale.yml now sets TS_SERVE_CONFIG (a static, committed tailscale-serve.json) so the sidecar forwards the tailnet IP's :8000 to 127.0.0.1:8000 (reachable directly since the sidecar shares the host's network namespace via network_mode: host).
  • make tailscale-status now also prints tailscale serve status so the proxy — not just tailnet membership — is visible.
  • README updated to match (port table, Tailscale section, Configuration block), plus a stale /health/ fix (no /health endpoint exists in main.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:8000 defaults to 0.0.0.0 (unchanged), Postgres/EMQX resolve to 127.0.0.1
  • API_BIND_HOST=127.0.0.1 docker compose config — confirms the override binds hummingbot-api:8000 to loopback
  • docker compose -f docker-compose.yml -f docker-compose.tailscale.yml config -q — valid with the sidecar overlay + TS_SERVE_CONFIG mount
  • bash -n setup.sh — syntax check passes
  • make -n tailscale-status — Makefile syntax check passes
  • pre-commit hooks (detect-private-key, detect-wallet-private-keys) pass
  • Manual smoke test on a real Tailscale-enabled deploy: confirm tailscale serve status shows port 8000 forwarded and the API is reachable at http://hummingbot-api:8000 from another tailnet device while nmap/curl from 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

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
david-hummingbot marked this pull request as ready for review August 21, 2026 13:53
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

The PR binds PostgreSQL and EMQX host ports to loopback, adds Tailscale Serve forwarding for the API, and introduces deployment diagnostics.

  • Makes the API host bind configurable through API_BIND_HOST.
  • Adds a Tailscale sidecar Serve configuration forwarding tailnet port 8000 to the loopback API.
  • Adds make doctor checks for configuration, containers, port exposure, Tailscale, authentication, and API reachability.
  • Updates setup prompts, credential handling, status commands, and deployment documentation.

Confidence Score: 3/5

The PR is not yet safe to merge because upgraded Tailscale deployments can still leave the trading API reachable through public interfaces.

Existing .env files bypass the only code that writes the loopback bind, while make deploy applies the Tailscale overlay without enforcing that value and Compose defaults the API mapping to 0.0.0.0.

Files Needing Attention: docker-compose.yml, setup.sh, Makefile

Important Files Changed

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]
Loading

Reviews (4): Last reviewed commit: "Setup: mask the password prompts, stop h..." | Re-trigger Greptile

Comment thread docker-compose.yml
david-hummingbot and others added 3 commits August 25, 2026 00:07
…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
cardosofede merged commit 6ef09c4 into main Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants