feat(gateway): TLS auto + rate limit + IP allowlist su write endpoint

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) <noreply@anthropic.com>
This commit is contained in:
AdrianoDev
2026-04-27 23:24:06 +02:00
parent c2fd8330ca
commit 867180f4bf
6 changed files with 114 additions and 11 deletions
+46
View File
@@ -27,6 +27,52 @@ bash tests/smoke/run.sh
Vedi `secrets/*.json` e variabili `*_TESTNET` / `ALPACA_PAPER` in Vedi `secrets/*.json` e variabili `*_TESTNET` / `ALPACA_PAPER` in
`docker-compose.yml` per override ambiente. `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) ### Risoluzione environment (testnet/mainnet)
Ogni servizio exchange usa `mcp_common.environment.resolve_environment()` Ogni servizio exchange usa `mcp_common.environment.resolve_environment()`
+11 -3
View File
@@ -36,12 +36,20 @@ services:
# GATEWAY — unica porta host, reverse proxy + landing page # GATEWAY — unica porta host, reverse proxy + landing page
# ======================================================== # ========================================================
gateway: gateway:
image: caddy:2-alpine build:
context: ./gateway
dockerfile: Dockerfile
image: cerbero-gateway:dev
restart: unless-stopped restart: unless-stopped
networks: [internal] networks: [internal]
security_opt: security_opt:
- no-new-privileges:true - 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: volumes:
- ./gateway/Caddyfile:/etc/caddy/Caddyfile:ro - ./gateway/Caddyfile:/etc/caddy/Caddyfile:ro
- ./gateway/public:/srv:ro - ./gateway/public:/srv:ro
@@ -55,7 +63,7 @@ services:
mcp-macro: { condition: service_healthy } mcp-macro: { condition: service_healthy }
mcp-sentiment: { condition: service_healthy } mcp-sentiment: { condition: service_healthy }
healthcheck: healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8080/"] test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
interval: 30s interval: 30s
timeout: 5s timeout: 5s
retries: 3 retries: 3
+49 -6
View File
@@ -1,23 +1,66 @@
{ {
admin off 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 { log {
output stdout 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/* { handle_path /mcp-deribit/* {
reverse_proxy mcp-deribit:9011 reverse_proxy mcp-deribit:9011
} }
handle_path /mcp-hyperliquid/* {
reverse_proxy mcp-hyperliquid:9012
}
handle_path /mcp-bybit/* { handle_path /mcp-bybit/* {
reverse_proxy mcp-bybit:9019 reverse_proxy mcp-bybit:9019
} }
handle_path /mcp-hyperliquid/* {
reverse_proxy mcp-hyperliquid:9012
}
handle_path /mcp-alpaca/* { handle_path /mcp-alpaca/* {
reverse_proxy mcp-alpaca:9020 reverse_proxy mcp-alpaca:9020
} }
+6
View File
@@ -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
+1 -1
View File
@@ -19,7 +19,7 @@ Il file `run.sh` verifica:
- sentiment `get_funding_rates BTC` - sentiment `get_funding_rates BTC`
Variabili di ambiente: 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`) - `TOKEN_FILE` — path al token bearer di lettura (default `secrets/observer.token`)
Exit code 0 = tutto OK, 1 = uno o più check falliti. Exit code 0 = tutto OK, 1 = uno o più check falliti.
+1 -1
View File
@@ -7,7 +7,7 @@ set -euo pipefail
cd "$(dirname "$0")/../.." cd "$(dirname "$0")/../.."
GATEWAY="${GATEWAY:-http://localhost:8080}" GATEWAY="${GATEWAY:-http://localhost}"
TOKEN_FILE="${TOKEN_FILE:-secrets/observer.token}" TOKEN_FILE="${TOKEN_FILE:-secrets/observer.token}"
if [ ! -f "$TOKEN_FILE" ]; then if [ ! -f "$TOKEN_FILE" ]; then