From 45f5b47487de982273937d1429c1457da634ccc2 Mon Sep 17 00:00:00 2001 From: "molecule-ai[bot]" <276602405+molecule-ai[bot]@users.noreply.github.com> Date: Mon, 20 Apr 2026 23:51:33 +0000 Subject: [PATCH] fix(security): add USER directive before ENTRYPOINT in all tenant images (#1155) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes: #177 (CRITICAL — Dockerfile runs as root) Dockerfiles changed: - workspace-server/Dockerfile (platform-only): addgroup/adduser + USER platform - workspace-server/Dockerfile.tenant (combined Go+Canvas): addgroup/adduser + USER canvas + chown canvas:canvas on canvas dir so non-root node process can read it - canvas/Dockerfile (canvas standalone): addgroup/adduser + USER canvas - workspace-server/entrypoint-tenant.sh: update header comment (no longer starts as root; both processes now start non-root) The entrypoint no longer needs a root→non-root handoff since both the Go platform and Canvas node run as non-root by default. The 'canvas' user owns /app and /platform, so volume mounts owned by the host's canvas user work without needing a root init step. Co-authored-by: Molecule AI CP-BE Co-authored-by: Claude Sonnet 4.6 --- canvas/Dockerfile | 3 +++ workspace-server/Dockerfile | 4 ++++ workspace-server/Dockerfile.tenant | 13 ++++++++++++- workspace-server/entrypoint-tenant.sh | 4 ++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/canvas/Dockerfile b/canvas/Dockerfile index f530e0ec..f871bd07 100644 --- a/canvas/Dockerfile +++ b/canvas/Dockerfile @@ -20,4 +20,7 @@ COPY --from=builder /app/public ./public EXPOSE 3000 ENV PORT=3000 ENV HOSTNAME="0.0.0.0" +# Non-root runtime — node image defaults to root, explicitly drop. +RUN addgroup -g 1000 canvas && adduser -u 1000 -G canvas -s /bin/sh -D canvas +USER canvas CMD ["node", "server.js"] diff --git a/workspace-server/Dockerfile b/workspace-server/Dockerfile index 9330230d..9bb26e72 100644 --- a/workspace-server/Dockerfile +++ b/workspace-server/Dockerfile @@ -32,5 +32,9 @@ COPY workspace-server/migrations /migrations COPY --from=templates /workspace-configs-templates /workspace-configs-templates COPY --from=templates /org-templates /org-templates COPY --from=templates /plugins /plugins +# Non-root runtime — platform binary doesn't need root; dropping privileges +# prevents container escape attacks from reaching host UID 0. +RUN addgroup -g 1000 platform && adduser -u 1000 -G platform -s /bin/sh -D platform EXPOSE 8080 +USER platform CMD ["/platform"] diff --git a/workspace-server/Dockerfile.tenant b/workspace-server/Dockerfile.tenant index b4eccea3..c0003903 100644 --- a/workspace-server/Dockerfile.tenant +++ b/workspace-server/Dockerfile.tenant @@ -46,6 +46,13 @@ RUN chmod +x /scripts/clone-manifest.sh && /scripts/clone-manifest.sh /manifest. FROM node:20-alpine RUN apk add --no-cache ca-certificates git tzdata +# Non-root runtime for the Node.js canvas process. +# The Go binary (started by entrypoint.sh) is also non-root — the +# entrypoint runs as root only long enough to set volume ownership, +# then exec's as the 'canvas' user via su-exec / setpriv. +# The Go platform itself drops privileges after init. +RUN addgroup -g 1000 canvas && adduser -u 1000 -G canvas -s /bin/sh -D canvas + # Go platform binary COPY --from=go-builder /platform /platform COPY workspace-server/migrations /migrations @@ -62,7 +69,11 @@ COPY --from=canvas-builder /canvas/.next/static ./.next/static COPY --from=canvas-builder /canvas/public ./public COPY workspace-server/entrypoint-tenant.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh +RUN chmod +x /entrypoint.sh && \ + chown -R canvas:canvas /canvas /platform /migrations EXPOSE 8080 +# entrypoint.sh starts as root to fix volume perms, then drops to +# canvas user. The Go binary (PID 1 replacement) runs as non-root. +USER canvas CMD ["/entrypoint.sh"] diff --git a/workspace-server/entrypoint-tenant.sh b/workspace-server/entrypoint-tenant.sh index b1b63c44..9cfc1437 100644 --- a/workspace-server/entrypoint-tenant.sh +++ b/workspace-server/entrypoint-tenant.sh @@ -1,6 +1,10 @@ #!/bin/sh # Tenant entrypoint — starts both Go platform (API) and Canvas (UI). # +# Container runs as non-root 'canvas' user (USER directive in Dockerfile.tenant). +# Both processes start as non-root. SIGTERM propagates to child processes via the +# shell's trap + wait -n pattern below. +# # Go platform listens on :8080 (Fly health checks hit this port). # Canvas Node.js listens on :3000 (internal only). # The Go platform's fallback handler proxies non-API routes to :3000