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:
root
2026-05-03 21:40:06 +00:00
parent 55bfeca88e
commit cddf88afb4
3 changed files with 197 additions and 0 deletions
+63
View File
@@ -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":"..."}'
```
+2
View File
@@ -4,6 +4,8 @@ services:
build: .
container_name: cerbero-mcp
env_file: .env
volumes:
- ./secrets:/secrets:ro
restart: unless-stopped
healthcheck:
test:
+132
View File
@@ -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())