Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ jobs:
ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY: ${{ vars.IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY }}
TWS_ACCEPT_INCOMING: ${{ vars.IB_GATEWAY_TWS_ACCEPT_INCOMING }}
READ_ONLY_API: ${{ vars.IB_GATEWAY_READ_ONLY_API }}
TWOFA_DEVICE: ${{ vars.IB_GATEWAY_TWOFA_DEVICE }}
IBKR_2FA_AUTOFILL: ${{ vars.IB_GATEWAY_2FA_AUTOFILL }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip TOTP secret enforcement when auto-fill is disabled

Adding IBKR_2FA_AUTOFILL here introduces a push-approval mode, but the workflow still unconditionally requires TOTP_SECRET later in Check whether deployment config is complete (require_secret_source TOTP_SECRET_SECRET_NAME ...) and again after secret resolution (resolved totp_secret is empty check). In the IBKR_2FA_AUTOFILL=no scenario, users who rely on IBKR Mobile push and have no TOTP seed will fail deployment before the container starts, so the new mode is effectively unusable unless a dummy secret is supplied.

Useful? React with 👍 / 👎.

SSH_PRIVATE_KEY_SECRET_NAME: ${{ vars.IB_GATEWAY_SSH_PRIVATE_KEY_SECRET_NAME }}
TWS_USERID_SECRET_NAME: ${{ vars.IB_GATEWAY_TWS_USERID_SECRET_NAME }}
TWS_PASSWORD_SECRET_NAME: ${{ vars.IB_GATEWAY_TWS_PASSWORD_SECRET_NAME }}
Expand Down Expand Up @@ -232,6 +234,8 @@ jobs:
"ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY": os.environ["ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY"],
"TWS_ACCEPT_INCOMING": os.environ["TWS_ACCEPT_INCOMING"],
"READ_ONLY_API": os.environ["READ_ONLY_API"],
"TWOFA_DEVICE": os.environ.get("TWOFA_DEVICE", ""),
"IBKR_2FA_AUTOFILL": os.environ.get("IBKR_2FA_AUTOFILL", ""),
}

def quote(value: str) -> str:
Expand Down
19 changes: 19 additions & 0 deletions 2fa_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
# ================= Configuration =================
SECRET_KEY = os.environ.get("TOTP_SECRET")
X11_DISPLAY = os.environ.get("DISPLAY_NUM", ":1")
AUTOFILL_ENABLED = os.environ.get("IBKR_2FA_AUTOFILL", "yes").strip().lower() not in {
"0",
"false",
"no",
"off",
}

# Timing constants (seconds)
CHECK_INTERVAL = 3
Expand Down Expand Up @@ -72,6 +78,9 @@ class WindowCandidate:

def validate_config():
"""Validate required config at startup; exit immediately if invalid."""
if not AUTOFILL_ENABLED:
log.info("TOTP auto-fill is disabled; bot will only log auth popup detection")
return
if not SECRET_KEY:
log.error("TOTP_SECRET not found in environment variables")
sys.exit(1)
Expand Down Expand Up @@ -188,6 +197,16 @@ def focus_input_area(candidate):

def submit_totp(candidate):
"""Submit a TOTP code to the selected authentication popup."""
if not AUTOFILL_ENABLED:
log.info(
"Authentication window found (id=%s, title=%r, size=%sx%s); auto-fill disabled",
candidate.window_id,
candidate.title,
candidate.width or "?",
candidate.height or "?",
)
return

