-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathDockerfile
More file actions
101 lines (95 loc) · 5.94 KB
/
Copy pathDockerfile
File metadata and controls
101 lines (95 loc) · 5.94 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# SECURITY REVIEW / VERIFY-BEFORE-DEPLOY — infra hardening PR (audit A402-01/06
# + mutable-artifacts). Still needs a Railway PREVIEW build before the deploy
# path (the image isn't built in CI), but the /data volume behaviour this design
# hinges on was VERIFIED against the live Railway deployment on 2026-07-18:
# agent402-volume mounts at /data owned by root:root, so the non-root switch is
# done via a root entrypoint that chowns /data then drops to node (below), NOT a
# Dockerfile USER. See docs/security-infra-hardening.md for the full checklist.
#
# Base image pinned by DIGEST for reproducible builds and CVE traceability
# (audit: "mutable deployment artifacts"). node:22-slim as of 2026-07-18. Re-pin
# after a deliberate base bump with:
# docker pull node:22-slim
# docker inspect --format='{{index .RepoDigests 0}}' node:22-slim
FROM node:22-slim@sha256:6c74791e557ce11fc957704f6d4fe134a7bc8d6f5ca4403205b2966bd488f6b3
WORKDIR /app
ENV NODE_ENV=production
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
COPY package.json package-lock.json ./
# ffmpeg powers the audio tools (normalize/convert/info); gosu drops privileges
# at startup (see the entrypoint). Installed alongside Chromium's deps.
RUN npm ci --omit=dev && npx playwright install --with-deps chromium \
&& apt-get update && apt-get install -y --no-install-recommends ffmpeg gosu \
&& rm -rf /var/lib/apt/lists/* \
# sanity-check gosu works (it silently no-ops on a broken install)
&& gosu node true \
# A402-06 / CVE-2026-8461 (FFmpeg MagicYUV) build gate: the audio toolchain
# MUST be present — a base-image drift that drops ffmpeg/ffprobe should FAIL
# the build loudly, not ship an image that 500s every media tool. Then record
# the exact build + whether the vulnerable MagicYUV *video* decoder is present.
# We do NOT fail on its presence: our tools only decode AUDIO (`-vn` on every
# ffmpeg call, enforced by scripts/test-ffmpeg-novideo.js), so the decoder is
# unreachable through our flags; failing would just break builds over an
# unexploitable path. scripts/check-ffmpeg-cve.sh reads the recorded status.
&& { command -v ffmpeg >/dev/null || { echo "FATAL: ffmpeg missing from image"; exit 1; }; } \
&& { command -v ffprobe >/dev/null || { echo "FATAL: ffprobe missing from image"; exit 1; }; } \
&& ffmpeg -version | head -1 > /app/.ffmpeg-version \
&& (ffmpeg -hide_banner -decoders 2>/dev/null | grep -i magicyuv >> /app/.ffmpeg-version || echo "magicyuv-decoder: absent" >> /app/.ffmpeg-version)
# Container hardening (audit R-04/R-05 blast-radius reduction — the achievable
# subset). Strip the setuid/setgid bit from every binary in the image so a
# post-compromise attacker inside the container has NO local privilege-escalation
# helper (su, mount, chsh, Chromium's SUID sandbox, …). Safe for our runtime:
# the server already runs non-root; gosu drops privileges via syscalls as root,
# not via a setuid bit; and Chromium runs with --no-sandbox so the SUID sandbox
# helper is unused. `-xdev` keeps the sweep on the image filesystem.
#
# NOTE: seccomp, capability-drop, a read-only root filesystem, and network
# egress firewalling are the REST of the container-hardening story — Railway's
# platform does not expose Docker security-opt / egress controls, so they are
# NOT settable from this repo. Closing them needs the secretless worker services
# in docs/worker-isolation-plan.md (Phases 2-3) or a platform that supports them.
RUN find / -xdev -perm /6000 -type f -exec chmod a-s {} + 2>/dev/null || true
COPY src ./src
# start.js is the shared-image dispatcher; worker/ is the secretless browser+media
# worker it boots when WORKER_MODE=true. Both services run THIS image (railway.toml
# pins every service to Dockerfile); WORKER_MODE unset → the main API server.
COPY start.js ./
COPY worker ./worker
# scripts/demo-payment.js is served at /demo.js (the runnable buyer demo)
COPY scripts ./scripts
# wiki/ is the source of truth for /docs (server-rendered) and is CI-synced
# to the GitHub wiki. Must be in the image or /docs is empty.
COPY wiki ./wiki
# assets/fonts is embedded into the brand images at boot — a missing file is
# a boot crash, not a degraded render.
COPY assets ./assets
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# A402-01: run the server as the unprivileged `node` user, NOT root. A renderer
# or media-parser compromise then lands as UID 1000 — unable to touch root-owned
# files or escalate — instead of as root in the container.
#
# We do NOT use a Dockerfile `USER node`, on purpose: VERIFIED on the live
# Railway deployment (2026-07-18) that the persistent volume `agent402-volume`
# mounts at /data owned by root:root at RUNTIME, and it holds the memory/stats
# SQLite (~1GB). A `USER node` container could not write it and the memory
# boot-fail-loud would fire on deploy. Instead docker-entrypoint.sh runs as root
# JUST long enough to chown /data to node, then execs the server via gosu so the
# process itself is non-root. `exec` keeps node as PID 1 so it receives SIGTERM
# for the graceful drain.
#
# NOTE (--no-sandbox stays): src/tools/render.js still launches Chromium with
# --no-sandbox because this container has no user-namespace / seccomp profile
# for Chromium's own sandbox. Removing it REQUIRES enabling that at the platform
# level first (see docs/security-infra-hardening.md) — dropping it here blindly
# 503s every browser tool. Non-root already removes the "escape == root" impact.
#
# NOTE (full isolation is a follow-up): the browser and media parsers still
# share this container with the payment/DB/operator env. True A402-01/02/06
# isolation is a separate browser/media worker service with no secrets — see
# the design doc. This PR does not implement it.
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 3000
# start.js dispatches: WORKER_MODE=true → the secretless worker, else the API.
# Still `node <script>` so the gosu entrypoint keeps dropping to the node user.
CMD ["node", "start.js"]