Skip to content
Merged
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 bioengine/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
Must stay in lock-step with ``pyproject.toml``'s ``version`` field.
The ``version-check.yml`` CI workflow enforces the match.
"""
__version__ = "0.16.26"
__version__ = "0.16.27"
10 changes: 8 additions & 2 deletions bioengine/worker/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,14 @@ def create_parser() -> argparse.ArgumentParser:
type=str,
nargs="+",
metavar="EMAIL",
help="List of user emails/IDs with administrative privileges for worker management. "
"If not specified, defaults to the authenticated user from Hypha login.",
help="Space-separated list of user emails/IDs with administrative privileges for "
"worker management. If not specified, defaults to the authenticated user from "
"Hypha login. SECURITY: passing '*' makes every caller that can reach the Hypha "
"server a full admin, including unauthenticated anonymous ones, because the "
"worker service is public. That lets anyone run arbitrary Python on this "
"deployment ('run_code', 'deploy_app', 'upload_app') and destroy it "
"('stop_worker', 'stop_all_apps'). Use named emails unless this deployment is "
"meant to run untrusted code.",
)
core_group.add_argument(
"--workspace-dir",
Expand Down
24 changes: 24 additions & 0 deletions bioengine/worker/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,28 @@ def _load_persisted_admin_users(self) -> None:
)
self.admin_users[:] = persisted

def _warn_if_wildcard_admin_users(self) -> None:
"""Warn that a wildcard admin list exposes code execution to anyone.

The deployment guide says this too, but nobody reads it at roll time.
"""
if "*" not in self.admin_users:
return

self.logger.warning(
"SECURITY: '*' is in this worker's admin users and the worker service is "
"registered with public visibility. Every caller that can reach the Hypha "
"server — including unauthenticated, anonymous ones — is therefore a full "
"admin of this worker. They can run arbitrary Python on this deployment "
"via 'run_code', 'deploy_app' or 'upload_app', with whatever filesystem, "
"credentials and network access its Ray cluster is given; in "
"single-machine mode that is this host, as the user running this process. "
"They can also destroy it via 'stop_worker', 'stop_all_apps' or "
"'delete_app'. This is remote code execution open to the internet, not "
"merely open read access. Replace '*' with named admin emails unless this "
"deployment is genuinely meant to run untrusted code."
)

def _persist_admin_users(self, admin_users: List[str]) -> None:
"""Write the admin users so the next restart overlays them on the seed.

Expand Down Expand Up @@ -1335,6 +1357,8 @@ async def start(self, blocking: bool = True) -> str:
# Completes initialization of AppsManager and CodeExecutor
await self._connect_to_server(STARTUP_CONNECT_BUDGET_S)

self._warn_if_wildcard_admin_users()

# Check for running data server
await self._discover_data_server()

Expand Down
12 changes: 11 additions & 1 deletion docs/deployment-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ BioEngine supports three deployment modes. The easiest way to generate deploymen

---

## Who can control the worker

`--admin-users` takes a space-separated list of emails or user IDs and defaults to the account whose token started the worker. Admins can perform every admin operation on the worker, in every deployment mode.

> **`--admin-users '*'` is remote code execution open to the internet.** The worker's Hypha service is registered with public visibility, and a service's authorization gates invocation, not discovery. Almost every admin operation is gated on the admin list with the wildcard honoured, so with `*` in the list **any caller that can reach the Hypha server — including an unauthenticated, anonymous one — is a full admin of your worker**. They can run arbitrary Python on the deployment through `run_code`, `deploy_app` or `upload_app`, with whatever filesystem, credentials and network access its Ray cluster is given — in single-machine mode that is the host you started the worker on, as the user who started it. They can also destroy it through `stop_worker`, `stop_all_apps` or `delete_app`. The wildcard is not "skip maintaining an admin list"; it is "this deployment runs untrusted code from strangers". Use named emails unless that is genuinely what you want. The worker logs a warning at startup whenever the wildcard is in effect.

The one exception is editing the admin list itself: a caller who is only covered by `*` cannot call `add_admin_user` or `remove_admin_user`, so a stranger cannot make the grant permanent or lock you out.

---

## Mode 1: Single Machine

Runs a local Ray cluster on one machine. Good for workstations, development, and small-scale analysis.
Expand Down Expand Up @@ -93,7 +103,7 @@ apptainer exec \
| `--workspace` | auto | Hypha workspace name (auto-detected from token) |
| `--server-url` | `https://hypha.aicell.io` | Hypha server URL |
| `--token` | prompt | Hypha authentication token |
| `--admin-users` | current user | Comma-separated emails or `*` for all |
| `--admin-users` | current user | Space-separated emails, or `*` for all — see [Who can control the worker](#who-can-control-the-worker) |
| `--client-id` | auto | Unique service identifier |

The workspace directory defaults to `~/.bioengine` and is mounted into the container at `/.bioengine`.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "bioengine"
version = "0.16.26"
version = "0.16.27"
description = "BioEngine — CLI and SDK for deploying and calling AI model services on BioEngine workers"
requires-python = ">=3.11"
authors = [
Expand Down
65 changes: 64 additions & 1 deletion tests/worker/test_admin_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
restart, when the ``--admin-users`` startup flag is replayed verbatim.
"""

import asyncio
import json
from types import SimpleNamespace

import pytest

Expand Down Expand Up @@ -36,11 +38,16 @@ def _bare_worker(tmp_path, admin_users, **attrs):
class _Logger:
def __init__(self):
self.records = []
self.warnings = []

def _record(self, message):
self.records.append(str(message))

info = warning = error = debug = _record
def warning(self, message):
self.warnings.append(str(message))
self._record(message)

info = error = debug = _record


async def test_a_granted_admin_can_immediately_call_admin_methods(tmp_path):
Expand Down Expand Up @@ -309,3 +316,59 @@ def __getattr__(self, name):
assert registered["list_admin_users"] == worker.list_admin_users
assert registered["add_admin_user"] == worker.add_admin_user
assert registered["remove_admin_user"] == worker.remove_admin_user


async def test_a_wildcard_admin_list_warns_that_anyone_can_run_code(tmp_path):
"""The wildcard reads as a permissions shortcut; it is open remote execution."""
worker = _bare_worker(tmp_path, ["admin@example.org", "*"])

worker._warn_if_wildcard_admin_users()

assert len(worker.logger.warnings) == 1
warning = worker.logger.warnings[0]
assert "run_code" in warning
assert "anonymous" in warning


async def test_a_named_admin_list_does_not_warn(tmp_path):
worker = _bare_worker(tmp_path, ["admin@example.org"])

worker._warn_if_wildcard_admin_users()

assert worker.logger.warnings == []


class _Reached(Exception):
"""Ends a start() once the point under test has been passed."""


async def test_the_wildcard_warning_is_emitted_during_startup(tmp_path):
"""A warning nobody calls is the same as no warning.

Ending start() at the step after the call site is what pins it to startup:
production runs blocking=True, so a warning placed later in start() would
not reach an operator until the worker shuts down.
"""

async def _noop(*_args, **_kwargs):
return None

async def _reached(*_args, **_kwargs):
raise _Reached

worker = _bare_worker(
tmp_path,
["admin@example.org", "*"],
heartbeat_file=tmp_path / "worker_heartbeat.json",
_shutdown_event=asyncio.Event(),
ray_cluster=SimpleNamespace(start=_noop),
_connect_to_server=_noop,
_discover_data_server=_reached,
_stop=_noop,
)

with pytest.raises(_Reached):
await worker.start(blocking=True)

assert len(worker.logger.warnings) == 1
assert "run_code" in worker.logger.warnings[0]
Loading