seconds_remaining = wait_for_fresh_totp_window()
log.info(
"Authentication window found (id=%s, title=%r, size=%sx%s); submitting code with %ss remaining",
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ VNC_SERVER_PASSWORD=your_vnc_password
TRADING_MODE=live
TWS_ACCEPT_INCOMING=accept
READ_ONLY_API=no
TWOFA_DEVICE=Mobile Device
TWOFA_TIMEOUT_ACTION=restart
RELOGIN_AFTER_TWOFA_TIMEOUT=yes
EXISTING_SESSION_DETECTED_ACTION=primary
IBKR_2FA_AUTOFILL=no
JAVA_HEAP_SIZE=512

# Recommended: use the exact CIDR used by your Cloud Run egress path
Expand All @@ -94,6 +96,8 @@ IB_GATEWAY_DEPLOY_PATH=/home/zwlddx0815/ib-docker
IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY=no
IB_GATEWAY_TWS_ACCEPT_INCOMING=accept
IB_GATEWAY_READ_ONLY_API=no
IB_GATEWAY_TWOFA_DEVICE=Mobile Device
IB_GATEWAY_2FA_AUTOFILL=no
```

The workflow maps these shared values to the gateway container's `.env`:
Expand All @@ -104,6 +108,8 @@ The workflow maps these shared values to the gateway container's `.env`:
- `IB_GATEWAY_ZONE` -> `GCE_ZONE`
- `IB_GATEWAY_GCE_USER` -> `GCE_USER`
- `IB_GATEWAY_DEPLOY_PATH` -> `DEPLOY_PATH`
- `IB_GATEWAY_TWOFA_DEVICE` -> `TWOFA_DEVICE`
- `IB_GATEWAY_2FA_AUTOFILL` -> `IBKR_2FA_AUTOFILL`

`ACCEPT_API_FROM_IP` is intentionally treated as required now. For manual `docker compose` usage, if you forget to set it, Compose will fail fast instead of starting a gateway that Cloud Run can never reach.

Expand Down Expand Up @@ -205,6 +211,8 @@ If you temporarily keep the values in GitHub Secrets during migration, you can r
| `IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY` | Set to `no` for Cloud Run private IP access |
| `IB_GATEWAY_TWS_ACCEPT_INCOMING` | Optional. Recommended `accept`. |
| `IB_GATEWAY_READ_ONLY_API` | Optional. Recommended `no` if this service places trades. |
| `IB_GATEWAY_TWOFA_DEVICE` | Optional. Exact IBC 2FA device name, for example `Mobile Device` for IBKR Mobile push. |
| `IB_GATEWAY_2FA_AUTOFILL` | Optional. Set to `no` when using IBKR Mobile push instead of local TOTP auto-fill. |

The current VM is an `e2-micro`, so the deployment intentionally sets `JAVA_HEAP_SIZE=512`
by default and enables a 2 GiB host swap file during keepalive/deploy. Without this,
Expand Down Expand Up @@ -358,9 +366,11 @@ VNC_SERVER_PASSWORD=your_vnc_password
TRADING_MODE=live
TWS_ACCEPT_INCOMING=accept
READ_ONLY_API=no
TWOFA_DEVICE=Mobile Device
TWOFA_TIMEOUT_ACTION=restart
RELOGIN_AFTER_TWOFA_TIMEOUT=yes
EXISTING_SESSION_DETECTED_ACTION=primary
IBKR_2FA_AUTOFILL=no
JAVA_HEAP_SIZE=512

ACCEPT_API_FROM_IP=10.8.0.0/26
Expand Down
4 changes: 4 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ services:
- READ_ONLY_API=${READ_ONLY_API:-no}
# Keep IBC aligned with the upstream image defaults so paper/live
# sessions recover after missed 2FA or stale-session prompts.
- TWOFA_DEVICE=${TWOFA_DEVICE:-}
- TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}
- RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes}
- EXISTING_SESSION_DETECTED_ACTION=${EXISTING_SESSION_DETECTED_ACTION:-primary}
# Set to no when IBC should use IBKR Mobile push approval instead of
# the local TOTP auto-fill helper.
- IBKR_2FA_AUTOFILL=${IBKR_2FA_AUTOFILL:-yes}
# e2-micro has less than 1 GiB RAM. The upstream image defaults to
# -Xmx768m, which can starve sshd/Docker/guest-agent. Keep this
# configurable, but use a safer default for the current VM.
Expand Down
2 changes: 2 additions & 0 deletions tests/test_docker_compose_ports.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ grep -Fq ' - "4001:4003"' "$compose_file"
grep -Fq ' - "4002:4004"' "$compose_file"
grep -Fq ' - TWS_ACCEPT_INCOMING=${TWS_ACCEPT_INCOMING:-accept}' "$compose_file"
grep -Fq ' - READ_ONLY_API=${READ_ONLY_API:-no}' "$compose_file"
grep -Fq ' - TWOFA_DEVICE=${TWOFA_DEVICE:-}' "$compose_file"
grep -Fq ' - TWOFA_TIMEOUT_ACTION=${TWOFA_TIMEOUT_ACTION:-restart}' "$compose_file"
grep -Fq ' - RELOGIN_AFTER_TWOFA_TIMEOUT=${RELOGIN_AFTER_TWOFA_TIMEOUT:-yes}' "$compose_file"
grep -Fq ' - EXISTING_SESSION_DETECTED_ACTION=${EXISTING_SESSION_DETECTED_ACTION:-primary}' "$compose_file"
grep -Fq ' - IBKR_2FA_AUTOFILL=${IBKR_2FA_AUTOFILL:-yes}' "$compose_file"
grep -Fq ' - JAVA_HEAP_SIZE=${JAVA_HEAP_SIZE:-512}' "$compose_file"
grep -Fq ' - IB_GATEWAY_PARALLEL_GC_THREADS=${IB_GATEWAY_PARALLEL_GC_THREADS:-2}' "$compose_file"
grep -Fq ' - IB_GATEWAY_CONC_GC_THREADS=${IB_GATEWAY_CONC_GC_THREADS:-1}' "$compose_file"
Expand Down
4 changes: 4 additions & 0 deletions tests/test_workflow_shared_config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ grep -Fq 'vars.IB_GATEWAY_CLOUD_RUN_EGRESS_CIDR' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_ALLOW_CONNECTIONS_FROM_LOCALHOST_ONLY' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_TWS_ACCEPT_INCOMING' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_READ_ONLY_API' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_TWOFA_DEVICE' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_2FA_AUTOFILL' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_SSH_PRIVATE_KEY_SECRET_NAME' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_TWS_USERID_SECRET_NAME' "$workflow_file"
grep -Fq 'vars.IB_GATEWAY_TWS_PASSWORD_SECRET_NAME' "$workflow_file"
Expand Down Expand Up @@ -71,6 +73,8 @@ done

grep -Fq '"TRADING_MODE": os.environ["IB_GATEWAY_MODE"]' "$workflow_file"
grep -Fq '"ACCEPT_API_FROM_IP": os.environ["CLOUD_RUN_EGRESS_CIDR"]' "$workflow_file"
grep -Fq '"TWOFA_DEVICE": os.environ.get("TWOFA_DEVICE", "")' "$workflow_file"
grep -Fq '"IBKR_2FA_AUTOFILL": os.environ.get("IBKR_2FA_AUTOFILL", "")' "$workflow_file"
grep -Fq 'REMOTE_DEPLOY_COMMAND=$(cat <<EOF' "$workflow_file"
if grep -Fq 'DEPLOY_SCRIPT=' "$workflow_file"; then
echo "Unexpected DEPLOY_SCRIPT temp upload flow still present" >&2
Expand Down