diff --git a/SECURITY.md b/SECURITY.md index aa31e70..a18cc9b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -36,4 +36,16 @@ This reduces the blast radius of a compromised worker by keeping plaintext API k - Avoid logging request headers and environment variables in production environments. - Assume agent output is untrusted input; validate and sanitize before using it in other systems. +## Kubernetes worker process identity + +The **Kubernetes instancer** (`instancer/backends/k8s.py`) schedules worker `Job` pods using the published worker image. That image is currently built to run its entrypoint as **root** (default for the base image), so the pod spec does not set `runAsNonRoot` / `runAsUser` yet. + +**Why this matters:** uploaded project code runs inside the worker; running the process as non-root limits impact if the container boundary is reached. + +**Mitigations operators can use today:** isolate namespaces, restrict service accounts, use network policies, prefer **proxy-token mode** for API keys (see above), and keep worker images updated. + +**Hardening path:** add a non-root `USER` in the worker image (with correct ownership of `WORKSPACE_BASE` / agent dirs), then set pod or container `securityContext` (`runAsNonRoot`, `runAsUser`, `fsGroup` as needed). Until that image change lands, expecting `securityContext` alone would break startup. + +If your deployment **does not use** the Kubernetes instancer (for example only Docker-based workers), this section does not apply to that path. + diff --git a/backend/.env.production b/backend/.env.production deleted file mode 100644 index 34152d9..0000000 --- a/backend/.env.production +++ /dev/null @@ -1,64 +0,0 @@ -## ── evmbench production env ──────────────────────────────────── -## Copy to .env and fill in real values before deploying. -## Lines marked CHANGE are mandatory; the rest have sane defaults. - -# ── General ── -BACKEND_DEV=false - -# ── Web / FastAPI ── -BACKEND_WEB_HOST=0.0.0.0 -BACKEND_WEB_PORT=1337 -BACKEND_WEB_WORKERS=4 - -# ── URLs (CHANGE both to your real domains) ── -FRONTEND_PUBLIC_URL=https://app.YOURDOMAIN.com -BACKEND_PUBLIC_URL=https://api.YOURDOMAIN.com - -# ── Postgres ── -POSTGRES_USER=evmbench -POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD -POSTGRES_DB=evmbench - -# ── RabbitMQ ── -RABBITMQ_USER=evmbench -RABBITMQ_PASSWORD=CHANGE_ME_STRONG_PASSWORD -RABBITMQ_PORT=5672 -RABBITMQ_QUEUE=instancer.jobs - -# ── Secrets service ── -SECRETS_TOKEN_RO=CHANGE_ME_GENERATE_TOKEN -SECRETS_TOKEN_WO=CHANGE_ME_GENERATE_TOKEN -BACKEND_SECRETS_BACKEND=http -BACKEND_SECRETS_BACKEND_ARGUMENTS={"url":"http://secretsvc:8081","token":"CHANGE_ME_SAME_AS_SECRETS_TOKEN_WO"} - -SECRETSVC_HOST=0.0.0.0 -SECRETSVC_PORT=8081 -SECRETSVC_WORKERS=1 - -# ── Result service ── -RESULTSVC_PORT=8083 - -# ── OpenAI key handling ── -# direct (default): user provides key; worker uses it directly -# proxy (optional): worker receives encrypted token; oai_proxy decrypts and forwards -BACKEND_OAI_KEY_MODE=direct - -# ── OAI Proxy (only required when BACKEND_OAI_KEY_MODE=proxy) ── -# OAI_PROXY_BASE_URL=http://oai.loc:8084 -# OAI_PROXY_HOST=0.0.0.0 -# OAI_PROXY_PORT=8084 -# OAI_PROXY_WORKERS=1 -# OAI_PROXY_AES_KEY=CHANGE_ME_GENERATE_32BYTE_KEY - -# ── Auth (GitHub OAuth) ── -AUTH_BACKEND=github -AUTH_BACKEND_ARGUMENTS={"client_id":"CHANGE_ME","client_secret":"CHANGE_ME"} - -# ── JWT ── -BACKEND_JWT_SECRET=CHANGE_ME_GENERATE_SECRET - -# ── CORS extra origins (JSON list of additional allowed origins) ── -# BACKEND_CORS_EXTRA_ORIGINS=["https://custom.example.com"] - -# ── Caddy domain (used by Caddyfile env placeholder) ── -BACKEND_DOMAIN=api.YOURDOMAIN.com diff --git a/backend/api/mcp/auth.py b/backend/api/mcp/auth.py index e5035ab..7336a47 100644 --- a/backend/api/mcp/auth.py +++ b/backend/api/mcp/auth.py @@ -10,13 +10,12 @@ import hmac from typing import TYPE_CHECKING +if TYPE_CHECKING: + from asgiref.typing import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Scope # Header name for MCP API key (ASGI headers are lowercase bytes). MCP_API_KEY_HEADER = b'mcp-api-key' -if TYPE_CHECKING: - from asgiref.typing import ASGIApplication, ASGIReceiveCallable, ASGISendCallable, Scope - class McpApiKeyMiddleware: """ASGI middleware that rejects requests without a valid MCP-API-Key header.""" diff --git a/backend/instancer/backends/k8s.py b/backend/instancer/backends/k8s.py index 0029eb9..11bf1a8 100644 --- a/backend/instancer/backends/k8s.py +++ b/backend/instancer/backends/k8s.py @@ -223,13 +223,9 @@ async def start_worker(self, options: StartWorkerOptions) -> StartWorkerResult: ), spec=client.V1PodSpec( automount_service_account_token=False, - security_context=client.V1SecurityContext( - # TODO(trixter-osec): consider in the future hardening and running as non-root? - # run_as_user=65534, - # run_as_group=65534, - # run_as_non_root=True, - # fs_group=65534, # not supported by the python client apparently...? - ), + # Pod runs as root until the worker image supports a non-root USER; see SECURITY.md + # ("Kubernetes worker process identity"). Enable runAsNonRoot/runAsUser when the image does. + security_context=client.V1SecurityContext(), restart_policy='Never', containers=[ client.V1Container( diff --git a/deploy/gce-setup.sh b/deploy/gce-setup.sh index 45d0658..442faa4 100755 --- a/deploy/gce-setup.sh +++ b/deploy/gce-setup.sh @@ -8,7 +8,7 @@ # Prerequisites: # - gcloud CLI authenticated with a project set # - A domain with DNS you can point to the VM IP -# - backend/.env filled in from .env.production template +# - backend/.env copied from .env.example and filled with production secrets set -euo pipefail # ── Defaults ── @@ -98,7 +98,7 @@ echo "" echo "3) On the VM, clone and deploy:" echo " git clone /opt/evmbench" echo " cd /opt/evmbench/backend" -echo " cp .env.production .env # then edit with real secrets" +echo " cp .env.example .env # then edit with real secrets" echo "" echo " # Generate secrets easily:" echo " python3 -c \"import secrets; print(secrets.token_urlsafe(32))\""