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
|
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
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`
|
- 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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user