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
|
## Licenza
|
||||||
|
|
||||||
Privato.
|
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: .
|
build: .
|
||||||
container_name: cerbero-mcp
|
container_name: cerbero-mcp
|
||||||
env_file: .env
|
env_file: .env
|
||||||
|
volumes:
|
||||||
|
- ./secrets:/secrets:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
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