Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ RUN python -c "from cloakbrowser.download import ensure_binary; ensure_binary()"
EXPOSE 8080

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/status')" || exit 1
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/health')" || exit 1

VOLUME /data

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ When `AUTH_TOKEN` is set:
- The web UI shows a login page. Enter the token to unlock.
- API consumers pass the token via `Authorization: Bearer <token>` header.
- VNC WebSocket connections are authenticated via the login cookie.
- The `/api/status` endpoint remains unauthenticated (for Docker healthcheck).
- The `/api/health` endpoint remains unauthenticated (for Docker healthcheck); it exposes no system details. The `/api/status` endpoint (running counts, version) now requires authentication.

> **Note**: The auth token is transmitted in cleartext over HTTP. If you expose the Manager to the internet, put it behind a reverse proxy with HTTPS (Caddy, nginx, Traefik).

Expand Down
15 changes: 13 additions & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,12 @@

# Optional authentication via AUTH_TOKEN env var.
# If not set, all routes are open (local dev). If set, all /api/* routes
# (except /api/auth/* and /api/status) require Bearer token or cookie.
# (except /api/auth/status, /api/auth/login and /api/health) require Bearer
# token or cookie.
AUTH_TOKEN: str | None = os.environ.get("AUTH_TOKEN") or None

# Paths that bypass authentication even when AUTH_TOKEN is set
_AUTH_EXEMPT = frozenset({"/api/auth/status", "/api/auth/login", "/api/status"})
_AUTH_EXEMPT = frozenset({"/api/auth/status", "/api/auth/login", "/api/health"})


def _check_auth(scope: Scope) -> bool:
Expand Down Expand Up @@ -567,6 +568,16 @@ async def get_profile_status(profile_id: str):
# ── System Status ─────────────────────────────────────────────────────────────


@app.get("/api/health")
async def health_check():
"""Unauthenticated liveness probe for the Docker healthcheck.

Intentionally returns no system details; see /api/status (auth-gated)
for running counts and versions.
"""
return {"status": "ok"}


@app.get("/api/status", response_model=StatusResponse)
async def get_system_status():
from cloakbrowser.config import CHROMIUM_VERSION
Expand Down
12 changes: 11 additions & 1 deletion backend/tests/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,18 @@ def test_logout_clears_cookie(client_auth: TestClient):


def test_healthcheck_always_accessible(client_auth: TestClient):
"""GET /api/status must work without auth (Docker healthcheck)."""
"""GET /api/health must work without auth (Docker healthcheck)."""
resp = client_auth.get("/api/health")
assert resp.status_code == 200


def test_status_requires_auth(client_auth: TestClient):
"""GET /api/status must require auth (leaks profile count and version)."""
resp = client_auth.get("/api/status")
assert resp.status_code == 401
resp = client_auth.get(
"/api/status", headers={"Authorization": "Bearer test-secret"}
)
assert resp.status_code == 200


Expand Down