mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-27 21:05:34 +00:00
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
7e8db280e4
commit
9cd760ad1e
@@ -39,6 +39,9 @@ Thumbs.db
|
||||
# Secrets
|
||||
.env
|
||||
.env.*
|
||||
# ...but keep checked-in example/templates (never contain real secrets)
|
||||
!.env.example
|
||||
!**/.env.example
|
||||
|
||||
# Project
|
||||
*.sqlite
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# Copy to `.env` in this directory (deploy/docker/.env) before `docker compose up`.
|
||||
# docker-compose.yml requires this — the container binds 0.0.0.0, so the
|
||||
# server refuses to start without an API key.
|
||||
#
|
||||
# Generate a key with: jarvis auth generate-key
|
||||
# Then clients must send: Authorization: Bearer <key>
|
||||
OPENJARVIS_API_KEY=
|
||||
@@ -8,6 +8,10 @@ services:
|
||||
environment:
|
||||
- OPENJARVIS_ENGINE_DEFAULT=ollama
|
||||
- OLLAMA_HOST=http://ollama:11434
|
||||
# The container binds 0.0.0.0, so an API key is REQUIRED. Compose fails
|
||||
# fast if OPENJARVIS_API_KEY is unset (set it in deploy/docker/.env —
|
||||
# see .env.example, or `export` it). Generate one: `jarvis auth generate-key`.
|
||||
- OPENJARVIS_API_KEY=${OPENJARVIS_API_KEY:?OPENJARVIS_API_KEY must be set (see deploy/docker/.env.example)}
|
||||
depends_on:
|
||||
ollama:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -4,15 +4,27 @@
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.openjarvis</string>
|
||||
<!-- Binds loopback only: the personal-device default, reachable from this
|
||||
Mac but not the network, so no API key is required. To expose it on
|
||||
your LAN, change the host below to 0.0.0.0 AND uncomment the
|
||||
EnvironmentVariables block to set an API key (an unauthenticated
|
||||
0.0.0.0 server will refuse to start). -->
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/jarvis</string>
|
||||
<string>serve</string>
|
||||
<string>--host</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>127.0.0.1</string>
|
||||
<string>--port</string>
|
||||
<string>8000</string>
|
||||
</array>
|
||||
<!--
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>OPENJARVIS_API_KEY</key>
|
||||
<string>REPLACE_WITH_A_REAL_KEY</string>
|
||||
</dict>
|
||||
-->
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
|
||||
@@ -10,6 +10,11 @@ ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=HOME=/opt/openjarvis
|
||||
# Binding 0.0.0.0 requires authentication. This file MUST exist and contain:
|
||||
# OPENJARVIS_API_KEY=<key> (generate one: `jarvis auth generate-key`)
|
||||
# It is not prefixed with "-", so the unit fails to start if the file is
|
||||
# missing — preventing an accidentally unauthenticated public server.
|
||||
EnvironmentFile=/etc/openjarvis/env
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
@@ -4,12 +4,25 @@ OpenJarvis provides Docker images for both CPU-only and GPU-accelerated deployme
|
||||
|
||||
## Quick Start
|
||||
|
||||
The fastest way to get OpenJarvis running in Docker is with Docker Compose, which starts both the API server and an Ollama backend:
|
||||
The container binds `0.0.0.0`, so an **API key is required** — the server
|
||||
refuses to start on a non-loopback address without one. Set it first:
|
||||
|
||||
```bash
|
||||
cd deploy/docker
|
||||
cp .env.example .env
|
||||
echo "OPENJARVIS_API_KEY=$(jarvis auth generate-key)" > .env # or paste your own
|
||||
```
|
||||
|
||||
Then start both the API server and an Ollama backend with Docker Compose:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
`docker compose` reads `OPENJARVIS_API_KEY` from `.env` (or your shell
|
||||
environment) and fails fast if it is unset. Clients must then send
|
||||
`Authorization: Bearer <key>` on `/v1/*` and `/api/*` requests.
|
||||
|
||||
This brings up two services:
|
||||
|
||||
| Service | Port | Description |
|
||||
|
||||
@@ -24,6 +24,14 @@ launchctl load ~/Library/LaunchAgents/com.openjarvis.plist
|
||||
|
||||
The service starts immediately (due to `RunAtLoad`) and will automatically restart at each login.
|
||||
|
||||
!!! note "Binds loopback by default"
|
||||
The plist binds `127.0.0.1` — reachable from this Mac but not the network,
|
||||
the right default for a personal device, and no API key is needed. To
|
||||
expose it on your LAN, change the host to `0.0.0.0` **and** uncomment the
|
||||
`EnvironmentVariables` block to set `OPENJARVIS_API_KEY`
|
||||
(`jarvis auth generate-key`); an unauthenticated `0.0.0.0` server refuses
|
||||
to start.
|
||||
|
||||
Verify it is running:
|
||||
|
||||
```bash
|
||||
@@ -55,10 +63,17 @@ The provided plist file at `deploy/launchd/com.openjarvis.plist`:
|
||||
<string>/usr/local/bin/jarvis</string>
|
||||
<string>serve</string>
|
||||
<string>--host</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>127.0.0.1</string>
|
||||
<string>--port</string>
|
||||
<string>8000</string>
|
||||
</array>
|
||||
<!-- To expose on the LAN: set host to 0.0.0.0 and uncomment this block.
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>OPENJARVIS_API_KEY</key>
|
||||
<string>REPLACE_WITH_A_REAL_KEY</string>
|
||||
</dict>
|
||||
-->
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
@@ -76,7 +91,7 @@ The provided plist file at `deploy/launchd/com.openjarvis.plist`:
|
||||
| Key | Value | Description |
|
||||
|----------------------|--------------------------------|------------------------------------------------------------------------------------------------------|
|
||||
| `Label` | `com.openjarvis` | Unique identifier for the service. Used with `launchctl` commands to manage the service. |
|
||||
| `ProgramArguments` | `["/usr/local/bin/jarvis", "serve", "--host", "0.0.0.0", "--port", "8000"]` | The command and arguments to execute. Each element of the command line is a separate string in the array. |
|
||||
| `ProgramArguments` | `["/usr/local/bin/jarvis", "serve", "--host", "127.0.0.1", "--port", "8000"]` | The command and arguments to execute. Binds loopback by default; see the note above to expose on the LAN with an API key. |
|
||||
| `RunAtLoad` | `true` | Start the service immediately when the plist is loaded (and on each login). |
|
||||
| `KeepAlive` | `true` | Automatically restart the service if it exits for any reason. launchd monitors the process and relaunches it. |
|
||||
| `StandardOutPath` | `/tmp/openjarvis.stdout.log` | File where standard output is written. Contains server startup messages and access logs. |
|
||||
|
||||
@@ -21,7 +21,17 @@ cd /opt/openjarvis/OpenJarvis && sudo -u openjarvis uv sync --extra server
|
||||
|
||||
## Installing the Service
|
||||
|
||||
Copy the unit file to the systemd directory, reload the daemon, and enable the service:
|
||||
The unit binds `0.0.0.0`, so an **API key is required** — and the unit
|
||||
declares `EnvironmentFile=/etc/openjarvis/env` (no `-` prefix), so it will
|
||||
**fail to start** until that file exists with a key. Create it first:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /etc/openjarvis
|
||||
echo "OPENJARVIS_API_KEY=$(jarvis auth generate-key)" | sudo tee /etc/openjarvis/env
|
||||
sudo chmod 600 /etc/openjarvis/env
|
||||
```
|
||||
|
||||
Then copy the unit file, reload the daemon, and enable the service:
|
||||
|
||||
```bash
|
||||
sudo cp deploy/systemd/openjarvis.service /etc/systemd/system/
|
||||
@@ -30,6 +40,10 @@ sudo systemctl enable openjarvis
|
||||
sudo systemctl start openjarvis
|
||||
```
|
||||
|
||||
Clients must send `Authorization: Bearer <key>` on `/v1/*` and `/api/*`
|
||||
requests. (If you instead bind to `127.0.0.1`, the key is optional and you
|
||||
can drop the `EnvironmentFile` line.)
|
||||
|
||||
Verify it is running:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user