feat / Secure the Gateway with auto-generated shared mTLS certs (SEC-048) - #185
Merged
Conversation
The Gateway was deployed unsecured (DEV=true -> plain HTTP, no client-cert auth) and published on all interfaces, exposing full wallet/trade control to anyone reaching port 15888. Run it secured by default with TLS + mTLS, using one shared cert set auto-generated on first start so the operator does nothing. - utils/gateway_certs.py: single source of truth for the shared mTLS cert set. Idempotent generation (existing CA reused), mirrors client certs to root_path()/certs for the in-process GatewayHttpClient, and builds the client SSLContext. All private keys encrypted with one secret (CONFIG_PASSWORD). - gateway_service: default dev_mode=False; generate + mount certs read-only at /home/gateway/certs; GATEWAY_PASSPHRASE sourced from CONFIG_PASSWORD; bind the bridge-mode host publish to 127.0.0.1 instead of 0.0.0.0. - gateway_client: accept an SSLContext and use it for https; accounts_service builds it for https URLs. config default URL -> https://localhost:15888. - main.py: sync client certs to root_path()/certs when SSL is enabled. - docker_service: mount the shared certs into each instance at /home/hummingbot/certs and set conf_client.yml gateway_use_ssl: true. dev_mode=True remains an explicit, loopback-only escape hatch for local debugging. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…(SEC-048) The containerized API mounts only ./bots:/hummingbot-api/bots, while gateway-files lived at the repo root and BOTS_PATH is the host path. So a containerized API wrote the mTLS certs to its own ephemeral filesystem and the Gateway container bind-mounted an empty host dir -> Gateway crash-looped on missing ./certs/server_key.pem. Move the shared dir under bots/ (inside the one host<->container mount) and split the path used to WRITE (container-local, the mounted bots/ dir) from the bind-mount SOURCE (host path), mirroring how DockerService handles instance volumes. Local runs are unaffected (both bases resolve to the repo). - utils/gateway_certs: GATEWAY_SUBPATH = bots/gateway-files; gateway_certs_dir(host=...) returns the local read/write path or the host mount-source path. - gateway_service: _gateway_base(host=...); makedirs on local base, bind-mount sources on host base (conf/logs/certs); remove() uses the local base. - docker_service: mount instance certs from the host path. - .gitignore: ignore bots/gateway-files/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The custom GatewayClient built its SSL context once at API startup, but the shared certs only exist after the Gateway is started (post-startup). The wallet/balance/swap client therefore stayed without mTLS until an API restart. Build the context lazily on first https request (cached), so certs generated once the Gateway starts are picked up with no restart; a still-missing cert set returns a clean 503 instead of crashing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…C-048) The compose override sent the API to http://host.docker.internal:15888 (the host's published port). With the Gateway publish now bound to 127.0.0.1, that path is no longer reachable, and it was plain http besides. Use https://gateway:15888 so the API reaches the Gateway container-to-container over the shared emqx-bridge network (the Gateway is attached to it on start; the cert SAN includes 'gateway'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e (SEC-048) The Gateway (v2.x) uses a single GATEWAY_PASSPHRASE for both TLS cert-key decryption and wallet encryption (readWalletKey() === readPassphrase()), and the shared mTLS cert set must be decryptable by this API's clients, which decrypt the client cert with CONFIG_PASSWORD (hummingbot's in-process GatewayHttpClient hardwires this). The cert / gateway / wallet passphrase is therefore necessarily CONFIG_PASSWORD. The previous per-request `passphrase` override was a footgun: a value other than CONFIG_PASSWORD encrypted the certs with it while the API still decrypted the client cert with CONFIG_PASSWORD, silently breaking every Gateway call. Remove the field and derive the passphrase solely from CONFIG_PASSWORD. An extra `passphrase` in the request body is ignored rather than rejected, so existing callers don't break. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…structions (SEC-048) The README told users to "Start Gateway in development mode with passphrase 'admin'", which (together with the old dev_mode=True default) is why a fresh install came up in dev mode with no certs. Document the secured-by-default behavior, auto-generated certs, CONFIG_PASSWORD as the single secret, and dev_mode as an explicit opt-in. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The API must never serve a wallet-holding Gateway over plain HTTP, so drop dev mode altogether rather than leaving it as an opt-in footgun (and the source of "fresh install starts in dev mode"). Remove the dev_mode field from GatewayConfig and always start the Gateway with TLS + mTLS (DEV=false, certs generated and mounted). An extra dev_mode in a request body is ignored, not rejected. README/config docs updated accordingly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cardosofede
force-pushed
the
feat/secure_gateway_mtls
branch
from
June 25, 2026 21:12
ec4ea52 to
3537119
Compare
Contributor
|
Commit 3537119
Started from a clean Docker deployment and call POST /gateway/start with an empty body. Confirmed bots/gateway-files/certs/ is created and Gateway starts secured ✅
Gateway blocks unsecured direct access on port 15888 ✅
For the next following tests:
API restart keeps Gateway routes working ✅
API starts before Gateway and recovers after Gateway starts ✅
Deploy a new bot instance and confirm conf_client.yml has gateway_use_ssl: true and the container mounts /home/hummingbot/certs read-only.On Linux, verify unauthenticated remote access to port 15888 is still blocked by mTLS.
|
rapcmia
approved these changes
Jun 29, 2026
rapcmia
left a comment
Contributor
There was a problem hiding this comment.
- Tested using docker using
make setup; make deploy - Tested POST /gateway/start with an empty JSON body and confirmed it started Gateway container successfully on port 15888.
- Tested HAPI proxy routes
GET /gateway/connectors,GET /gateway/chains, and the live MeteoraGET /gateway/clmm/pool-inforoute, and confirmed they returned real Gateway data after Gateway was up. - Tested the HAPI recovery flow and confirmed Gateway-backed routes returned the expected unavailable response while Gateway was down, then recovered without restarting HAPI once Gateway finished booting.
- Tested direct no-cert access to https://localhost:15888/ and https://localhost:15888/docs, and both requests were rejected during TLS with certificate required, so unsecured direct HTTPS access remained blocked.
- Confirmed integration testing also passed with hummingbot/condor#135, where the secured Gateway flow and cross-service integration completed successfully.
Note:
- Created separate issue #187
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
Closes SEC-048. The Gateway was deployed unsecured (
DEV=true→ plain HTTP, no client-cert auth) and published on all interfaces, exposing full wallet/trade control to anyone reaching port 15888. This makes it run secured by default with TLS + mutual-cert (mTLS) auth, using one shared cert set auto-generated on first start — no manual step for the operator.What changed
utils/gateway_certs.py(new): single source of truth for the shared mTLS cert set. Idempotent generation (existing CA reused), mirrors client certs toroot_path()/certsfor the in-processGatewayHttpClient, builds the clientSSLContext. All private keys encrypted with one secret (CONFIG_PASSWORD). Lives underbots/so it's inside the shared host↔container mount, with the write path (container-local) split from the bind-mount source (host).gateway_service/models/gateway:dev_modedefaults False; certs generated + mounted read-only at/home/gateway/certs;GATEWAY_PASSPHRASEsourced fromCONFIG_PASSWORD; bridge-mode host publish bound to127.0.0.1instead of0.0.0.0.gateway_client/accounts_service: client speaks https + presents its cert, with theSSLContextbuilt lazily (certs don't exist until the Gateway is started — picked up with no API restart).main.py: sync client certs toroot_path()/certswhen SSL is on.docker_service: deployed instances mount the shared certs at/home/hummingbot/certsand getgateway_use_ssl: true.docker-compose.yml: API reaches the Gateway in-network athttps://gateway:15888(host.docker.internal is unreachable once the publish is loopback-bound).dev_mode=Trueremains an explicit, loopback-only escape hatch for local debugging.Verified live (containerized deployment, gateway image v2.14.0)
bots/gateway-files/certs/, host publish bound to127.0.0.1:15888.{"status":"ok"}; plain HTTP → rejected.GET /gateway/connectors,/gateway/chains, and a Meteoraclmm/pool-infoquery all returned live data.gateway_use_ssl: true.Migration note
GATEWAY_PASSPHRASEnow comes fromCONFIG_PASSWORD. A Gateway whose config was previously encrypted with a different user-supplied passphrase must re-init its Gateway config. Operators on https must also setGATEWAY_URL=https://....🤖 Generated with Claude Code