mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
fix(security): fail closed in production; arm registry auth by default
GHSA-4f7g-w95g-5q2c (CVSS 9.8) — the default registry deploy ran in
header-trust mode: with ROBOCO_AGENT_AUTH_REQUIRED unset and cloud auth
off, require_panel_token / _check_agent_auth_token returned without
verifying a credential, so any client reaching the API could write
settings and claim X-Agent-Role: ceo with no token. Binding :8000 to
loopback (c4053d5f) closed the direct path but not nginx :3000, which
proxies /api/ to the orchestrator and passes client X-Agent-* through.
Root cause: header-trust is the default even in production. _auth_required
now fails closed when settings.environment == production (the registry
compose already declares it) — an explicit false still opts out for a
trusted private network. The registry compose arms auth by default and
requires ROBOCO_PANEL_AGENT_TOKEN so nginx injects a valid CEO token and
the panel keeps working. The CEO's NAS deploy is unaffected: it runs
cloud auth, which already enforced tokens on every role.
This commit is contained in:
@@ -314,7 +314,12 @@ services:
|
|||||||
ROBOCO_PORT: 8000
|
ROBOCO_PORT: 8000
|
||||||
ROBOCO_ENCRYPTION_KEY: ${ROBOCO_ENCRYPTION_KEY:?ROBOCO_ENCRYPTION_KEY is required}
|
ROBOCO_ENCRYPTION_KEY: ${ROBOCO_ENCRYPTION_KEY:?ROBOCO_ENCRYPTION_KEY is required}
|
||||||
ROBOCO_AGENT_AUTH_SECRET: ${ROBOCO_AGENT_AUTH_SECRET:?ROBOCO_AGENT_AUTH_SECRET is required}
|
ROBOCO_AGENT_AUTH_SECRET: ${ROBOCO_AGENT_AUTH_SECRET:?ROBOCO_AGENT_AUTH_SECRET is required}
|
||||||
ROBOCO_AGENT_AUTH_REQUIRED: ${ROBOCO_AGENT_AUTH_REQUIRED:-false}
|
# Fail closed by default (GHSA-4f7g-w95g-5q2c): this deploy is
|
||||||
|
# ROBOCO_ENVIRONMENT=production, and header-trust off a network-reachable
|
||||||
|
# port lets any client claim X-Agent-Role: ceo. nginx injects the CEO
|
||||||
|
# token below so the panel keeps working. Set false only on a trusted
|
||||||
|
# private network with no untrusted reach to nginx.
|
||||||
|
ROBOCO_AGENT_AUTH_REQUIRED: ${ROBOCO_AGENT_AUTH_REQUIRED:-true}
|
||||||
ROBOCO_LOCAL_LLM_BASE_URL: http://roboco-ollama:11434/v1
|
ROBOCO_LOCAL_LLM_BASE_URL: http://roboco-ollama:11434/v1
|
||||||
ROBOCO_LOCAL_LLM_MODEL: glm-5.2:cloud
|
ROBOCO_LOCAL_LLM_MODEL: glm-5.2:cloud
|
||||||
ROBOCO_DEFAULT_EMBEDDING_MODEL: qwen3-embedding:0.6b
|
ROBOCO_DEFAULT_EMBEDDING_MODEL: qwen3-embedding:0.6b
|
||||||
@@ -476,7 +481,10 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:80"
|
- "3000:80"
|
||||||
environment:
|
environment:
|
||||||
ROBOCO_PANEL_AGENT_TOKEN: ${ROBOCO_PANEL_AGENT_TOKEN:-}
|
# Required when cloud auth is off: with auth armed, nginx must inject a
|
||||||
|
# valid CEO token or the panel is locked out (GHSA-4f7g-w95g-5q2c). Mint
|
||||||
|
# one with the agent-auth secret; see docs/backend/ops.
|
||||||
|
ROBOCO_PANEL_AGENT_TOKEN: ${ROBOCO_PANEL_AGENT_TOKEN:?ROBOCO_PANEL_AGENT_TOKEN is required (mint a CEO token from ROBOCO_AGENT_AUTH_SECRET) unless ROBOCO_CLOUD_AUTH_ENABLED=true}
|
||||||
NGINX_ENVSUBST_FILTER: "^ROBOCO_"
|
NGINX_ENVSUBST_FILTER: "^ROBOCO_"
|
||||||
volumes:
|
volumes:
|
||||||
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template:ro
|
- ./docker/nginx.conf:/etc/nginx/templates/default.conf.template:ro
|
||||||
|
|||||||
+12
-2
@@ -230,9 +230,19 @@ OptionalAgentId = Annotated[UUID | None, Depends(get_optional_agent_id)]
|
|||||||
|
|
||||||
|
|
||||||
def _auth_required() -> bool:
|
def _auth_required() -> bool:
|
||||||
"""True when agent HMAC auth is mandatory (prod-ish) vs opt-in (dev)."""
|
"""True when agent HMAC auth is mandatory (prod-ish) vs opt-in (dev).
|
||||||
|
|
||||||
|
An UNSET flag fails closed in production: a public deployment must not sit
|
||||||
|
in header-trust mode, where any client reaching the API can claim
|
||||||
|
``X-Agent-Role: ceo`` without a signed token (GHSA-4f7g-w95g-5q2c). An
|
||||||
|
explicit opt-out still stands for a trusted private-network prod deploy.
|
||||||
|
"""
|
||||||
val = os.environ.get("ROBOCO_AGENT_AUTH_REQUIRED", "").strip().lower()
|
val = os.environ.get("ROBOCO_AGENT_AUTH_REQUIRED", "").strip().lower()
|
||||||
return val in ("1", "true", "yes")
|
if val in ("1", "true", "yes"):
|
||||||
|
return True
|
||||||
|
if val in ("0", "false", "no"):
|
||||||
|
return False
|
||||||
|
return settings.environment == "production"
|
||||||
|
|
||||||
|
|
||||||
def _check_agent_auth_token(
|
def _check_agent_auth_token(
|
||||||
|
|||||||
@@ -184,12 +184,30 @@ def test_auth_required_truthy_values(monkeypatch: pytest.MonkeyPatch) -> None:
|
|||||||
assert _auth_required() is True
|
assert _auth_required() is True
|
||||||
|
|
||||||
|
|
||||||
def test_auth_required_falsy_values(monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_auth_required_explicit_false_values(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
for v in ("0", "no", "false", ""):
|
# An explicit opt-out is honored in every environment (trusted private net).
|
||||||
|
monkeypatch.setattr(_deps.settings, "environment", "production")
|
||||||
|
for v in ("0", "no", "false"):
|
||||||
monkeypatch.setenv("ROBOCO_AGENT_AUTH_REQUIRED", v)
|
monkeypatch.setenv("ROBOCO_AGENT_AUTH_REQUIRED", v)
|
||||||
assert _auth_required() is False
|
assert _auth_required() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_required_unset_fails_open_in_dev(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
|
monkeypatch.delenv("ROBOCO_AGENT_AUTH_REQUIRED", raising=False)
|
||||||
|
monkeypatch.setattr(_deps.settings, "environment", "development")
|
||||||
|
assert _auth_required() is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_required_unset_fails_closed_in_production(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> None:
|
||||||
|
# GHSA-4f7g-w95g-5q2c: an unset flag must not leave a production deploy in
|
||||||
|
# header-trust mode where any client can claim X-Agent-Role: ceo.
|
||||||
|
monkeypatch.delenv("ROBOCO_AGENT_AUTH_REQUIRED", raising=False)
|
||||||
|
monkeypatch.setattr(_deps.settings, "environment", "production")
|
||||||
|
assert _auth_required() is True
|
||||||
|
|
||||||
|
|
||||||
def test_check_agent_auth_token_no_token_in_dev_passes(
|
def test_check_agent_auth_token_no_token_in_dev_passes(
|
||||||
monkeypatch: pytest.MonkeyPatch,
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user