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
|
# Secrets
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
# ...but keep checked-in example/templates (never contain real secrets)
|
||||||
|
!.env.example
|
||||||
|
!**/.env.example
|
||||||
|
|
||||||
# Project
|
# Project
|
||||||
*.sqlite
|
*.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:
|
environment:
|
||||||
- OPENJARVIS_ENGINE_DEFAULT=ollama
|
- OPENJARVIS_ENGINE_DEFAULT=ollama
|
||||||
- OLLAMA_HOST=http://ollama:11434
|
- 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:
|
depends_on:
|
||||||
ollama:
|
ollama:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -4,15 +4,27 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>Label</key>
|
<key>Label</key>
|
||||||
<string>com.openjarvis</string>
|
<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>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/usr/local/bin/jarvis</string>
|
<string>/usr/local/bin/jarvis</string>
|
||||||
<string>serve</string>
|
<string>serve</string>
|
||||||
<string>--host</string>
|
<string>--host</string>
|
||||||
<string>0.0.0.0</string>
|
<string>127.0.0.1</string>
|
||||||
<string>--port</string>
|
<string>--port</string>
|
||||||
<string>8000</string>
|
<string>8000</string>
|
||||||
</array>
|
</array>
|
||||||
|
<!--
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>OPENJARVIS_API_KEY</key>
|
||||||
|
<string>REPLACE_WITH_A_REAL_KEY</string>
|
||||||
|
</dict>
|
||||||
|
-->
|
||||||
<key>RunAtLoad</key>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>KeepAlive</key>
|
<key>KeepAlive</key>
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ ExecStart=/opt/openjarvis/.venv/bin/jarvis serve --host 0.0.0.0 --port 8000
|
|||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
Environment=HOME=/opt/openjarvis
|
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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
@@ -4,12 +4,25 @@ OpenJarvis provides Docker images for both CPU-only and GPU-accelerated deployme
|
|||||||
|
|
||||||
## Quick Start
|
## 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
|
```bash
|
||||||
docker compose up -d
|
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:
|
This brings up two services:
|
||||||
|
|
||||||
| Service | Port | Description |
|
| 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.
|
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:
|
Verify it is running:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -55,10 +63,17 @@ The provided plist file at `deploy/launchd/com.openjarvis.plist`:
|
|||||||
<string>/usr/local/bin/jarvis</string>
|
<string>/usr/local/bin/jarvis</string>
|
||||||
<string>serve</string>
|
<string>serve</string>
|
||||||
<string>--host</string>
|
<string>--host</string>
|
||||||
<string>0.0.0.0</string>
|
<string>127.0.0.1</string>
|
||||||
<string>--port</string>
|
<string>--port</string>
|
||||||
<string>8000</string>
|
<string>8000</string>
|
||||||
</array>
|
</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>
|
<key>RunAtLoad</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>KeepAlive</key>
|
<key>KeepAlive</key>
|
||||||
@@ -76,7 +91,7 @@ The provided plist file at `deploy/launchd/com.openjarvis.plist`:
|
|||||||
| Key | Value | Description |
|
| Key | Value | Description |
|
||||||
|----------------------|--------------------------------|------------------------------------------------------------------------------------------------------|
|
|----------------------|--------------------------------|------------------------------------------------------------------------------------------------------|
|
||||||
| `Label` | `com.openjarvis` | Unique identifier for the service. Used with `launchctl` commands to manage the service. |
|
| `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). |
|
| `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. |
|
| `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. |
|
| `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
|
## 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
|
```bash
|
||||||
sudo cp deploy/systemd/openjarvis.service /etc/systemd/system/
|
sudo cp deploy/systemd/openjarvis.service /etc/systemd/system/
|
||||||
@@ -30,6 +40,10 @@ sudo systemctl enable openjarvis
|
|||||||
sudo systemctl start 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:
|
Verify it is running:
|
||||||
|
|
||||||
```bash
|
```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