diff --git a/backend/Dockerfile b/backend/Dockerfile index b1b17595b46..0c80988a4bf 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -120,6 +120,14 @@ WORKDIR /app # Copy backend with pre-built virtualenv from builder COPY --from=builder /app/backend ./backend +# Run as an unprivileged user. The Gateway writes runtime state under +# ~/.deer-flow, so keep HOME writable for this user. Deployments that need +# the DooD sandbox overlay can opt back into root via `user: root` in +# docker-compose (see docker/docker-compose.dood.yaml). +RUN useradd --create-home --uid 1000 deerflow +USER deerflow +ENV HOME=/home/deerflow + # Expose Gateway API port. EXPOSE 8001 diff --git a/docker/docker-compose.dood.yaml b/docker/docker-compose.dood.yaml index d353407c3df..42cc250a321 100644 --- a/docker/docker-compose.dood.yaml +++ b/docker/docker-compose.dood.yaml @@ -22,5 +22,9 @@ # onto it. DEER_FLOW_DOCKER_SOCKET defaults to /var/run/docker.sock. services: gateway: + # The base image now runs as an unprivileged user; talking to the host + # Docker socket (root:root by default) requires root, so this overlay + # opts the gateway back in explicitly. + user: root volumes: - ${DEER_FLOW_DOCKER_SOCKET:-/var/run/docker.sock}:/var/run/docker.sock diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf index 61f97a9248b..988cdb78d5e 100644 --- a/docker/nginx/nginx.conf +++ b/docker/nginx/nginx.conf @@ -69,6 +69,12 @@ http { proxy_buffering off; proxy_cache off; + # The Gateway trusts X-DeerFlow-Internal-Token / X-DeerFlow-Owner-User-Id + # as internal credentials that bypass session auth. nginx does not + # inherit proxy_set_header into locations that define their own, so + # every proxying location below clears these two headers explicitly + # to keep browser-supplied values from ever reaching the Gateway. + # Keep the unified nginx endpoint same-origin by default. When split # frontend/backend or port-forwarded deployments need browser CORS, # configure the Gateway allowlist with GATEWAY_CORS_ORIGINS so CORS and @@ -84,6 +90,8 @@ http { # Headers proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -117,6 +125,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -128,6 +138,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -139,6 +151,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -150,6 +164,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -161,6 +177,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -172,6 +190,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -190,6 +210,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -208,6 +230,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -219,6 +243,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -230,6 +256,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -241,6 +269,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -252,6 +282,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -266,6 +298,8 @@ http { proxy_pass http://$provisioner_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -278,6 +312,8 @@ http { proxy_pass http://$gateway_upstream; proxy_http_version 1.1; proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; @@ -293,6 +329,8 @@ http { # Headers proxy_set_header Host $http_host; + proxy_set_header X-DeerFlow-Internal-Token ""; + proxy_set_header X-DeerFlow-Owner-User-Id ""; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $forwarded_proto; diff --git a/docker/provisioner/Dockerfile b/docker/provisioner/Dockerfile index 96ef93156f9..ca43435c401 100644 --- a/docker/provisioner/Dockerfile +++ b/docker/provisioner/Dockerfile @@ -24,6 +24,9 @@ RUN pip install --no-cache-dir \ WORKDIR /app COPY app.py . +RUN useradd --create-home --uid 1000 deerflow +USER deerflow + EXPOSE 8002 CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8002"] diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 4c6b22123c3..b4998461ce0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -51,5 +51,7 @@ RUN pnpm config set store-dir ${PNPM_STORE_PATH} RUN if [ -n "${NPM_REGISTRY}" ]; then pnpm config set registry "${NPM_REGISTRY}"; fi WORKDIR /app COPY --from=builder /app/frontend ./frontend +# Run as the unprivileged `node` user (uid/gid 1000) shipped with the image. +USER node EXPOSE 3000 CMD ["sh", "-c", "cd /app/frontend && pnpm start"] diff --git a/frontend/src/app/api/memory/[...path]/route.ts b/frontend/src/app/api/memory/[...path]/route.ts index 8de48d7425c..0923c66f038 100644 --- a/frontend/src/app/api/memory/[...path]/route.ts +++ b/frontend/src/app/api/memory/[...path]/route.ts @@ -12,6 +12,11 @@ async function proxyRequest(request: NextRequest, pathname: string) { headers.delete("host"); headers.delete("connection"); headers.delete("content-length"); + // The gateway treats these as internal trust credentials that bypass + // session auth; browser clients can set arbitrary custom headers, so a + // request transiting this public proxy must never forward them. + headers.delete("x-deerflow-internal-token"); + headers.delete("x-deerflow-owner-user-id"); const hasBody = !["GET", "HEAD"].includes(request.method); const response = await fetch(buildBackendUrl(pathname), { diff --git a/frontend/src/app/api/memory/route.ts b/frontend/src/app/api/memory/route.ts index 2957c69c1a1..00a3e73a567 100644 --- a/frontend/src/app/api/memory/route.ts +++ b/frontend/src/app/api/memory/route.ts @@ -12,6 +12,11 @@ async function proxyRequest(request: NextRequest, pathname: string) { headers.delete("host"); headers.delete("connection"); headers.delete("content-length"); + // The gateway treats these as internal trust credentials that bypass + // session auth; browser clients can set arbitrary custom headers, so a + // request transiting this public proxy must never forward them. + headers.delete("x-deerflow-internal-token"); + headers.delete("x-deerflow-owner-user-id"); const hasBody = !["GET", "HEAD"].includes(request.method); const response = await fetch(buildBackendUrl(pathname), {