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:
@@ -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()`
|
||||
|
||||
+11
-3
@@ -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
|
||||
|
||||
+49
-6
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user