| # deploy/Caddyfile.example — reference Caddy config for this box. | |
| # | |
| # The deployment runs TWO bots behind one Caddy: | |
| # | |
| # arb.elghaly.dev -> 127.0.0.1:8080 the Rust bot (basicauth, private) | |
| # bot.elghaly.dev -> 127.0.0.1:8081 this Python bot (public, read-only) | |
| # | |
| # Copy to /etc/caddy/Caddyfile, adjust, then: | |
| # | |
| # sudo caddy validate --config /etc/caddy/Caddyfile | |
| # sudo systemctl reload caddy | |
| # | |
| # ───────────────────────────────────────────────────────────────────────── | |
| # THE PATH ALLOWLIST IS THE POINT OF THIS FILE | |
| # ───────────────────────────────────────────────────────────────────────── | |
| # A bare `reverse_proxy 127.0.0.1:8081` publishes EVERY route the Python | |
| # bot serves, and two of them are not public routes: | |
| # | |
| # POST /command fund-moving. Runs /hunt, toggles ghost and mint mode. | |
| # GET /logs operational detail — wallet addresses, tx signatures, | |
| # route sizes, internal errors. | |
| # | |
| # Both are Bearer-token gated in the application (modules/bot_api_server.py) | |
| # and both are rate-limited and lockout-protected (modules/http_guard.py), | |
| # so a bare proxy is not an immediate compromise. It is still the wrong | |
| # shape. Anything that can move funds should be unreachable for two | |
| # independent reasons, so that no single mistake — a token in a shell | |
| # history, a bad `git add`, a regression in the auth check — is sufficient | |
| # on its own. | |
| # | |
| # So this config lists what MAY be reached and 404s everything else. Note | |
| # the direction: an allowlist fails closed, so a new endpoint added to the | |
| # Python server tomorrow is private until someone deliberately publishes | |
| # it. A blocklist would fail open and publish it by default. | |
| arb.elghaly.dev { | |
| basicauth { | |
| # Generate with: caddy hash-password | |
| # The hash is not a password, but it is still an offline-crackable | |
| # artifact — keep it out of screenshots and out of git. | |
| admin REPLACE_WITH_BCRYPT_HASH | |
| } | |
| reverse_proxy 127.0.0.1:8080 | |
| } | |
| bot.elghaly.dev { | |
| encode gzip | |
| # The public read-only surface. Nothing here can act, and nothing here | |
| # returns a secret — see modules/bot_api_server.py's own audit note for | |
| # the field-by-field reasoning. | |
| @public { | |
| path / | |
| path /status | |
| path /api/pnl | |
| path /api/receipts | |
| path /api/routes | |
| path /api/nearmiss | |
| path /api/engine | |
| path /receipts.csv | |
| } | |
| handle @public { | |
| reverse_proxy 127.0.0.1:8081 | |
| } | |
| # Everything else, including /command and /logs, does not exist as far | |
| # as the internet is concerned. 404 rather than 403: a 403 confirms the | |
| # path is real and worth attacking, which is free reconnaissance. | |
| handle { | |
| respond "not found" 404 | |
| } | |
| header { | |
| # This page loads no external anything — no CDN, no fonts, no | |
| # analytics — so the policy can be this tight honestly. 'unsafe-inline' | |
| # covers the one inline <style> and <script> the page is built from. | |
| Content-Security-Policy "default-src 'none'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" | |
| X-Content-Type-Options "nosniff" | |
| Referrer-Policy "no-referrer" | |
| X-Frame-Options "DENY" | |
| -Server | |
| } | |
| } | |