Files
Roman Donchenko 4aa0be3df5 Prevent source files from being modified by the Django user (#10575)
Currently, all source files are located in `/home/django`, and owned by
`django`. This means that if there's any vulnerability that lets an
attacker overwrite files in the server, they can replace source files
with their own code, and potentially get that code executed. That's
pretty bad, so I want to harden against that.

Make all source files owned by root, and move them to `/opt/cvat`. Add a
`manage.py` symlink in `/home/django` for backwards compatibility. It
happens that if a script is a symlink, Python does not add the symlink's
directory to `sys.path`, which is great for us, since that lets us avoid
a writable directory on there.

Still, even though `/home/django/manage.py` is owned by root, an
attacker could potentially be able to delete it and replace it with
their own malicious file. To be a bit more safe, replace `~/manage.py`
calls in backend scripts with `django-admin`.

To make sure CVAT can still find the data directory, add a new
environment variable, `CVAT_BASE_DIR` and set it in the Docker image.

This also fixes a minor bug: we no longer override the `HOME`
environment variable in the `Dockerfile`, so now it's automatically set
by `Docker` depending on the current user.
2026-07-23 13:22:24 +03:00

221 lines
7.1 KiB
Docker

# syntax=docker/dockerfile:1
ARG BASE_IMAGE=ubuntu:24.04
FROM ${BASE_IMAGE} AS build-image-base
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
cargo-1.85 \
curl \
g++ \
gcc \
git \
libhdf5-dev \
libldap2-dev \
libmp3lame-dev \
libsasl2-dev \
libxml2-dev \
libxmlsec1-dev \
libxmlsec1-openssl \
make \
nasm \
pkg-config \
python3-dev \
python3-pip \
&& update-alternatives \
--install /usr/bin/rustc rustc /usr/bin/rustc-1.85 185 \
--slave /usr/bin/cargo cargo /usr/bin/cargo-1.85 \
&& rm -rf /var/lib/apt/lists/*
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
# We build OpenH264, FFmpeg and PyAV in a separate build stage,
# because this way Docker can do it in parallel to all the other packages.
FROM build-image-base AS build-image-av
# Compile Openh264 and FFmpeg
ARG PREFIX=/opt/ffmpeg
ARG PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
ENV FFMPEG_VERSION=8.0 \
OPENH264_VERSION=2.6.0
WORKDIR /tmp/openh264
RUN curl -sL https://github.com/cisco/openh264/archive/v${OPENH264_VERSION}.tar.gz --output - | \
tar -zx --strip-components=1 && \
make -j5 && make install-shared PREFIX=${PREFIX} && make clean
WORKDIR /tmp/ffmpeg
RUN curl -sL https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz --output - | \
tar -zx --strip-components=1 && \
./configure --disable-nonfree --disable-gpl --enable-libopenh264 --enable-libmp3lame \
--enable-shared --disable-static --disable-doc --disable-programs --prefix="${PREFIX}" && \
make -j5 && make install && make clean
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
# Since we're using pip-compile-multi, each dependency can only be listed in
# one requirements file. In the case of PyAV, that should be
# `dataset_manifest/requirements.txt`. Make sure it's actually there,
# and then remove everything else.
RUN grep -q '^av==' /tmp/utils/dataset_manifest/requirements.txt
RUN sed -i '/^av==/!d' /tmp/utils/dataset_manifest/requirements.txt
RUN --mount=type=cache,target=/root/.cache/pip/http-v2 \
python3 -m pip wheel --no-binary=av \
-r /tmp/utils/dataset_manifest/requirements.txt \
-w /tmp/wheelhouse
# This stage builds wheels for all dependencies (except PyAV)
FROM build-image-base AS build-image
COPY cvat/requirements/ /tmp/cvat/requirements/
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
# Exclude av from the requirements file
RUN sed -i '/^av==/d' /tmp/utils/dataset_manifest/requirements.txt
ARG CVAT_CONFIGURATION="production"
# https://github.com/SAML-Toolkits/python3-saml#note
# Building from source is recommended for lxml and xmlsec to avoid libxml2 version conflicts
RUN --mount=type=cache,target=/root/.cache/pip/http-v2 \
DATUMARO_HEADLESS=1 python3 -m pip wheel --no-deps --no-binary lxml,xmlsec \
-r /tmp/cvat/requirements/${CVAT_CONFIGURATION}.txt \
-w /tmp/wheelhouse
FROM golang:1.26.5 AS build-smokescreen
RUN git clone --filter=blob:none --no-checkout https://github.com/stripe/smokescreen.git
RUN cd smokescreen && git checkout eb1ac09 && go build -o /tmp/smokescreen
FROM ${BASE_IMAGE}
ARG http_proxy
ARG https_proxy
ARG no_proxy
ARG socks_proxy
ARG TZ="Etc/UTC"
ENV TERM=xterm \
http_proxy=${http_proxy} \
https_proxy=${https_proxy} \
no_proxy=${no_proxy} \
socks_proxy=${socks_proxy} \
LANG='C.UTF-8' \
LC_ALL='C.UTF-8' \
TZ=${TZ}
ARG USER="django"
ARG CVAT_CONFIGURATION="production"
ENV DJANGO_SETTINGS_MODULE="cvat.settings.${CVAT_CONFIGURATION}"
# Install necessary apt packages
RUN apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
adduser \
bzip2 \
ca-certificates \
curl \
git \
libgl1 \
libgomp1 \
libldap2 \
libmp3lame0 \
libpython3.12t64 \
libsasl2-2 \
libxml2 \
libxmlsec1 \
libxmlsec1-openssl \
nginx \
p7zip-full \
poppler-utils \
python3 \
python3-venv \
supervisor \
tzdata \
unrar \
wait-for-it \
&& ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
dpkg-reconfigure -f noninteractive tzdata && \
rm -rf /var/lib/apt/lists/*
# Install smokescreen
COPY --from=build-smokescreen /tmp/smokescreen /usr/local/bin/smokescreen
# Add a non-root user
ENV USER=${USER}
RUN deluser --remove-home ubuntu && \
adduser --uid=1000 --shell /bin/bash --disabled-password --gecos "" ${USER}
ENV CVAT_BASE_DIR=/home/${USER}
ARG CLAM_AV="no"
RUN if [ "$CLAM_AV" = "yes" ]; then \
apt-get update && \
apt-get --no-install-recommends install -yq \
clamav \
libclamunrar && \
sed -i 's/ReceiveTimeout 30/ReceiveTimeout 300/g' /etc/clamav/freshclam.conf && \
freshclam && \
chown -R ${USER}:${USER} /var/lib/clamav && \
rm -rf /var/lib/apt/lists/*; \
fi
# Install wheels from the build image
RUN python3 -m venv /opt/venv
ENV PATH="/opt/venv/bin:${PATH}"
ARG PIP_DISABLE_PIP_VERSION_CHECK=1
RUN --mount=type=bind,from=build-image,source=/tmp/wheelhouse,target=/mnt/wheelhouse \
--mount=type=bind,from=build-image-av,source=/tmp/wheelhouse,target=/mnt/wheelhouse-av \
python -m pip install --no-index /mnt/wheelhouse/*.whl /mnt/wheelhouse-av/*.whl
ENV NUMPROCS=1
COPY --from=build-image-av /opt/ffmpeg/lib /usr/lib
# These variables are required for supervisord substitutions in files
# This library allows remote python debugging with VS Code
ARG CVAT_DEBUG_ENABLED
RUN if [ "${CVAT_DEBUG_ENABLED}" = 'yes' ]; then \
python3 -m pip install --no-cache-dir debugpy; \
fi
# Removing pip due to security reasons. See: https://scout.docker.com/vulnerabilities/id/CVE-2018-20225
# The vulnerability is dubious and we don't use pip at runtime, but some vulnerability scanners mark it as a high vulnerability,
# and it was decided to remove pip from the final image
RUN python -m pip uninstall -y pip
# Install and initialize CVAT, copy all necessary files
COPY cvat/nginx.conf /etc/nginx/nginx.conf
COPY --parents \
backend_entrypoint.d cvat supervisord utils \
backend_entrypoint.sh \
components/analytics/clickhouse/init.py \
manage.py \
rqscheduler.py \
wait_for_deps.sh \
/opt/cvat/
RUN python -m compileall -q /opt/cvat
# Link manage.py to the home directory for backwards compatibility.
RUN ln -s /opt/cvat/manage.py ${CVAT_BASE_DIR}/manage.py
RUN echo "/opt/cvat" > /opt/venv/lib/python3.12/site-packages/cvat.pth
ARG COVERAGE_PROCESS_START
RUN if [ "${COVERAGE_PROCESS_START}" ]; then \
echo "import coverage; coverage.process_startup()" > /opt/venv/lib/python3.12/site-packages/coverage_subprocess.pth; \
fi
# RUN all commands below as 'django' user.
# Use numeric UID/GID so that the image is compatible with the Kubernetes runAsNonRoot setting.
USER 1000:1000
WORKDIR ${CVAT_BASE_DIR}
RUN mkdir -p data share keys logs /tmp/supervisord /tmp/cvat static
EXPOSE 8080
ENTRYPOINT ["/opt/cvat/backend_entrypoint.sh"]