A Python + in-JVM Java agent drop-in replacement for IBC on IB Gateway, targeted at the headless Docker use case. Launches Gateway, drives the login dialog (including TOTP 2FA), applies post-login API config, monitors for re-authentication events, and exposes IBC's TCP command server.
IBC is being deprecated in September 2026. This is one of the community paths forward — written in Python so the maintainer community of
gnzsnz/ib-gateway-dockercan read, patch, and extend it without a JVM or Rust toolchain.
The fastest way to use ibg-controller is the pre-built image on
GitHub Container Registry:
docker pull ghcr.io/code-hustler-ft3d/ibg-controller:latest
docker run -d --name ibkr \
--env-file /path/to/your/.env \
-e TRADING_MODE=paper \
-e TWS_SERVER_PAPER=cdc1.ibllc.com \
-p 127.0.0.1:4002:4004 \
ghcr.io/code-hustler-ft3d/ibg-controller:latestTags published: :latest, :<major>.<minor> (e.g. :0.5), and
:v<major>.<minor>.<patch> (e.g. :v0.5.9). Every tag is signed with
cosign via Sigstore keyless signing — see SECURITY.md
for the verification recipe. For reproducible deployments, pin to a
digest (ghcr.io/code-hustler-ft3d/ibg-controller@sha256:...) — the
digest is printed in each release's CI log.
If you'd rather build the image yourself (or compose ibg-controller into a larger image of your own):
# In the Dockerfile's setup stage:
COPY ./ibg-controller /root/ibg-controller
RUN cd /root/ibg-controller && \
make install DESTDIR=/root && \
cd / && rm -rf /root/ibg-controller /root/build
# In the production stage, add these packages:
# python3 python3-gi gir1.2-atspi-2.0 at-spi2-core
# libatk-wrapper-java libatk-wrapper-java-jni
# dbus-x11 matchbox-window-manager
# and follow docs/MIGRATION.md for the JRE accessibility bridge
# configuration.Then at docker run time:
docker run -d --name ibkr \
--env-file /path/to/your/.env \
-e TRADING_MODE=paper \
-e USE_PYATSPI2_CONTROLLER=yes \
-e TWS_SERVER_PAPER=cdc1.ibllc.com \
-e TWOFACTOR_CODE=<your TOTP secret> \
-p 127.0.0.1:4002:4004 \
your-ib-gateway-imageFull Dockerfile migration instructions: docs/MIGRATION.md.
Already running IBC? Converting your config.ini to ibg-controller
env vars: docs/FROM_IBC.md.
Finding your regional server (TWS_SERVER): docs/BOOTSTRAP.md.
Why each piece is necessary: docs/ARCHITECTURE.md.
A ready-to-use Dockerfile is shipped at the repo root. It extends
gnzsnz/ib-gateway-docker,
installs the AT-SPI stack, configures the Java accessibility bridge
into Gateway's JRE, and drops the controller artifacts from dist/
into /home/ibgateway/:
# Build the agent jar and stage the controller
make
# Build the image. UPSTREAM_IMAGE defaults to ghcr.io/gnzsnz/ib-gateway:stable
# (a moving tag). For reproducible production builds, pin a digest:
docker build -t ibg-controller:local \
--build-arg UPSTREAM_IMAGE=ghcr.io/gnzsnz/ib-gateway:10.45.1c@sha256:... \
.Use this when you want a single ready-to-run image with no further
customization. If you're composing ibg-controller into a larger image
of your own, use the make install DESTDIR=... path shown in the
Quick start above instead.
| Requirement | Notes |
|---|---|
| Linux | amd64 and arm64. Ubuntu 24.04 base tested. |
| IB Gateway | Tested on 10.45.1c. Should work on any 10.x with a compatible install4j launcher and a Zulu JRE 17+. |
| Python 3.10+ | For f-strings with = and type hints. |
| JDK 17+ | Only at build time, for the agent. Runtime uses Gateway's bundled Zulu JRE. |
| Ubuntu packages | python3 python3-gi gir1.2-atspi-2.0 at-spi2-core libatk-wrapper-java libatk-wrapper-java-jni dbus-x11 matchbox-window-manager |
| JRE config | $JAVA_HOME/conf/accessibility.properties with assistive_technologies=org.GNOME.Accessibility.AtkWrapper, and libatk-wrapper.so copied into $JAVA_HOME/lib/ |
| Feature | Gateway | TWS | Notes |
|---|---|---|---|
| Single-mode paper cold-start | ✅ verified | TWS code-path unit-tested; app-name match needs real TWS to validate | |
| Single-mode live cold-start | ✅ verified | ||
Dual mode (TRADING_MODE=both) |
✅ verified | Per-instance state isolation, agent sockets, ready files, JVM-PID-scoped find_app | |
| TOTP 2FA | ✅ verified | ||
| IB Key push 2FA | ✅ wait mode | ✅ wait mode | Controller detects the 2FA dialog, logs "approve on your phone", polls for dialog dismissal. User approves via IB Key mobile app. Same approach as ibctl. |
| Existing-session dialog | ✅ verified | Clicks Continue Login; late-arrival handler catches the dialog if it shows during the 2FA wait |
|
TWS_MASTER_CLIENT_ID |
✅ verified | Set + read back | |
READ_ONLY_API |
✅ verified | Set + read back via JCHECK | |
AUTO_LOGOFF_TIME / AUTO_RESTART_TIME |
✅ verified | Gateway shows one or the other based on account state; controller tries both labels | |
STOP command |
✅ verified | ||
RESTART command (in-place) |
✅ code verified | Full tear-down / re-launch / re-drive login pipeline — verified against live Gateway | |
RECONNECTACCOUNT |
✅ verified | ||
ENABLEAPI |
✅ verified | ||
RECONNECTDATA |
❌ no Gateway menu item | TWS-only |
"✅ verified" = run end-to-end against a real IB account with logged
evidence in the parent repo's spike/ directory. "
- IBC — Java, ~8000 lines. Replaced entirely for Gateway.
TWS support is an isolated code switch (
GATEWAY_OR_TWS=tws) that's unit-tested for the launcher-discovery path but not yet live-tested against real TWS. - oathtool — stdlib-only TOTP generation.
- xdotool — not needed for input; the in-JVM agent handles text entry.
┌────────────────────────────────────────┐
│ Docker container (headless) │
│ │
│ Xvfb :1 ← matchbox WM │
│ │ │
│ ↓ │
│ IB Gateway JVM │
│ ├─ -javaagent:gateway-input-agent.jar │
│ │ │ │
│ │ ↓ │
│ │ Unix socket (/tmp/gateway-input-{mode}.sock)
│ │ ↑ │
│ └─ AT-SPI2 (org.GNOME.Accessibility.AtkWrapper)
│ │ │
│ ↓ │
│ gateway_controller.py (Python) │
│ ├─ pyatspi2: find components, click │
│ ├─ agent socket: type text, set │
│ │ config, navigate JTree │
│ ├─ state machine: login → 2FA → │
│ │ config → ready → monitor │
│ ├─ IBC-compat command server (TCP) │
│ └─ signals /tmp/gateway_ready_{mode} │
│ │
└────────────────────────────────────────┘
- Python controller does component discovery, clicks, state observation, the re-auth monitor loop, and the IBC-compat command server.
- Java agent (~750 lines) loaded via
-javaagent:into Gateway's JVM exists because Gateway's Swing fields reject every external input mechanism (AT-SPIEditableTextwrites returnfalse, synthetic X11 events get filtered by Swing's AWT subsystem). The agent uses Swing's ownJTextField.setText(),AbstractButton.doClick(),JTree.setSelectionPath(), andJToggleButton.doClick()— the only things that actually work. - AT-SPI2 drives component discovery and button clicks in the main window. Text input, tree navigation, and config dialog manipulation go through the agent.
Full diagnostic history and architectural reasoning: docs/ARCHITECTURE.md
| Var | Notes |
|---|---|
TWS_USERID |
Your IB username (or paper username if TRADING_MODE=paper and TWS_USERID_PAPER isn't set) |
TWS_PASSWORD |
Your IB password |
TWS_PASSWORD_FILE |
Alternative: read the password from a file at this path (Docker secrets pattern). run.sh loads it via file_env before launching the controller. |
TWS_USERID_PAPER |
Paper account username — used when TRADING_MODE=paper |
TWS_PASSWORD_PAPER |
Paper account password — used when TRADING_MODE=paper |
TRADING_MODE |
live, paper, or both (default: paper). both runs two Gateway JVMs in parallel with isolated state. |
TWOFACTOR_CODE |
Base32 TOTP secret from IBKR Mobile Authenticator setup. When set, the controller generates a TOTP code and enters it into the Second Factor Authentication dialog. |
TWOFACTOR_CODE_FILE |
Alternative: read the TOTP secret from a file (Docker secrets pattern). |
| Var | Notes |
|---|---|
TWOFA_EXIT_INTERVAL |
Seconds to wait for the Second Factor Authentication dialog to appear. Default 120. Matches IBC's env var name. |
TWOFA_TIMEOUT_ACTION |
What to do if the wait expires: exit (controller exits non-zero), restart (in-place Gateway re-launch via the RESTART command path), or none (fall through to wait_for_api_port — matches Phase 1 behavior, useful for paper accounts / autorestart tokens that skip 2FA entirely). Default none. |
RELOGIN_AFTER_TWOFA_TIMEOUT |
yes/no. If yes, re-drive the login form once before dispatching TWOFA_TIMEOUT_ACTION. Matches IBC. |
| Var | Notes |
|---|---|
BYPASS_WARNING |
Comma- or semicolon-separated list of extra button labels to auto-dismiss in the post-login disclaimer loop. Extends the built-in allowlist (I understand and accept, I Accept, Acknowledge, Accept and Continue). Bare OK is permanently refused — clicking OK on Gateway's "Connecting to server..." progress modal cancels the in-progress login. |
TWS_COLD_RESTART |
yes/no. If yes, apply_warm_state() skips any GATEWAY_WARM_STATE copy and forces Gateway to cold-start. Useful for debugging stuck state. Matches IBC's env var. |
| Var | Notes |
|---|---|
TWS_SERVER |
IBKR regional server hostname (e.g. ndc1.ibllc.com, cdc1.ibllc.com). Default: Gateway's built-in default. See docs/BOOTSTRAP.md for how to find your server. |
TWS_SERVER_PAPER |
Same but for paper mode. Some users have live on one region and paper on another. |
| Var | Notes |
|---|---|
EXISTING_SESSION_DETECTED_ACTION |
primary (default) / primaryoverride / secondary / manual. Matches IBC's setting of the same name. Gateway 10.45.1c's dialog uses Continue Login for primary and Cancel for secondary. |
| Var | Notes |
|---|---|
TWS_MASTER_CLIENT_ID |
Integer. Sets Master API client ID in Configure → Settings → API. |
READ_ONLY_API |
yes/no. Toggles Read-Only API checkbox. |
AUTO_LOGOFF_TIME |
HH:MM. Sets Configure → Settings → Lock and Exit → Set Auto Log Off Time. |
AUTO_RESTART_TIME |
HH:MM AM/PM. Sets Configure → Settings → Lock and Exit → Set Auto Restart Time. |
Gateway's Lock and Exit panel shows either the Auto Log Off Time field or the Auto Restart Time field depending on whether the account has an active autorestart daily-token cycle. The controller tries both labels and sets the one Gateway is currently displaying. If the user sets the one Gateway isn't showing, a clear warning is logged. Setting both env vars makes the controller handle whichever Gateway is displaying in any given session.
| Var | Notes |
|---|---|
CONTROLLER_COMMAND_SERVER_PORT |
TCP port to listen on for IBC-compat commands. Unset (default) disables the server. Set to 7462 to match IBC's default. In dual mode, the paper instance auto-offsets to port+1 to avoid a bind collision. |
CONTROLLER_COMMAND_SERVER_HOST |
Bind address. Default 0.0.0.0 so Docker port forwarding works; restrict exposure with Docker's -p 127.0.0.1:7462:7462 for loopback-only external access. |
Supported commands: STOP, RESTART (in-place Gateway JVM re-launch
- full re-login),
RECONNECTACCOUNT,ENABLEAPI.RECONNECTDATAreturns a clean error on Gateway (no File → Reconnect Data menu item) and dispatches on TWS.
Optional auth token (recommended if you expose the port beyond
localhost): set CONTROLLER_COMMAND_SERVER_AUTH_TOKEN=<random-secret>.
When set, clients must send AUTH <token>\n as their first line
before a command:
AUTH <token>
STOP
The token is checked with hmac.compare_digest to resist timing
side-channels. Without the token set, the command server runs in
IBC-compat no-auth mode and logs a loud WARNING at startup.
| Var | Notes |
|---|---|
CCP_MAINTENANCE_RECOVERY_DELAY_SECONDS |
Seconds to sleep after a JVM code-0 exit (or cold start) inside IBKR's daily maintenance window (~23:30-00:30 America/New_York) before re-auth. Default 480 (8 min). The delay lets IBKR's auth server drain the cooperatively-shutdown session; re-auth'ing too quickly during this window is silently dropped → CCP LOCKOUT cascade. See docs/DISCONNECT_RECOVERY.md for the 2026-04-20/21 incident that motivated this. |
CCP_LOCKOUT_MAX_JVM_RESTARTS |
Cap on CCP-lockout-triggered JVM restart cycles. Default 0 (halt on first persistent lockout — see ALERT_CCP_PERSISTENT_HALT). Set to a positive integer (e.g. 5) to restore the pre-v0.5.9 auto-recovery loop. |
| Var | Notes |
|---|---|
CONTROLLER_HEALTH_SERVER_PORT |
TCP port for the HTTP /health endpoint. Default 8080 in the shipped image, unset on source checkout. In dual mode, paper auto-offsets to port+1. Set to empty to disable the health server entirely. |
CONTROLLER_HEALTH_SERVER_HOST |
Bind address. Default 0.0.0.0 so Docker port forwarding works. Restrict external exposure with Docker's -p 127.0.0.1:8080:8080 on the host side. |
GET /health returns JSON with the controller's state, Gateway JVM
liveness, API port status, CCP lockout streak, and the timestamp of
the most recent successful auth. HTTP 200 if the controller is logged
in and serving (state==MONITORING AND api_port_open AND
jvm_alive); 503 otherwise. GET /ready returns 200 while the
process is running (for Kubernetes-style readiness).
The controller also emits stable grep-contract log tokens
(ALERT_CCP_PERSISTENT, ALERT_JVM_RESTART_EXHAUSTED,
ALERT_2FA_FAILED, ALERT_PASSWORD_EXPIRED, ALERT_LOGIN_FAILED,
ALERT_SHUTDOWN) that external monitors can pattern-match on
regardless of log level. ALERT_PASSWORD_EXPIRED fires in two
flavors — status=warning (with days_remaining=N when the dialog
reports it, actionable before lockout) and status=expired (login
is already blocked, rotate the password in IBKR's web portal).
ALERT_LOGIN_FAILED fires when Gateway surfaces a credential-rejection
modal or when the launcher.log fingerprint matches a bad-credentials
auth flow (reason=bad-credentials), so monitors can tell a
stale-password account lockout apart from IBKR's silent cooldown
(ALERT_CCP_PERSISTENT) without waiting for the CCP-retry ceiling.
ALERT_SHUTDOWN is the lifecycle complement — INFO-level and emitted
on SIGTERM/SIGINT with graceful=true|false so dashboards can
distinguish operator-initiated shutdowns from Gateway-JVM crashes,
and flag stuck-JVM graceful=false restarts that need attention.
The shipped Dockerfile includes a HEALTHCHECK that curls /health
every 30s with a 180s start-period. In DUAL_MODE=yes it probes both
the live and paper ports; either side being unhealthy marks the
container unhealthy.
Full protocol, field semantics, and integration examples (cron,
Prometheus blackbox_exporter, jq): docs/OBSERVABILITY.md.
Playbook for operator-action scenarios (CCP lockout, 2FA failure,
JVM restart exhaustion, Gateway JVM crash): docs/DISCONNECT_RECOVERY.md.
| Var | Notes |
|---|---|
GATEWAY_OR_TWS |
gateway (default) or tws. Switches launcher discovery ($TWS_PATH/ibgateway/... vs $TWS_PATH/tws/...) and AT-SPI app name search between IB Gateway and Trader Workstation. Code path unit-tested; live validation pending a TWS-with-controller Dockerfile variant. |
| Var | Notes |
|---|---|
TWS_SETTINGS_PATH |
In dual mode, run.sh sets this per instance (e.g. /home/ibgateway/Jts_live) so live and paper have isolated state. Single-mode users don't need to set it. |
CONTROLLER_READY_FILE |
Override the readiness signal file. Defaults to /tmp/gateway_ready (single mode) or /tmp/gateway_ready_{mode} (dual mode, set automatically by run.sh). |
| Var | Notes |
|---|---|
GATEWAY_WARM_STATE |
Optional path. If set, the controller copies files from this directory into Gateway's settings dir before launch. Useful for seeding a fresh container with a previously-captured jts.ini + encrypted state dir (for autorestart token support). |
GATEWAY_INPUT_AGENT_JAR |
Override path to the agent jar. Default: ~/gateway-input-agent.jar. |
GATEWAY_INPUT_AGENT_SOCKET |
Override Unix socket path. Default: /tmp/gateway-input.sock (single mode) or /tmp/gateway-input-{mode}.sock (dual mode, set automatically by run.sh). |
CONTROLLER_DEBUG |
Set to 1 to enable debug-level logging. |
CONTROLLER_TEST_MODE |
Set to 1 to exit immediately after clicking Log In (for smoke tests). |
This tool is designed to drop into a
gnzsnz/ib-gateway-docker
image via a Dockerfile.template addition, replacing IBC. Install via
the Makefile target (make install DESTDIR=...) that drops
gateway-input-agent.jar and scripts/gateway_controller.py into the
ibgateway user's home.
See docs/MIGRATION.md for swapping out IBC in a
gnzsnz/ib-gateway-docker-style Dockerfile.
If you already have an IBC config.ini, the
ibc_config_to_env.py one-shot tool
converts it to the equivalent ibg-controller env vars (.env,
docker run -e, or docker-compose format), warning on unsupported
keys instead of silently dropping them. Full mapping table and cutover
recipe: docs/FROM_IBC.md.
./ibc_config_to_env.py /path/to/your/IBC/config.ini > .env
# or: ./ibc_config_to_env.py --format compose config.ini# Build the agent jar and stage the controller into dist/
make
# Syntax-check the Python controller + validate the agent jar manifest
make test
# Create a release tarball (dist/ibg-controller-0.5.9.tar.gz)
make release VERSION=0.5.9
# Install directly into a running ibgateway home (for dev on host, or
# as called by the Docker image's setup stage)
make install DESTDIR=/home/ibgatewayBuild requires a JDK 17+ (javac + jar) and make. No Maven, no Gradle.
VER=0.5.9
curl -sSLO https://github.com/code-hustler-ft3d/ibg-controller/releases/download/v${VER}/ibg-controller-${VER}.tar.gz
tar -xzf ibg-controller-${VER}.tar.gz
cd ibg-controller-${VER}
DESTDIR=/home/ibgateway ./install.shThe tarball layout is flat:
ibg-controller-0.5.9/
├── gateway-input-agent.jar ← installed to $DESTDIR/gateway-input-agent.jar
├── gateway_controller.py ← installed to $DESTDIR/scripts/gateway_controller.py
├── ibc_config_to_env.py ← one-shot IBC config.ini → env migration tool
├── install.sh
├── README.md, CHANGELOG.md, LICENSE, SECURITY.md
└── docs/
├── ADR-001-in-jvm-dialog-dispatcher.md
├── ARCHITECTURE.md
├── BOOTSTRAP.md
├── DISCONNECT_RECOVERY.md
├── FROM_IBC.md
├── MIGRATION.md
├── OBSERVABILITY.md
└── UPGRADING.md
IBKR's auth server occasionally stops responding to fresh password logins for several minutes (and occasionally hours) after a burst of failed attempts from the same account. There are two visible failure modes:
- CCP silent timeout — Gateway logs a 20-second silent
AuthTimeoutMonitor-CCP: Timeout!inlauncher.logwith no dialog on-screen. Gateway 10.45.1c also hides the error dialog that 10.44.1g surfaces in the same state, making this worse. - Stuck-connecting retry loop — Gateway's login dialog stays up
showing
Attempt N: connecting to server (trying for another XX seconds). The auth protocol never starts, so noTimeout!line appears inlauncher.logat all — the only visible signal is the dialog text.
What the controller does automatically: as of v0.4.0, the controller
detects both modes, applies an exponential backoff between retries
(60s → 120s → 240s → 480s → 600s cap), and recovers by re-driving
Log In on the existing Gateway JVM — never by killing and
relaunching it. This matches
IBC's LoginManager.initiateLogin
pattern: IBKR's auth server treats each new JVM as a fresh handshake
and keeps the CCP limiter armed, so the previous v0.2.2–v0.3.2
"backoff + do_restart_in_place" design never let the lockout clear.
You'll see these lines:
CCP LOCKOUT DETECTED — IBKR's auth server silently dropped the auth request
CCP backoff: waiting 60s before next auth attempt
Retrying auth in-JVM after CCP backoff (attempt 1/8)
In-JVM relogin attempt (no JVM restart — matches IBC's LoginManager.initiateLogin semantics)
or
Login dialog stuck in 'connecting to server' retry loop — IBKR auth server isn't accepting sessions right now. Applying CCP backoff before retry.
CCP backoff: waiting 120s before next auth attempt
The backoff counter is per-trading-mode (live and paper run as separate processes in dual mode, so they don't share state), and resets on genuine 2FA-success. Up to 8 in-JVM retries per controller lifetime; past that the controller exits and the container orchestrator's restart policy takes over. Just let it run — the controller will keep retrying with increasing delays until IBKR's rate limiter clears, often 5–60 minutes total.
If you're still stuck after an hour of patient backoff, double-check:
- You're sending the right username for the trading mode (the
controller auto-swaps to
TWS_USERID_PAPERwhenTRADING_MODE=paper, but double-check your env file) - Your
TWS_SERVER/TWS_SERVER_PAPERmatches the regional server your account is actually hosted on — seedocs/BOOTSTRAP.md - If you have a previously working container's
/home/ibgateway/Jtsstate available, mount it viaGATEWAY_WARM_STATE— autorestart token reauth goes through a different code path than fresh-password auth and bypasses the cooldown
If a clean retry from a known-good config still fails after the
backoff has run its course, the issue is almost certainly account-side
(wrong server, wrong userid, account locked), not the controller.
Gateway's launcher.log at /home/ibgateway/Jts/launcher.log will
confirm the CCP-Timeout case — you'll see the
Authenticating → Timeout! pattern with nothing in between. The
stuck-connecting case won't show in launcher.log; check the
controller's own logs for the "stuck in 'connecting to server'"
warning instead.
The ATK bridge didn't load. Check:
ls $JAVA_HOME/conf/accessibility.propertiesinside the container — must containassistive_technologies=org.GNOME.Accessibility.AtkWrapperls $JAVA_HOME/lib/libatk-wrapper.so— must exist (copied from/usr/lib/*/jni/libatk-wrapper.soat image build time)at-spi-bus-launcher --launch-immediatelywas started BEFORE Gateway. If the accessibility bus isn't up when Gateway's JVM initializes, the ATK wrapper silently skips itself.
The controller's EXISTING_SESSION_DETECTED_ACTION=primary clicks
Continue Login, which tells IBKR to kick the other session. If
something else keeps reconnecting as that account (another container,
the mobile app, TWS on your desktop), you'll ping-pong forever. Shut
down the other session first.
The Java agent's GET_PID command returned nothing. This usually
means the agent's Unix socket is the old IBC default path (check
GATEWAY_INPUT_AGENT_SOCKET in the container's env) or the
ProcessHandle API isn't available (you're running on JRE < 17).
Verify the JRE is Zulu 17 or newer.
Gateway's Lock and Exit panel shows either "Set Auto Log Off Time
(HH:MM)" or "Set Auto Restart Time (HH:MM)" depending on account
state. The controller tries the label matching the env var you set.
If it can't find it, it warns which label Gateway is currently
showing and suggests the other env var. Set both AUTO_LOGOFF_TIME
and AUTO_RESTART_TIME if you want the controller to handle whichever
Gateway is displaying.
Full supply chain details (cosign verification, SBOM extraction,
pinning to digests, vuln reporting): SECURITY.md.
Quick version of the deployment hygiene below.
This is a narrow threat model — single-user container running a trading tool. But there are real things to get right.
If you're running with CONTROLLER_COMMAND_SERVER_PORT set:
- Bind to loopback only via Docker:
-p 127.0.0.1:7462:7462. The in-container bind is0.0.0.0by default so Docker port forwarding works at all; Docker's mapping is what controls external exposure. Never use a bare-p 7462:7462(which binds the host's external interface) unless you also set an auth token. - Set
CONTROLLER_COMMAND_SERVER_AUTH_TOKENto a random secret if the port is reachable by anything other than127.0.0.1on the host, or if the host is multi-tenant. Without it, anyone who can reach the port can sendSTOP/RESTART/RECONNECTACCOUNT. - The controller logs a loud WARNING at startup if the command server is enabled without an auth token. Don't ignore it.
Credentials:
- Pass them via
docker run --env-file /path/to/.env, never on the command line. The controller redacts them in its own logs; Gateway itself also redacts them inlauncher.log. - Set your
.envfile to600on the host. Docker Engine only reads the file atdocker runtime; what's in/proc/<pid>/environafter that depends on your Docker Engine version. - For additional safety, consider Docker secrets (
_FILEenv vars likeTWS_PASSWORD_FILE) — the controller delegates those torun.sh'sfile_envhelper which loads them from a separate file path.
Logs and sharing them:
CONTROLLER_DEBUG=1dumps modal dialog contents. The controller redacts account numbers matchingDU\d{5,10}/U\d{5,10}(IBKR account format) before logging. Other identifying information like your username may still appear in window titles. Review logs before posting them publicly.- Gateway's own
/home/ibgateway/Jts/launcher.logis NOT controlled by us and may include fragments of your session. If you attach it to a bug report, sanitize first.
Warm state and file inputs:
GATEWAY_WARM_STATEis trusted — only set it to a directory you own or copied from your own working container. A malicious warm-state dir could inject arbitrary content into$JTS_CONFIG_DIR.TWS_SERVER/TWS_SERVER_PAPERare validated as hostnames (DNS label characters only) at controller startup. An invalid hostname aborts before Gateway starts.
What's NOT protected:
- The Java agent's Unix socket is reachable by any process in the
same container that can read
/tmp/gateway-input-*.sock(owner-only perms are set, but the container is single-user so this is mostly for defense-in-depth). - The command server has no rate limiting.
- No TLS on the command server. Mitigated by recommending
loopback-only binding via Docker's
-p 127.0.0.1:....
MIT — see LICENSE. Builds on work from IBC, ibctl, and gnzsnz/ib-gateway-docker, all credited below.
- @rlktradewright for IBC. Most of what we know about driving Gateway's dialogs comes from reading IBC.
- Lcstyle/ibctl for the Java-agent architecture and the edge-case catalog. The idea of using an in-JVM agent for the operations that Swing rejects externally came from ibctl.
- @gnzsnz for steering the tool's architecture in
issue #366
and for making the
ib-gateway-dockerimage this is meant to drop into.