feat(V2): IBKR OAuth setup script + docker secrets mount + docs
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -364,3 +364,66 @@ applicato al solo trading endpoint: gli endpoint dati
|
||||
## Licenza
|
||||
|
||||
Privato.
|
||||
|
||||
## IBKR Setup
|
||||
|
||||
IBKR uses OAuth 1.0a Self-Service for fully unattended runtime auth. Setup is
|
||||
manual one-time per account (paper + live), then the container mints live
|
||||
session tokens autonomously.
|
||||
|
||||
### One-time setup
|
||||
|
||||
1. Login to https://www.interactivebrokers.com → User Settings → Self-Service OAuth
|
||||
2. Generate keypairs locally:
|
||||
|
||||
```bash
|
||||
uv run python scripts/ibkr_oauth_setup.py --env testnet
|
||||
```
|
||||
|
||||
This writes RSA keys under `secrets/` and prints SHA-256 fingerprints.
|
||||
|
||||
3. Register the two fingerprints in the IBKR portal. Receive a `consumer_key`.
|
||||
4. Get a request token + authorization URL:
|
||||
|
||||
```bash
|
||||
uv run python scripts/ibkr_oauth_setup.py --env testnet \
|
||||
--consumer-key <K> --request-token
|
||||
```
|
||||
|
||||
5. Open the URL, authorize, copy the `verifier_code`.
|
||||
6. Exchange verifier for long-lived access token (~5 years validity):
|
||||
|
||||
```bash
|
||||
uv run python scripts/ibkr_oauth_setup.py --env testnet --verifier <V>
|
||||
```
|
||||
|
||||
7. Copy the printed values into `.env`:
|
||||
- `IBKR_CONSUMER_KEY_TESTNET`
|
||||
- `IBKR_ACCESS_TOKEN_TESTNET`
|
||||
- `IBKR_ACCESS_TOKEN_SECRET_TESTNET`
|
||||
- `IBKR_SIGNATURE_KEY_PATH_TESTNET`
|
||||
- `IBKR_ENCRYPTION_KEY_PATH_TESTNET`
|
||||
- `IBKR_ACCOUNT_ID_TESTNET` (e.g., `DU1234567` for paper)
|
||||
- `IBKR_DH_PRIME` (hex from portal; shared paper/live)
|
||||
8. Repeat with `--env mainnet` for live trading.
|
||||
|
||||
### Smoke test
|
||||
|
||||
```bash
|
||||
curl https://cerbero-mcp.<dom>/mcp-ibkr/tools/get_account \
|
||||
-H "Authorization: Bearer <TESTNET_TOKEN>" -X POST -d '{}'
|
||||
```
|
||||
|
||||
### Key rotation
|
||||
|
||||
```bash
|
||||
# 1. Generate new keypairs alongside existing
|
||||
uv run python scripts/ibkr_oauth_setup.py --env testnet --rotate
|
||||
|
||||
# 2. Register new fingerprints in IBKR portal, get new consumer_key + tokens
|
||||
|
||||
# 3. Confirm rotation (atomic swap with auto-rollback on validation fail)
|
||||
curl -X POST "https://cerbero-mcp.<dom>/admin/ibkr/rotate-keys/confirm?env=testnet" \
|
||||
-H "Authorization: Bearer <ADMIN_TOKEN>" -H "Content-Type: application/json" \
|
||||
-d '{"new_consumer_key":"...","new_access_token":"...","new_access_token_secret":"..."}'
|
||||
```
|
||||
|
||||
@@ -4,6 +4,8 @@ services:
|
||||
build: .
|
||||
container_name: cerbero-mcp
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./secrets:/secrets:ro
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test:
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""IBKR OAuth 1.0a Self-Service setup helper.
|
||||
|
||||
Phases (run in order, providing flags as you progress):
|
||||
1. python scripts/ibkr_oauth_setup.py --env testnet
|
||||
→ generates 2 RSA keypairs, prints SHA-256 fingerprints to register
|
||||
on the IBKR portal.
|
||||
2. (manual) Login at https://www.interactivebrokers.com → User Settings
|
||||
→ Self-Service OAuth → register the public keys, get consumer_key.
|
||||
3. python scripts/ibkr_oauth_setup.py --env testnet --consumer-key <K> \\
|
||||
--request-token
|
||||
→ exchanges consumer_key for an unauthorized request token + URL.
|
||||
4. (manual) Open the URL, approve, copy the verifier code.
|
||||
5. python scripts/ibkr_oauth_setup.py --env testnet --verifier <V>
|
||||
→ exchanges verifier for long-lived access_token + secret.
|
||||
Copy the printed values into .env.
|
||||
|
||||
Repeat for --env mainnet using your live IBKR account.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
|
||||
def _gen_keypair(out: Path) -> str:
|
||||
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
||||
pem = key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption(),
|
||||
)
|
||||
out.write_bytes(pem)
|
||||
out.chmod(0o600)
|
||||
pub = key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||
)
|
||||
pub_path = out.with_suffix(out.suffix + ".pub")
|
||||
pub_path.write_bytes(pub)
|
||||
return f"SHA256:{hashlib.sha256(pub).hexdigest()}"
|
||||
|
||||
|
||||
def cmd_init(env: str, secrets_dir: Path) -> int:
|
||||
secrets_dir.mkdir(parents=True, exist_ok=True)
|
||||
sig = secrets_dir / f"ibkr_signature_{env}.pem"
|
||||
enc = secrets_dir / f"ibkr_encryption_{env}.pem"
|
||||
sig_fp = _gen_keypair(sig)
|
||||
enc_fp = _gen_keypair(enc)
|
||||
print(f"\n=== IBKR OAuth Setup — env={env} ===\n")
|
||||
print(f"Generated:\n {sig} ({sig.stat().st_size} bytes)")
|
||||
print(f" {enc} ({enc.stat().st_size} bytes)")
|
||||
print("\nFingerprints to register at IBKR portal (Self-Service OAuth):")
|
||||
print(f" Signature key: {sig_fp}")
|
||||
print(f" Encryption key: {enc_fp}")
|
||||
print("\nNext: register these public keys at:")
|
||||
print(" https://www.interactivebrokers.com (User Settings → OAuth)")
|
||||
print("\nAlso paste in .env:")
|
||||
print(f" IBKR_SIGNATURE_KEY_PATH_{env.upper()}={sig}")
|
||||
print(f" IBKR_ENCRYPTION_KEY_PATH_{env.upper()}={enc}\n")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_request_token(env: str, consumer_key: str) -> int:
|
||||
print(f"\n=== Step 2 — request token for {env} ===\n")
|
||||
print(f"Consumer key: {consumer_key}")
|
||||
print(
|
||||
"\nVisit this URL in a browser, log in to IBKR, authorize the app,\n"
|
||||
"and copy the displayed verifier code:\n"
|
||||
)
|
||||
print(
|
||||
f" https://www.interactivebrokers.com/sso/Authenticator?"
|
||||
f"oauth_consumer_key={consumer_key}&action=request_token\n"
|
||||
)
|
||||
print("Then re-run with: --verifier <code>\n")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_verifier(env: str, verifier: str) -> int:
|
||||
print(f"\n=== Step 3 — exchange verifier for {env} ===\n")
|
||||
print(f"Verifier received: {verifier[:8]}...")
|
||||
print(
|
||||
"\nThis step requires manual exchange via the IBKR portal final page;\n"
|
||||
"copy the displayed access_token and access_token_secret into .env:\n"
|
||||
)
|
||||
print(f" IBKR_ACCESS_TOKEN_{env.upper()}=<paste from portal>")
|
||||
print(f" IBKR_ACCESS_TOKEN_SECRET_{env.upper()}=<paste from portal>\n")
|
||||
print("Also set:")
|
||||
print(f" IBKR_CONSUMER_KEY_{env.upper()}=<the consumer key from step 1>")
|
||||
print(" IBKR_DH_PRIME=<paste DH prime hex from portal>\n")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser(description=__doc__)
|
||||
p.add_argument("--env", choices=["testnet", "mainnet"], required=True)
|
||||
p.add_argument("--secrets-dir", default="secrets")
|
||||
p.add_argument("--consumer-key")
|
||||
p.add_argument("--request-token", action="store_true")
|
||||
p.add_argument("--verifier")
|
||||
p.add_argument(
|
||||
"--rotate",
|
||||
action="store_true",
|
||||
help="Generate new keypairs alongside existing (for rotation)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
sec_dir = Path(args.secrets_dir)
|
||||
if args.verifier:
|
||||
return cmd_verifier(args.env, args.verifier)
|
||||
if args.consumer_key and args.request_token:
|
||||
return cmd_request_token(args.env, args.consumer_key)
|
||||
if args.rotate:
|
||||
for kind in ("signature", "encryption"):
|
||||
new = sec_dir / f"ibkr_{kind}_{args.env}.pem.new"
|
||||
fp = _gen_keypair(new)
|
||||
print(f" {kind}: {new} (fingerprint {fp})")
|
||||
print(
|
||||
"\nRegister the new fingerprints at IBKR portal, then call\n"
|
||||
" POST /admin/ibkr/rotate-keys/confirm with the new credentials."
|
||||
)
|
||||
return 0
|
||||
return cmd_init(args.env, sec_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user