Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 77 additions & 26 deletions worker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,50 +1,101 @@
FROM ubuntu:22.04
# SVF sources are PINNED to reviewed, immutable commit SHAs. To bump: update
# both SHAs together and re-review. ALLOW_UNPINNED_SVF=true is for local
# refresh builds only (the guard below refuses "master" otherwise).
# SVF_SHA -> AxisCommunications/signed-video-framework
# 1ae9fed = tag v2.3.5 (latest release; == master HEAD 2026-05-30)
# SVF_EXAMPLES_SHA -> AxisCommunications/signed-video-framework-examples
# e009c31 = master HEAD 2026-05-30 (repo has no tags; links the
# system-installed lib above, so it tracks v2.3.5)
FROM python:3.12-slim-bookworm AS svf-builder

ENV DEBIAN_FRONTEND=noninteractive

# System dependencies (meson+ninja for SVF, ffmpeg/gstreamer for video)
RUN apt-get update && apt-get install -y \
ARG SVF_SHA=1ae9fedfe6e7a7b6db65d05cc13f6098b1f92eba
ARG SVF_EXAMPLES_SHA=e009c310fef10a997ffad6d21720154fbb155a38
ARG ALLOW_UNPINNED_SVF=false

RUN if [ "$ALLOW_UNPINNED_SVF" != "true" ] \
&& { [ "$SVF_SHA" = "master" ] || [ "$SVF_EXAMPLES_SHA" = "master" ]; }; then \
echo "SVF_SHA and SVF_EXAMPLES_SHA must be reviewed full commit SHAs for production builds"; \
echo "Pass --build-arg ALLOW_UNPINNED_SVF=true only for local refresh builds"; \
exit 1; \
fi

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
meson \
ninja-build \
pkg-config \
ca-certificates \
git \
python3 \
python3-pip \
libgstreamer1.0-dev \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-ugly \
gstreamer1.0-libav \
libcurl4-openssl-dev \
libglib2.0-dev \
libgstreamer-plugins-base1.0-dev \
libgstreamer1.0-dev \
libssl-dev \
libglib2.0-dev \
libcurl4-openssl-dev \
ffmpeg \
meson \
ninja-build \
pkg-config \
&& rm -rf /var/lib/apt/lists/*

# Clone and build libsigned-video-framework with meson
RUN git clone https://github.com/AxisCommunications/signed-video-framework.git /opt/svf \
&& git -C /opt/svf checkout --detach "${SVF_SHA}" \
&& meson setup /opt/svf /opt/svf-build \
&& ninja -C /opt/svf-build \
&& ninja -C /opt/svf-build install \
&& ldconfig
&& ninja -C /opt/svf-build install

# Clone and build signed-video-framework-examples (contains validator binary)
RUN git clone https://github.com/AxisCommunications/signed-video-framework-examples.git /opt/svf-examples \
&& git -C /opt/svf-examples checkout --detach "${SVF_EXAMPLES_SHA}" \
&& meson setup -Dbuild_all_apps=true /opt/svf-examples /opt/svf-examples-build \
&& ninja -C /opt/svf-examples-build \
&& ninja -C /opt/svf-examples-build install \
&& ldconfig
&& ninja -C /opt/svf-examples-build install


FROM python:3.12-slim-bookworm AS runtime

ENV DEBIAN_FRONTEND=noninteractive \
PATH=/usr/local/bin:/usr/bin:/bin \
LD_LIBRARY_PATH=/usr/local/lib:/usr/local/lib/x86_64-linux-gnu \
PYTHONUNBUFFERED=1

RUN apt-get update && apt-get install -y --no-install-recommends \
bubblewrap \
ca-certificates \
ffmpeg \
gstreamer1.0-libav \
gstreamer1.0-plugins-bad \
gstreamer1.0-plugins-base \
gstreamer1.0-plugins-good \
gstreamer1.0-plugins-ugly \
libcurl4 \
libglib2.0-0 \
libgstreamer-plugins-base1.0-0 \
libgstreamer1.0-0 \
libseccomp2 \
libssl3 \
seccomp \
strace \
&& rm -rf /var/lib/apt/lists/*

COPY --from=svf-builder /usr/local/bin/ /usr/local/bin/
COPY --from=svf-builder /usr/local/lib/ /usr/local/lib/

# Python app
WORKDIR /app
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

COPY app/sandbox_launcher.py /usr/local/bin/edgeproof-rlimit-launcher
RUN chmod 0755 /usr/local/bin/edgeproof-rlimit-launcher \
&& ldconfig \
&& ffprobe -version >/dev/null \
&& (command -v signed-video-validator || command -v sv_validator || command -v validator)

COPY certs/ /app/certs/
COPY app/ /app/app/
COPY tests/ /app/tests/

RUN useradd -r -u 10001 -m -d /home/svc -s /usr/sbin/nologin svc \
&& mkdir -p /tmp/edgeproof \
&& chown -R svc:svc /tmp/edgeproof

USER svc

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]
78 changes: 78 additions & 0 deletions worker/TIER0-BUILD-REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Tier 0 Sandbox Build Report

## Implemented

- Added `app/sandbox.py` with `SandboxResult`, startup capability probing, cached `bwrap` vs `DEGRADED` mode, bounded output capture, process-group kill on timeout, scrubbed child env, and fail-closed `run_sandboxed()`.
- Added exec-only rlimit launcher at `app/sandbox_launcher.py`; it sets `RLIMIT_AS`, `RLIMIT_CPU`, `RLIMIT_FSIZE`, `RLIMIT_NOFILE`, `RLIMIT_NPROC`, `RLIMIT_CORE=0`, and `PR_SET_NO_NEW_PRIVS`, then `execvpe()`s the final binary.
The launcher uses stdlib `resource` plus `ctypes` against libc for `prctl`; no `preexec_fn` is used.
- Routed both ffprobe sites and the SVF validator through `run_sandboxed()`.
- Added ffprobe `-protocol_whitelist file -analyzeduration 5M -probesize 10M` at both ffprobe call sites.
`-protocol_whitelist file` limits ffmpeg protocols only; path traversal protection comes from the filesystem namespace/bind set.
- Replaced unbounded upload read with Content-Length rejection plus bounded 8 MB chunk streaming to disk.
- Added the worker-local verification semaphore and explicit sandbox memory budget defaults.
- Added `/health` sandbox mode reporting and `/verify` 503 gating for `DEGRADED` unless `ALLOW_DEGRADED_SANDBOX=true`.
- Hardened SVF cleanup to `shutil.rmtree(..., ignore_errors=True)`.
- Added `O_NOFOLLOW`/`openat`-style result-file read for `validation_results.txt`.
- Added fail-closed handling for nonzero validator exits, signal deaths, launch failures, and timeouts before verdict parsing.
- Added parent-side ffprobe parse guards for hostile duration/frame/shape values.
- Reworked Dockerfile into builder/runtime stages, non-root `USER svc` uid `10001`, runtime `bubblewrap`, `seccomp`, `strace`, ffmpeg/GStreamer runtime deps, and copied only installed SVF artifacts into runtime.
- Added `pytest.ini` and `tests/test_sandbox.py` with host tests plus `@pytest.mark.linux_sandbox` Linux-container tests.

## Resource Defaults

- `MAX_FILE_SIZE_BYTES`: 500 MiB
- `SANDBOX_MAX_INPUT_BYTES`: 500 MiB
- `SANDBOX_RLIMIT_AS_BYTES`: 1 GiB
- `SANDBOX_RLIMIT_CPU_SECONDS_FFPROBE`: 20 seconds
- `SANDBOX_RLIMIT_CPU_SECONDS_VALIDATOR`: 60 seconds
- `SANDBOX_RLIMIT_FSIZE_BYTES`: 16 MiB
- `SANDBOX_RLIMIT_NPROC`: 64
- `SANDBOX_MAX_OUTPUT_BYTES`: 16 MiB
- `SANDBOX_MEMORY_LIMIT_BYTES`: 1536 MiB
- `SANDBOX_MAX_CONCURRENT_JOBS`: 1
- `SANDBOX_ALLOW_NET`: false
- `ALLOW_DEGRADED_SANDBOX`: false

## Acceptance Coverage

- Host tests: AC4a, AC8 partial, AC12 partial, AC13 boundary rejection, AC15, AC17 route gate.
- Linux-container tests: AC1, AC2, AC3, AC4b, AC5, AC6, AC8, AC9, AC10, AC11, AC14, AC17, plus ldd bind-contract coverage.
- Conditional tests: AC7 is `xfail` because seccomp-bpf is deliberately deferred; AC16 is `xfail` until genuine/tampered AXIS byte fixtures are added.

Expected CI command:

```bash
docker build -t epworker .
docker run --rm epworker pytest -m linux_sandbox
```

Host static/unit command once Python deps are installed:

```bash
python -m pytest -m "not linux_sandbox"
```

## Verification Run Here

- `python -m py_compile app/sandbox.py app/sandbox_launcher.py app/services/video_info.py app/services/svf_runner.py app/main.py tests/test_sandbox.py` passed.
- AST parse check for the same files passed.
- Host pytest did not run in this macOS workspace because `pytest` and app deps such as `pydantic_settings` are not installed.
- Docker/Linux rubric was not run here; local shell networking is blocked, so `docker build` cannot fetch SVF repos from GitHub in this environment.

## Deferred / Conditional

- Control G seccomp-bpf is deferred. No fail-open seccomp profile was shipped. `--unshare-net` remains the network boundary for Increment 1.
- Amendment 21 verdict-from-text injection is flag-only in this increment. A threat-model note was added near `parse_svf_output`; the parser still needs a later authoritative-status-line fix.
- Amendment 22 callback SSRF is flag-only in this increment. A threat-model note was added near the callback POST; parent callback allowlisting is deferred.
- AC16 real-bytes regression test is present as a fixture contract, but the repo has no genuine AXIS signed clip or tampered fixture to execute it.

## Deviations

- The Dockerfile has `SVF_SHA` and `SVF_EXAMPLES_SHA` build args and detached checkouts, but the defaults remain `master` because this environment could not resolve current upstream commit SHAs via shell networking. Before production use, set both defaults to reviewed full commit SHAs or pass them as build args in CI.

## Fix-pass (review round 1)

- FIX-1: Added `SandboxResult.output_overflow`, set it from the bounded-output reader, and fail closed on output overflow before SVF verdict parsing or ffprobe JSON parsing at both ffprobe gates.
- FIX-2: Changed the rlimit launcher to return sentinel exit code `125` for launcher setup/exec failures while keeping `127` for missing command, and changed sandbox launcher-failure detection to use only numeric `125`/`127`.
- FIX-3: Added `--unshare-cgroup` to the bubblewrap namespace flags used by normal sandbox runs and the capability probe.
- FIX-4: Added a Dockerfile build guard that fails if `SVF_SHA` or `SVF_EXAMPLES_SHA` is `master` unless `ALLOW_UNPINNED_SVF=true` is passed, and documented that production builds require reviewed full commit SHAs.
Loading