Make the API token fully optional #5
No files matched your search
+4
-2
@@ -44,8 +44,10 @@ MAX_POSITION_PCT=1.0
|
|||||||
MAX_TRADES_PER_DAY=0
|
MAX_TRADES_PER_DAY=0
|
||||||
|
|
||||||
# --- API token (optional) ---
|
# --- API token (optional) ---
|
||||||
# When set, /api requires this token. The frontend nginx injects it server-side,
|
# When set, /api and the /api/ws live feed require this token. The frontend
|
||||||
# so the browser never sees it. Leave empty to disable.
|
# nginx injects it server-side, so the browser never sees it. Leave empty for
|
||||||
|
# no token; the app works fully without it. (The Traefik Basic auth still
|
||||||
|
# guards the UI in the deployed stack.)
|
||||||
API_TOKEN=
|
API_TOKEN=
|
||||||
API_TOKEN_HEADER=X-API-Token
|
API_TOKEN_HEADER=X-API-Token
|
||||||
|
|
||||||
|
|||||||
@@ -147,13 +147,13 @@ jobs:
|
|||||||
echo "::error::CRYPTO_FRONTEND_AUTH_USERS is not set (repo/org variable or secret); refusing to deploy"
|
echo "::error::CRYPTO_FRONTEND_AUTH_USERS is not set (repo/org variable or secret); refusing to deploy"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# /api/ws bypasses Basic auth (browsers don't reliably replay
|
# CRYPTO_API_TOKEN is optional. When unset, the API and the /api/ws
|
||||||
# credentials on a WebSocket upgrade) and relies on the API token
|
# live feed are unauthenticated beyond the Traefik Basic auth that
|
||||||
# instead. Refuse to ship if that token is missing, or the live feed
|
# still guards everything except /api/ws. Set it to require a bearer
|
||||||
# would be unauthenticated.
|
# token on /api (nginx injects it server-side, so the browser never
|
||||||
|
# sees it).
|
||||||
if [ -z "$CRYPTO_API_TOKEN" ]; then
|
if [ -z "$CRYPTO_API_TOKEN" ]; then
|
||||||
echo "::error::CRYPTO_API_TOKEN must be set because /api/ws skips Basic auth; refusing to deploy"
|
echo "Note: CRYPTO_API_TOKEN is not set; /api and /api/ws will not require a bearer token."
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub (optional)
|
- name: Log in to Docker Hub (optional)
|
||||||
|
|||||||
@@ -164,10 +164,10 @@ real migration step.
|
|||||||
depend on `GuardedRuntimeDep`, not `RuntimeDep` (which no longer exists).
|
depend on `GuardedRuntimeDep`, not `RuntimeDep` (which no longer exists).
|
||||||
- `/api/ws` skips Traefik Basic auth via a dedicated higher-priority router
|
- `/api/ws` skips Traefik Basic auth via a dedicated higher-priority router
|
||||||
(`crypto-trading-ws`) because browsers don't reliably replay Basic creds on a
|
(`crypto-trading-ws`) because browsers don't reliably replay Basic creds on a
|
||||||
WS upgrade. The WS is gated by `API_TOKEN` instead (header, injected by nginx,
|
WS upgrade. When `API_TOKEN` is set the WS requires it (header, injected by
|
||||||
or `?token=` for manual clients) — see `websocket_endpoint`. The deploy job
|
nginx, or `?token=` for manual clients); when unset the path is open, matching
|
||||||
fails closed if `CRYPTO_API_TOKEN` is unset. Keep the `/api/ws` block in
|
the rest of `/api`. `CRYPTO_API_TOKEN` is optional. Keep the `/api/ws` block
|
||||||
`nginx.conf` separate from `/api/` so upgrade headers are only set there.
|
in `nginx.conf` separate from `/api/` so upgrade headers are only set there.
|
||||||
|
|
||||||
## Backtesting
|
## Backtesting
|
||||||
|
|
||||||
|
|||||||
@@ -136,9 +136,10 @@ auth, not instead of it.
|
|||||||
`/api/ws` is special: browsers do not reliably replay Basic-auth credentials on
|
`/api/ws` is special: browsers do not reliably replay Basic-auth credentials on
|
||||||
a WebSocket upgrade (which left the live feed stuck on "reconnecting"), so
|
a WebSocket upgrade (which left the live feed stuck on "reconnecting"), so
|
||||||
Traefik serves that one path on a separate higher-priority router **without**
|
Traefik serves that one path on a separate higher-priority router **without**
|
||||||
Basic auth. It is still gated by `API_TOKEN`, which nginx injects on the
|
Basic auth. If `API_TOKEN` is set, the WebSocket still requires it (nginx
|
||||||
handshake. Because of this the deploy **requires** `CRYPTO_API_TOKEN` to be set —
|
injects it on the handshake). If `API_TOKEN` is unset, `/api/ws` is as open as
|
||||||
without it the WebSocket would be unauthenticated.
|
the rest of `/api`, which is fine for a trusted/private deployment but is the
|
||||||
|
reason to set a token if the host is exposed.
|
||||||
|
|
||||||
## Risk controls
|
## Risk controls
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,29 @@ def test_api_token_enforced(runtime: Runtime) -> None:
|
|||||||
assert client.get("/api/health").status_code == 200
|
assert client.get("/api/health").status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_websocket_connects_without_any_token(runtime: Runtime) -> None:
|
||||||
|
# API_TOKEN is unset by default: the API and the live feed must work with no
|
||||||
|
# token at all, even when (as nginx does) an empty token header is sent.
|
||||||
|
runtime.settings.api_token = ""
|
||||||
|
app = create_app(runtime.settings, runtime)
|
||||||
|
with (
|
||||||
|
TestClient(app) as client,
|
||||||
|
client.websocket_connect("/api/ws", headers={"X-API-Token": ""}) as websocket,
|
||||||
|
):
|
||||||
|
message = websocket.receive_json()
|
||||||
|
assert message["type"] == "status"
|
||||||
|
assert message["data"]["running"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_works_without_token(runtime: Runtime) -> None:
|
||||||
|
runtime.settings.api_token = ""
|
||||||
|
app = create_app(runtime.settings, runtime)
|
||||||
|
with TestClient(app) as client:
|
||||||
|
assert client.get("/api/status").status_code == 200
|
||||||
|
assert client.get("/api/strategies").status_code == 200
|
||||||
|
assert client.post("/api/backtest", json={"strategy_key": "sma_cross"}).status_code != 401
|
||||||
|
|
||||||
|
|
||||||
def test_websocket_rejects_missing_token(runtime: Runtime) -> None:
|
def test_websocket_rejects_missing_token(runtime: Runtime) -> None:
|
||||||
runtime.settings.api_token = "secret"
|
runtime.settings.api_token = "secret"
|
||||||
app = create_app(runtime.settings, runtime)
|
app = create_app(runtime.settings, runtime)
|
||||||
|
|||||||
+3
-4
@@ -98,10 +98,9 @@ services:
|
|||||||
- "traefik.http.middlewares.crypto-trading-auth.basicauth.users=${CRYPTO_FRONTEND_AUTH_USERS:?CRYPTO_FRONTEND_AUTH_USERS is required (htpasswd string, e.g. htpasswd -nbB user pass)}"
|
- "traefik.http.middlewares.crypto-trading-auth.basicauth.users=${CRYPTO_FRONTEND_AUTH_USERS:?CRYPTO_FRONTEND_AUTH_USERS is required (htpasswd string, e.g. htpasswd -nbB user pass)}"
|
||||||
# Browsers do not reliably replay Basic-auth credentials on a WebSocket
|
# Browsers do not reliably replay Basic-auth credentials on a WebSocket
|
||||||
# upgrade, which left the live feed stuck "reconnecting". Give /api/ws its
|
# upgrade, which left the live feed stuck "reconnecting". Give /api/ws its
|
||||||
# own higher-priority router that skips Basic auth; the backend still
|
# own higher-priority router that skips Basic auth so the feed connects.
|
||||||
# requires the API token, which nginx injects server-side. When no API
|
# If CRYPTO_API_TOKEN is set, the backend still requires it (nginx injects
|
||||||
# token is configured this path is unauthenticated, so only use it on a
|
# it server-side); if it is unset, /api/ws is as open as the rest of /api.
|
||||||
# trusted network.
|
|
||||||
- "traefik.http.routers.crypto-trading-ws.rule=Host(`crypto.${BASE_DOMAIN:-matitos.org}`) && PathPrefix(`/api/ws`)"
|
- "traefik.http.routers.crypto-trading-ws.rule=Host(`crypto.${BASE_DOMAIN:-matitos.org}`) && PathPrefix(`/api/ws`)"
|
||||||
- "traefik.http.routers.crypto-trading-ws.entrypoints=websecure"
|
- "traefik.http.routers.crypto-trading-ws.entrypoints=websecure"
|
||||||
- "traefik.http.routers.crypto-trading-ws.tls.certresolver=myresolvercd"
|
- "traefik.http.routers.crypto-trading-ws.tls.certresolver=myresolvercd"
|
||||||
|
|||||||
+2
-2
@@ -9,8 +9,8 @@ server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket feed. Kept in its own block so the upgrade headers are only set
|
# WebSocket feed. Kept in its own block so the upgrade headers are only set
|
||||||
# here, and so the API token is injected server-side on the handshake (the
|
# here, and so the optional API token is injected server-side on the
|
||||||
# browser cannot set headers on a WebSocket).
|
# handshake (the browser cannot set headers on a WebSocket).
|
||||||
location /api/ws {
|
location /api/ws {
|
||||||
proxy_pass http://backend:8000;
|
proxy_pass http://backend:8000;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|||||||
Reference in new issue
Block a user