Files
OpenJarvis/tests/deploy/test_deploy_auth.py
krypticmouseandClaude Opus 4.7 9cd760ad1e security(deploy): stop default deployments shipping an open, unauthenticated server (#221)
All three deployment methods bound 0.0.0.0:8000 with no API key, so following
the README produced a server reachable from any device on the network with no
auth. `check_bind_safety` already refuses to start a non-loopback bind without
a key (so these configs actually failed to start) — this wires the key in so
the documented path yields a *working, authenticated* server.

- docker-compose.yml: require `OPENJARVIS_API_KEY` via `${VAR:?...}` so
  `docker compose up` fails fast when unset; added `deploy/docker/.env.example`
  (un-ignored in .gitignore).
- systemd: add `EnvironmentFile=/etc/openjarvis/env` (no `-` prefix, so a
  missing key file blocks startup rather than exposing an open server).
- launchd: bind `127.0.0.1` by default (the personal-device default — no
  network exposure, no key needed) with a documented, commented opt-in to
  0.0.0.0 + `OPENJARVIS_API_KEY`. Avoids shipping a usable default credential.
- Docs (docker/systemd/launchd) updated with the key-setup step.
- Tests assert each config can't reintroduce an open server, plus
  `check_bind_safety` behavior across loopback/public × key/no-key.

Closes #221

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 23:56:55 +00:00

70 lines
2.2 KiB
Python

"""Deployment configs must not ship an unauthenticated public server (#221).
Every shipped deployment method must either bind loopback (no network
exposure) or require an API key, so that following the docs never yields an
open `0.0.0.0:8000` server. `check_bind_safety` is the runtime backstop;
these tests guard the static config files that drive it.
"""
from __future__ import annotations
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
DEPLOY = REPO_ROOT / "deploy"
def _read(rel: str) -> str:
return (DEPLOY / rel).read_text()
def test_docker_compose_requires_api_key():
text = _read("docker/docker-compose.yml")
# The container binds 0.0.0.0, so the key must be a *required* variable
# (compose's ${VAR:?...} fails fast when unset).
assert "OPENJARVIS_API_KEY" in text
assert "OPENJARVIS_API_KEY:?" in text
def test_docker_env_example_present():
assert (DEPLOY / "docker" / ".env.example").is_file()
assert "OPENJARVIS_API_KEY" in _read("docker/.env.example")
def test_systemd_unit_binds_public_and_requires_env_file():
text = _read("systemd/openjarvis.service")
# Public bind -> must pull in an EnvironmentFile (no leading '-', so the
# unit fails to start if it's missing).
assert "--host 0.0.0.0" in text
assert "EnvironmentFile=/etc/openjarvis/env" in text
assert "\n-EnvironmentFile" not in text and "=-/etc" not in text
def test_launchd_plist_binds_loopback():
text = _read("launchd/com.openjarvis.plist")
# Personal-device default: loopback, not the network.
assert "<string>127.0.0.1</string>" in text
assert "<string>0.0.0.0</string>" not in text
@pytest.mark.parametrize(
("host", "api_key", "should_exit"),
[
("127.0.0.1", "", False),
("localhost", "", False),
("0.0.0.0", "oj_sk_x", False),
("0.0.0.0", "", True),
("192.168.1.10", "", True),
],
)
def test_check_bind_safety(host, api_key, should_exit):
from openjarvis.server.auth_middleware import check_bind_safety
if should_exit:
with pytest.raises(SystemExit):
check_bind_safety(host, api_key=api_key)
else:
check_bind_safety(host, api_key=api_key) # must not raise