From 867180f4bffdfdfac19ff64c92130c3d70214d73 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Mon, 27 Apr 2026 23:24:06 +0200 Subject: [PATCH] feat(gateway): TLS auto + rate limit + IP allowlist su write endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configura il gateway Caddy per il deploy su cerbero-mcp.tielogic.xyz: - Build custom Caddy con plugin mholt/caddy-ratelimit (Dockerfile + build via xcaddy). - TLS automatico via Let's Encrypt (richiede DNS A record + porte 80/443 raggiungibili), HSTS preload, header di sicurezza. - Rate limit per IP (60 req/min sui read, 10 req/min sui write, sliding window). - Allowlist IP sui write endpoint (place_*, cancel_*, set_*, close_*, transfer_*, amend_*, switch_*): IP non in WRITE_ALLOWLIST → 403. - Default WRITE_ALLOWLIST copre loopback + Docker bridge: bot sulla stessa macchina (host o container) funziona senza configurazione, IP pubblici esterni vanno aggiunti esplicitamente. - Smoke test e README aggiornati per il nuovo URL gateway. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 46 ++++++++++++++++++++++++++++++++++++ docker-compose.yml | 14 ++++++++--- gateway/Caddyfile | 55 ++++++++++++++++++++++++++++++++++++++----- gateway/Dockerfile | 6 +++++ tests/smoke/README.md | 2 +- tests/smoke/run.sh | 2 +- 6 files changed, 114 insertions(+), 11 deletions(-) create mode 100644 gateway/Dockerfile diff --git a/README.md b/README.md index ff088cd..3011146 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,52 @@ bash tests/smoke/run.sh Vedi `secrets/*.json` e variabili `*_TESTNET` / `ALPACA_PAPER` in `docker-compose.yml` per override ambiente. +### Deploy su VPS pubblica (`cerbero-mcp.tielogic.xyz`) + +Il gateway Caddy è configurato per: +- TLS automatico via Let's Encrypt (richiede DNS A/AAAA che punti al + VPS e porte 80+443 raggiungibili). +- HSTS preload, header di sicurezza (`X-Content-Type-Options`, + `X-Frame-Options`, `Referrer-Policy`). +- Rate limit per IP (60 req/min su read, 10 req/min su write) tramite + plugin `mholt/caddy-ratelimit`. +- Allowlist IP sui write endpoint (`place_*`, `cancel_*`, `set_*`, + `close_*`, `transfer_*`, `amend_*`, `switch_*`): IP non presenti in + `WRITE_ALLOWLIST` ricevono `403 forbidden`. + +Variabili d'ambiente per il deploy: + +```bash +# .env (su VPS) +ACME_EMAIL=adrianodalpastro@tielogic.com +GATEWAY_HTTP_PORT=80 +GATEWAY_HTTPS_PORT=443 + +# Allowlist write endpoint (CIDR space-separated). Default copre: +# - loopback IPv4/IPv6 (bot sull'host VPS chiama http://localhost) +# - Docker bridge 172.16.0.0/12 (bot in container nella stessa compose network) +# Aggiungi gli IP pubblici dei tuoi bot esterni se li hai. +WRITE_ALLOWLIST="127.0.0.1/32 ::1/128 172.16.0.0/12 1.2.3.4/32" +``` + +Tre scenari per il trading bot: +1. Bot container nella stessa compose network → chiama `http://gateway:80` + internamente. Source IP = Docker bridge → coperto dalla default. +2. Bot processo sull'host VPS → chiama `http://localhost`. Source IP = + `127.0.0.1` → coperto dalla default. +3. Bot esterno (laptop, altro server) → chiama + `https://cerbero-mcp.tielogic.xyz` con TLS. Devi aggiungere l'IP + pubblico del bot in `WRITE_ALLOWLIST`. + +Senza configurare `WRITE_ALLOWLIST` la default è loopback + Docker bridge: +nessun IP pubblico esterno può triggerare ordini. + +Sull'host VPS i secret devono avere permessi restrittivi: + +```bash +chmod 600 secrets/*.json secrets/*.token +``` + ### Risoluzione environment (testnet/mainnet) Ogni servizio exchange usa `mcp_common.environment.resolve_environment()` diff --git a/docker-compose.yml b/docker-compose.yml index 5398ab1..8d16c04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,12 +36,20 @@ services: # GATEWAY — unica porta host, reverse proxy + landing page # ======================================================== gateway: - image: caddy:2-alpine + build: + context: ./gateway + dockerfile: Dockerfile + image: cerbero-gateway:dev restart: unless-stopped networks: [internal] security_opt: - no-new-privileges:true - ports: ["${GATEWAY_PORT:-8080}:8080"] + ports: + - "${GATEWAY_HTTP_PORT:-80}:80" + - "${GATEWAY_HTTPS_PORT:-443}:443" + environment: + ACME_EMAIL: ${ACME_EMAIL:-adrianodalpastro@tielogic.com} + WRITE_ALLOWLIST: ${WRITE_ALLOWLIST:-127.0.0.1/32 ::1/128 172.16.0.0/12} volumes: - ./gateway/Caddyfile:/etc/caddy/Caddyfile:ro - ./gateway/public:/srv:ro @@ -55,7 +63,7 @@ services: mcp-macro: { condition: service_healthy } mcp-sentiment: { condition: service_healthy } healthcheck: - test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"] + test: ["CMD", "wget", "-q", "--spider", "http://localhost/"] interval: 30s timeout: 5s retries: 3 diff --git a/gateway/Caddyfile b/gateway/Caddyfile index 140b3d9..9ecf5ae 100644 --- a/gateway/Caddyfile +++ b/gateway/Caddyfile @@ -1,23 +1,66 @@ { admin off - auto_https off + email {$ACME_EMAIL:adrianodalpastro@tielogic.com} + + # Plugin mholt/caddy-ratelimit + order rate_limit before basicauth } -:8080 { +cerbero-mcp.tielogic.xyz { log { output stdout - format console + format json } + # ───── Security headers ───── + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + X-Frame-Options "DENY" + Referrer-Policy "no-referrer" + -Server + } + + # ───── IP allowlist su endpoint write ───── + # WRITE_ALLOWLIST: CIDR space-separated (es. "1.2.3.4/32 5.6.7.0/24"). + # Default 127.0.0.1/32 — fail-closed se non configurato. + @writes_blocked { + path_regexp ^/mcp-[a-z]+/tools/(place_|cancel_|set_|close_|transfer_|amend_|switch_) + not remote_ip {$WRITE_ALLOWLIST:127.0.0.1/32 ::1/128 172.16.0.0/12} + } + respond @writes_blocked "forbidden: source ip not in allowlist" 403 + + # ───── Rate limit ───── + # Reads: 60 req/min/IP, writes: 10 req/min/IP (sliding window). + rate_limit { + zone reads { + match { + not path_regexp ^/mcp-[a-z]+/tools/(place_|cancel_|set_|close_|transfer_|amend_|switch_) + } + key {remote_ip} + events 60 + window 1m + } + zone writes { + match { + path_regexp ^/mcp-[a-z]+/tools/(place_|cancel_|set_|close_|transfer_|amend_|switch_) + } + key {remote_ip} + events 10 + window 1m + } + } + + # ───── Reverse proxy ───── handle_path /mcp-deribit/* { reverse_proxy mcp-deribit:9011 } - handle_path /mcp-hyperliquid/* { - reverse_proxy mcp-hyperliquid:9012 - } handle_path /mcp-bybit/* { reverse_proxy mcp-bybit:9019 } + handle_path /mcp-hyperliquid/* { + reverse_proxy mcp-hyperliquid:9012 + } handle_path /mcp-alpaca/* { reverse_proxy mcp-alpaca:9020 } diff --git a/gateway/Dockerfile b/gateway/Dockerfile new file mode 100644 index 0000000..dd0015a --- /dev/null +++ b/gateway/Dockerfile @@ -0,0 +1,6 @@ +FROM caddy:2.8-builder-alpine AS builder +RUN xcaddy build \ + --with github.com/mholt/caddy-ratelimit + +FROM caddy:2.8-alpine +COPY --from=builder /usr/bin/caddy /usr/bin/caddy diff --git a/tests/smoke/README.md b/tests/smoke/README.md index 8f94d52..c140951 100644 --- a/tests/smoke/README.md +++ b/tests/smoke/README.md @@ -19,7 +19,7 @@ Il file `run.sh` verifica: - sentiment `get_funding_rates BTC` Variabili di ambiente: -- `GATEWAY` — URL base gateway (default `http://localhost:8080`) +- `GATEWAY` — URL base gateway (default `http://localhost`; in produzione `https://cerbero-mcp.tielogic.xyz`) - `TOKEN_FILE` — path al token bearer di lettura (default `secrets/observer.token`) Exit code 0 = tutto OK, 1 = uno o più check falliti. diff --git a/tests/smoke/run.sh b/tests/smoke/run.sh index b386b9e..fe18c54 100755 --- a/tests/smoke/run.sh +++ b/tests/smoke/run.sh @@ -7,7 +7,7 @@ set -euo pipefail cd "$(dirname "$0")/../.." -GATEWAY="${GATEWAY:-http://localhost:8080}" +GATEWAY="${GATEWAY:-http://localhost}" TOKEN_FILE="${TOKEN_FILE:-secrets/observer.token}" if [ ! -f "$TOKEN_FILE" ]; then