From e367630c64df7d2e11575d2d45592cadb6e36b84 Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Thu, 13 Aug 2026 19:53:10 +0200 Subject: [PATCH 1/2] feat: harden and preconfigure OpenWebUI for Subactor --- .gitignore | 1 + README.md | 33 +++++++ docker-compose.yml | 17 +++- scripts/configure-openwebui-subactor.sh | 111 ++++++++++++++++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) create mode 100755 scripts/configure-openwebui-subactor.sh diff --git a/.gitignore b/.gitignore index 82fa757..51758d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env.example +.secrets/ .idea repos output diff --git a/README.md b/README.md index 1e2c2c9..05739ac 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,39 @@ Oficjalny frontend (docker image) podłączony do MCP Gateway: - Chat z modelami `mcp-skills/refactor` i `mcp-skills/analyze` - **URL**: http://localhost:3000 +Domyślnie OpenWebUI wymaga logowania, nie pozwala na samodzielną rejestrację i +nasłuchuje tylko na `127.0.0.1`. Gateway również jest związany z loopbackiem. +Przed uruchomieniem profilu utwórz trzy pliki o prawach `0600`: + +```bash +install -d -m 700 .secrets +umask 077 +test -s .secrets/openwebui-session.secret || openssl rand -hex 32 > .secrets/openwebui-session.secret +test -s .secrets/openwebui-mcp-jwt.secret || openssl rand -hex 32 > .secrets/openwebui-mcp-jwt.secret +test -s .secrets/openwebui-mcp-bearer.secret || openssl rand -hex 32 > .secrets/openwebui-mcp-bearer.secret +docker compose --profile openwebui up -d +``` + +Pliki można trzymać poza repozytorium, ustawiając +`OPENWEBUI_SESSION_SECRET_FILE`, `OPENWEBUI_MCP_JWT_SECRET_FILE` i +`OPENWEBUI_MCP_BEARER_SECRET_FILE`. Port można zmienić przez `PORT_OPENWEBUI`, +ale publiczny bind powinien być wystawiany wyłącznie przez uwierzytelnione TLS +proxy; surowy MCP i Control nie powinny być publikowane do Internetu. + +Po uruchomieniu można idempotentnie predefiniować zawężone połączenie Subactor: + +```bash +SUBACTOR_ACCOUNT_ID=softreck \ +SUBACTOR_PROVIDER=chatgpt \ +SUBACTOR_TOOL_ID=codex \ +SUBACTOR_CONTROL_URL=http://172.17.0.1:8088 \ +./scripts/configure-openwebui-subactor.sh +``` + +Skrypt nie drukuje tokenu. Konfiguruje wyłącznie `cli.status`, `cli.plan` i +`cli.execute`; wykonanie nadal wymaga osobnego grantu oraz aktywnej intencji po +stronie Subactor. + ### MCP Skills - HTTP API Serwer FastAPI z endpointami (poza MCP STDIO): - `POST /sync` - Synchronizacja repo z git-proxy diff --git a/docker-compose.yml b/docker-compose.yml index 40558ca..edb44c5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -150,7 +150,7 @@ services: - ./.env:/app/.env - audit-storage:/audit ports: - - "${PORT_GATEWAY:-9000}:9000" + - "${MCP_GATEWAY_BIND_HOST:-127.0.0.1}:${PORT_GATEWAY:-9000}:9000" healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:9000/health', timeout=3)"] interval: 20s @@ -261,7 +261,11 @@ services: environment: - OPENAI_API_BASE_URL=http://mcp-gateway:9000/v1 - OPENAI_API_KEY=${WEBUI_API_KEY:-sk-mcp-default-dev-key} - - WEBUI_AUTH=${OPENWEBUI_AUTH:-False} + - WEBUI_AUTH=${OPENWEBUI_AUTH:-True} + - ENABLE_SIGNUP=${OPENWEBUI_ENABLE_SIGNUP:-False} + - ENABLE_FORWARD_USER_INFO_HEADERS=True + - FORWARD_USER_INFO_HEADER_JWT=X-OpenWebUI-User-Jwt + - FORWARD_USER_INFO_HEADER_JWT_EXPIRES_SECONDS=300 # Renderowanie Markdown / artefaktów - ENABLE_MERMAID=${ENABLE_MERMAID:-true} - ENABLE_LATEX=${ENABLE_LATEX:-true} @@ -273,8 +277,15 @@ services: - SHOW_ADMIN_DETAILS=${SHOW_ADMIN_DETAILS:-false} volumes: - openwebui-data:/app/backend/data + - ${OPENWEBUI_MCP_JWT_SECRET_FILE:-./.secrets/openwebui-mcp-jwt.secret}:/run/secrets/openwebui-mcp-jwt:ro + - ${OPENWEBUI_MCP_BEARER_SECRET_FILE:-./.secrets/openwebui-mcp-bearer.secret}:/run/secrets/openwebui-mcp-bearer:ro + - ${OPENWEBUI_SESSION_SECRET_FILE:-./.secrets/openwebui-session.secret}:/run/secrets/openwebui-session:ro + command: + - bash + - -lc + - export WEBUI_SECRET_KEY="$$(tr -d '\r\n' < /run/secrets/openwebui-session)"; export FORWARD_USER_INFO_HEADER_JWT_SECRET="$$(tr -d '\r\n' < /run/secrets/openwebui-mcp-jwt)"; exec bash start.sh ports: - - "${PORT_OPENWEBUI:-3000}:8080" + - "${OPENWEBUI_BIND_HOST:-127.0.0.1}:${PORT_OPENWEBUI:-3000}:8080" depends_on: mcp-gateway: condition: service_healthy diff --git a/scripts/configure-openwebui-subactor.sh b/scripts/configure-openwebui-subactor.sh new file mode 100755 index 0000000..60ed741 --- /dev/null +++ b/scripts/configure-openwebui-subactor.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +set -euo pipefail + +account_id="${SUBACTOR_ACCOUNT_ID:-softreck}" +provider="${SUBACTOR_PROVIDER:-chatgpt}" +tool_id="${SUBACTOR_TOOL_ID:-codex}" +control_url="${SUBACTOR_CONTROL_URL:-http://172.17.0.1:8088}" +connection_id="${SUBACTOR_OPENWEBUI_CONNECTION_ID:-subactor-${account_id}-${provider}-${tool_id}}" + +docker compose exec -T \ + -e SUBACTOR_ACCOUNT_ID="$account_id" \ + -e SUBACTOR_PROVIDER="$provider" \ + -e SUBACTOR_TOOL_ID="$tool_id" \ + -e SUBACTOR_CONTROL_URL="$control_url" \ + -e SUBACTOR_OPENWEBUI_CONNECTION_ID="$connection_id" \ + openwebui sh -lc ' + export WEBUI_SECRET_KEY="$(tr -d "\r\n" < /run/secrets/openwebui-session)" + python - + ' <<'PY' +import asyncio +import json +import os +import re +from pathlib import Path +from urllib.parse import urlsplit + +from open_webui.models.config import Config + + +def identifier(name: str) -> str: + value = os.environ[name].strip().lower() + if re.fullmatch(r"[a-z0-9][a-z0-9-]{0,62}", value) is None: + raise SystemExit(f"invalid {name}") + return value + + +async def configure() -> None: + account_id = identifier("SUBACTOR_ACCOUNT_ID") + provider = identifier("SUBACTOR_PROVIDER") + tool_id = identifier("SUBACTOR_TOOL_ID") + connection_id = os.environ["SUBACTOR_OPENWEBUI_CONNECTION_ID"].strip() + if re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}", connection_id) is None: + raise SystemExit("invalid SUBACTOR_OPENWEBUI_CONNECTION_ID") + + control_url = os.environ["SUBACTOR_CONTROL_URL"].strip().rstrip("/") + parsed = urlsplit(control_url) + if parsed.scheme not in {"http", "https"} or not parsed.hostname: + raise SystemExit("invalid SUBACTOR_CONTROL_URL") + if parsed.username or parsed.password or parsed.query or parsed.fragment: + raise SystemExit("SUBACTOR_CONTROL_URL must not contain credentials or a query") + + secret_path = Path("/run/secrets/openwebui-mcp-bearer") + token = secret_path.read_text(encoding="utf-8").strip() + if len(token) < 32: + raise SystemExit("OpenWebUI MCP bearer secret is missing or too short") + + url = ( + f"{control_url}/mcp/accounts/{account_id}/providers/{provider}/tools/{tool_id}" + ) + connection = { + "auth_type": "bearer", + "config": { + "access_grants": [ + { + "permission": "read", + "principal_id": "*", + "principal_type": "user", + } + ], + "enable": True, + "function_name_filter_list": "cli.status,cli.plan,cli.execute", + }, + "headers": None, + "info": { + "description": ( + "Scoped Subactor MCP: closed DSL, grant and intent required for execution." + ), + "id": connection_id, + "name": f"Subactor — {account_id} / {provider} / {tool_id}", + }, + "key": token, + "path": "", + "type": "mcp", + "url": url, + } + connections = await Config.get("tool_server.connections", []) + if not isinstance(connections, list): + raise SystemExit("OpenWebUI tool_server.connections is not a list") + updated = [ + item + for item in connections + if (item.get("info") or {}).get("id") != connection_id + ] + updated.append(connection) + await Config.upsert({"tool_server.connections": updated}) + print( + json.dumps( + { + "configured": True, + "connection_id": connection_id, + "url": url, + "tools": ["cli.status", "cli.plan", "cli.execute"], + "token_logged": False, + }, + sort_keys=True, + ) + ) + + +asyncio.run(configure()) +PY From 2e42786819d37a3a2aeeff0b39833b1c6aaa3d6f Mon Sep 17 00:00:00 2001 From: Tom Softreck Date: Thu, 13 Aug 2026 20:07:20 +0200 Subject: [PATCH 2/2] ci: add hosted verification gate --- .github/workflows/verify.yml | 54 ++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/verify.yml diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..54dce5c --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,54 @@ +name: verify + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.12" + cache: pip + - name: Install test dependencies + run: python -m pip install --disable-pip-version-check -e '.[dev]' + - name: Run unit tests + run: python -m pytest -q + - name: Validate secure OpenWebUI boundary + shell: bash + run: | + set -euo pipefail + bash -n scripts/configure-openwebui-subactor.sh + docker compose --profile openwebui config -q + docker compose --profile openwebui config --format json | jq -e ' + .services["mcp-gateway"].ports == [{ + "mode": "ingress", + "host_ip": "127.0.0.1", + "target": 9000, + "published": "9000", + "protocol": "tcp" + }] and + .services.openwebui.ports == [{ + "mode": "ingress", + "host_ip": "127.0.0.1", + "target": 8080, + "published": "3000", + "protocol": "tcp" + }] and + .services.openwebui.environment.WEBUI_AUTH == "True" and + .services.openwebui.environment.ENABLE_SIGNUP == "False" and + any( + .services.openwebui.volumes[]; + .target == "/run/secrets/openwebui-mcp-bearer" and + .read_only == true + ) + '