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
25 changes: 20 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ DATA_DIR=/path/to/data
DB_INIT_DIR=/path/to/db-init
VIDEO_DIR=/path/to/omnibioai-videos/content

# ══════════════════════════════════════════════════════
# REQUIRED SECRETS — the release compose files have NO
# defaults for these and refuse to start without them.
# The Studio app generates all of them on first launch;
# set them by hand only when running compose directly.
# See SECURITY-COMPOSE-HARDENING.md.
# ══════════════════════════════════════════════════════

# ── Database ───────────────────────────────────────────
# AUTO-GENERATED on first launch — do not share
MYSQL_ROOT_PASSWORD=change-me-in-production
Expand All @@ -21,6 +29,9 @@ MYSQL_ROOT_PASSWORD=change-me-in-production
AUTH_SECRET_KEY=change-me-in-production
# AUTO-GENERATED on first launch — do not share
LICENSE_SECRET=change-me-in-production
# AUTO-GENERATED on first launch — do not share
# Signs LIMS's own session cookies (distinct from AUTH_SECRET_KEY)
LIMSX_DJANGO_SECRET_KEY=change-me-in-production

# ── LIMS ───────────────────────────────────────────────
LIMS_USERNAME=admin
Expand All @@ -43,10 +54,14 @@ DISCORD_ALERT_WEBHOOK_URL=

# ── GitHub (for pulling private images) ────────────────
GHCR_PULL_TOKEN=
# AUTO-GENERATED on first launch — do not share
GF_ADMIN_PASSWORD=omnibioai
# REQUIRED. AUTO-GENERATED on first launch — do not share
GF_ADMIN_PASSWORD=change-me-in-production

# ── IDE Services ───────────────────────────────────────
JUPYTER_TOKEN=omnibioai
RSTUDIO_PASSWORD=omnibioai
VSCODE_PASSWORD=omnibioai
# All REQUIRED. AUTO-GENERATED on first launch — do not share.
# These previously defaulted to the literal "omnibioai" on every
# installation, i.e. every Studio deployment shipped with the same
# publicly-known Jupyter/RStudio/VS Code credentials.
JUPYTER_TOKEN=change-me-in-production
RSTUDIO_PASSWORD=change-me-in-production
VSCODE_PASSWORD=change-me-in-production
40 changes: 40 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ jobs:
&& echo "✅ package.json valid"
node -e "JSON.parse(require('fs').readFileSync('electron-builder.json'))" \
&& echo "✅ electron-builder.json valid"
- name: Secret-generation unit tests
run: npm run test:secrets
- name: Compose security regression tests
run: |
python3 -m pip install --quiet pyyaml pytest
python3 -m pytest tests/test_compose_network_exposure.py \
tests/test_compose_release_config.py -q
- name: Create .env for compose validation
run: |
cat > .env << 'ENVEOF'
Expand All @@ -45,11 +52,44 @@ jobs:
NCBI_EMAIL=ci@omnibioai.org
MYSQL_ROOT_PASSWORD=cipassword
AUTH_SECRET_KEY=cikey
LICENSE_SECRET=cilicense
GF_ADMIN_PASSWORD=cigrafana
LIMSX_DJANGO_SECRET_KEY=cilims
JUPYTER_TOKEN=cijupyter
RSTUDIO_PASSWORD=cirstudio
VSCODE_PASSWORD=civscode
ENVEOF
- name: Validate docker-compose
run: |
docker compose -f docker-compose.yml config --quiet \
&& echo "✅ docker-compose.yml valid"
- name: Validate release compose files
# docker-compose.release.yml is the file electron-builder actually
# bundles into packaged installers; docker-compose-release.yml (dash)
# is kept in parity with it (see tests/test_compose_release_config.py)
# for anyone still invoking it directly. Both must validate with a
# full required-secret .env, and both must confirm they're
# unpublishable without one -- see SECURITY-COMPOSE-HARDENING.md.
run: |
for f in docker-compose.release.yml docker-compose-release.yml; do
docker compose -f "$f" config --quiet \
&& echo "✅ $f valid with required secrets set"
done

# dev-only overlay must also merge cleanly on top of the release default
docker compose -f docker-compose.release.yml -f docker-compose.release.dev-ports.yml config --quiet \
&& echo "✅ docker-compose.release.dev-ports.yml merges cleanly"
- name: Release compose must fail closed without required secrets
run: |
grep -v -E '^(MYSQL_ROOT_PASSWORD|AUTH_SECRET_KEY|LICENSE_SECRET|GF_ADMIN_PASSWORD|LIMSX_DJANGO_SECRET_KEY|JUPYTER_TOKEN|RSTUDIO_PASSWORD|VSCODE_PASSWORD)=' .env > /tmp/missing-secrets.env
for f in docker-compose.release.yml docker-compose-release.yml; do
if docker compose --env-file /tmp/missing-secrets.env -f "$f" config --quiet 2>/tmp/err.log; then
echo "❌ $f started without required secrets -- fail-closed guard regressed"
exit 1
fi
grep -q "required variable" /tmp/err.log \
&& echo "✅ $f correctly refuses to start without required secrets"
done

# ── 2. Build React UI ───────────────────────────────────
build-ui:
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ omnibioai-studio/
├── electron-builder.json← electron-builder packaging config
├── docker-compose.yml ← Dev compose file (loaded in dev mode)
├── docker-compose.release.yml ← Packaged app compose file
├── docker-compose.release.dev-ports.yml ← Dev-only overlay: republishes
│ MySQL/Redis locally. Never bundled, never a
│ production default — see SECURITY-COMPOSE-HARDENING.md
├── SECURITY-COMPOSE-HARDENING.md ← Deployment network boundary + required secrets
├── build/ ← App icons (icon.png, .ico, .icns)
├── db-init/ ← SQL init scripts (copied into userData on first run)
├── monitoring/ ← Prometheus + Grafana config
Expand Down Expand Up @@ -280,6 +284,7 @@ Packaging config is in `electron-builder.json`. The release pipeline (`.github/w
- **IPC:** every new `ipcMain.handle` must have a corresponding `contextBridge` exposure — never call `ipcRenderer` directly from renderer code
- **Security:** external URLs must be opened with `shell.openExternal` — never `loadURL` an HTTPS URL into the main window
- **Paths:** use `app.getPath('userData')` for user data, `process.resourcesPath` for bundled resources — never hardcode absolute paths
- **Secrets/exposure:** never add a `${VAR:-somedefault}` fallback for a credential in a release compose file, and never publish MySQL/Redis there. Local access goes in `docker-compose.release.dev-ports.yml`. New credentials must also be added to `SECRET_DEFAULTS` in `electron/secrets.js` or a fresh install will fail to start — see [SECURITY-COMPOSE-HARDENING.md](SECURITY-COMPOSE-HARDENING.md)

---

Expand Down
26 changes: 23 additions & 3 deletions DEPLOYMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

| Port | Service | Notes |
|---|---|---|
| 3306 | MySQL | Internal only — not exposed externally in prod |
| 6380 | Redis | Host-mapped from container 6379 |
| 3306 | MySQL | **Internal only — not published to the host.** Reachable inside the compose network as `mysql:3306`. For local access, layer `docker-compose.release.dev-ports.yml` (see [SECURITY-COMPOSE-HARDENING.md](SECURITY-COMPOSE-HARDENING.md)) |
| 6379 | Redis | **Internal only — not published to the host.** Reachable inside the compose network as `redis:6379`. Same dev-only overlay applies (published as 6380 on the host when it is used) |
| 7000 | lims | LIMS Django API |
| 7070 | control-center | OmniBioAI Control Center API |
| 8000 | workbench | Main Django workbench |
Expand Down Expand Up @@ -51,12 +51,22 @@ Create `deploy/compose/.env` (never commit this file).

### Required — All Environments

> **The release compose files fail closed on these.** `MYSQL_ROOT_PASSWORD`,
> `AUTH_SECRET_KEY`, `LICENSE_SECRET`, `GF_ADMIN_PASSWORD`,
> `LIMSX_DJANGO_SECRET_KEY`, `JUPYTER_TOKEN`, `RSTUDIO_PASSWORD`, and
> `VSCODE_PASSWORD` have **no defaults** — `docker compose` refuses to start
> without them rather than silently provisioning a guessable credential. See
> [SECURITY-COMPOSE-HARDENING.md](SECURITY-COMPOSE-HARDENING.md). The Studio
> desktop app generates all of them per-installation on first launch; you only
> need to set them by hand when running compose directly.

```dotenv
# ── Database ──────────────────────────────────────────────────────────────────
MYSQL_ROOT_PASSWORD=<strong-password>
MYSQL_ROOT_PASSWORD=<strong-password> # REQUIRED — no default
MYSQL_DEFAULT_DB=omnibioai

# ── LIMS ──────────────────────────────────────────────────────────────────────
# REQUIRED — no default. Signs LIMS's own session cookies.
LIMSX_DJANGO_SECRET_KEY=<generate: python3 -c "import secrets; print(secrets.token_urlsafe(50))">
# LIMSX_DJANGO_DEBUG=False # set to False in production

Expand All @@ -73,15 +83,25 @@ ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...

# ── Auth service ──────────────────────────────────────────────────────────────
# REQUIRED — no default. Signs every platform JWT.
AUTH_SECRET_KEY=<generate: python3 -c "import secrets; print(secrets.token_urlsafe(50))">

# ── Container registry ────────────────────────────────────────────────────────
GITHUB_TOKEN=<PAT with read:packages scope — for ghcr.io pull>
GHCR_PULL_TOKEN=<PAT with read:packages scope — BuildKit secret for image builds that need @man4ish/ui, and for license-server/tes/model-registry/lims/rag image pulls>

# ── License (unified OMNI-XXXX flow) ───────────────────────────────────────────
# REQUIRED — no default.
LICENSE_SECRET=<random-token — used by the legacy standalone license-server, :8099>

# ── Monitoring / interactive services ─────────────────────────────────────────
# All REQUIRED — no defaults. Previously these silently defaulted to the
# literal "omnibioai" on every installation.
GF_ADMIN_PASSWORD=<strong-password>
JUPYTER_TOKEN=<generate: python3 -c "import secrets; print(secrets.token_urlsafe(32))">
RSTUDIO_PASSWORD=<strong-password>
VSCODE_PASSWORD=<strong-password>

# ── Neo4j (RAG knowledge graph) ─────────────────────────────────────────────────
NEO4J_PASSWORD=<strong-password> # defaults to "omnibioai" if unset — override in production

Expand Down
21 changes: 19 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,25 @@ regardless of whether the server call itself succeeded.
### Data Layer
| Service | Port | Image |
|---------|------|-------|
| MySQL | :3306 | mysql:8.0 |
| Redis | :6379 (mapped :6380 on host) | redis:7-alpine |
| MySQL | :3306 (internal only in production/release — see below) | mysql:8.0 |
| Redis | :6379 (internal only in production/release — see below) | redis:7-alpine |

**Production/release** (`docker-compose.release.yml`, the config packaged
into the Electron app): MySQL and Redis are **not published to the host** —
reachable only inside the Compose network, as `mysql:3306` / `redis:6379`.
Every other service still addresses them exactly that way.

**Development**: the local dev stack (`docker-compose.yml`) still publishes
both directly (`:3306` / `:6380`) for convenience, as it always has. To get
the same local access against the release stack instead, layer the explicit
`docker-compose.release.dev-ports.yml` overlay:
```bash
docker compose -f docker-compose.release.yml -f docker-compose.release.dev-ports.yml up -d
```
This overlay binds to `127.0.0.1` only, not `0.0.0.0`, and is never bundled
into the packaged app or referenced by its startup path — it has to be
opted into explicitly. See [SECURITY-COMPOSE-HARDENING.md](SECURITY-COMPOSE-HARDENING.md)
for the full rationale.

### Security Control Plane
| Service | Port | Image |
Expand Down
Loading
Loading