diff --git a/Dockerfile b/Dockerfile index 118a285..c8aa139 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,8 +5,13 @@ LABEL description='Slim Docker container featuring embedded Nut server from blaw LABEL repo='eskwisit/nut-server' LABEL email='mru2683@gmail.com' +ARG NUT_VERSION=3.3 +ARG NUT_SHA256=3d8e8723c13385e098c9c76e2417e8e5a1ca3c51176b42a5d443ba9432dbc28b + # Prepare Nut install -RUN apt-get update && apt-get -y install \ +RUN apt-get update && apt-get -y install --no-install-recommends \ + ca-certificates \ + gosu \ wget \ unzip \ gcc \ @@ -15,32 +20,58 @@ RUN apt-get update && apt-get -y install \ zlib1g-dev \ libjpeg-dev -# Fetch Nut codebase -RUN wget https://github.com/blawar/nut/archive/refs/tags/v3.3.zip -RUN unzip v3.3.zip -d /root -RUN mv /root/nut-3.3 /root/nut +# Fetch Nut codebase and verify its integrity +RUN wget -O nut.zip "https://github.com/blawar/nut/archive/refs/tags/v${NUT_VERSION}.zip" +RUN echo "${NUT_SHA256} nut.zip" | sha256sum -c - +RUN unzip nut.zip -d /root +RUN mv "/root/nut-${NUT_VERSION}" /root/nut # Edit config RUN mv /root/nut/conf/nut.default.conf /root/nut/conf/nut.conf RUN sed -i '/scan/c "scan": ["\/titles"]' /root/nut/conf/nut.conf +# Local drive access lets any authenticated client read arbitrary paths of the +# container filesystem and disables ".." traversal checks +RUN sed -i '/enableLocalDriveAccess/c "enableLocalDriveAccess": 0,' /root/nut/conf/nut.conf +# Upstream ships a users.conf holding the default guest/guest account; the +# entrypoint generates a real one instead +RUN rm -f /root/nut/conf/users.conf +# Keep a pristine copy outside of the volumes so the entrypoint can restore it +RUN mkdir -p /opt/nut-defaults && cp /root/nut/conf/nut.conf /opt/nut-defaults/nut.conf # Remove GUI packages RUN sed -i '/pyqt5/d' /root/nut/requirements.txt RUN sed -i '/qt-range-slider/d' /root/nut/requirements.txt +# Update dependencies carrying known vulnerabilities +RUN sed -i '/^requests/c requests>=2.32.3' /root/nut/requirements.txt +RUN sed -i '/^Pillow/c Pillow>=10.3.0' /root/nut/requirements.txt +RUN sed -i '/^urllib3/c urllib3>=1.26.19,<2' /root/nut/requirements.txt + # Add missing requirements RUN echo markupsafe==2.0.1 >>/root/nut/requirements.txt # Install project dependencies -RUN pip3 install -U pip -RUN pip3 install -r /root/nut/requirements.txt +RUN pip3 install -U --no-cache-dir pip +RUN pip3 install --no-cache-dir -r /root/nut/requirements.txt + +# Drop the build toolchain and the bundled Nintendo shop client certificate, +# which holds a private key and is only used by the CDN downloader +RUN rm -f /root/nut/ShopN.pem +RUN apt-get purge -y --auto-remove wget unzip gcc libssl-dev libcurl4-openssl-dev zlib1g-dev libjpeg-dev +RUN apt-get install -y --no-install-recommends libcurl4 libjpeg62-turbo +RUN apt-get clean && rm -rf /var/lib/apt/lists/* /root/nut.zip + +# Run the server as an unprivileged user +RUN groupadd -g 1000 nut && useradd -u 1000 -g nut -M -d /root/nut nut +RUN chmod 711 /root +RUN mkdir -p /titles /root/nut/_NSPOUT && chown -R nut:nut /titles /root/nut + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod 755 /usr/local/bin/docker-entrypoint.sh VOLUME [ "/titles", "/root/nut/conf", "/root/nut/_NSPOUT" ] EXPOSE 9000 -# clean up -RUN rm v3.3.zip -RUN apt-get autoremove - +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] CMD ["python3", "/root/nut/nut.py", "--server"] diff --git a/README.md b/README.md index 7ae2e75..ea633e5 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,28 @@ Slim Docker image featuring embedded Nut server from [blawar/nut](https://github docker pull eskwisit/nut-server # Run image -docker run -d --name=nut-server -v /path/to/titles:/titles:rw eskwisit/nut-server +docker run -d --name=nut-server -e NUT_PASSWORD=change-me -v /path/to/titles:/titles:rw eskwisit/nut-server ``` +## Authentication + +Nut requires HTTP basic auth for every request. The container creates +`conf/users.conf` on first start from `NUT_USERNAME` (defaults to `nut`) and +`NUT_PASSWORD`. If `NUT_PASSWORD` is unset, a random password is generated and +printed once to the container logs: + +```bash +docker logs nut-server +``` + +Mount the conf volume or set both variables to keep the credentials stable +across container recreations. Nut sends credentials over plain HTTP, so expose +it on a trusted network or behind a TLS reverse proxy only. + +Local drive access is disabled (`"enableLocalDriveAccess": 0` in `conf/nut.conf`) +so clients can only browse the configured title paths. Enabling it lets any +authenticated client read arbitrary files from the container. + ## Volumes If you need to tune the configuration, you can mount titles, conf and NSPOUT volumes. diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 0000000..d82778d --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,45 @@ +#!/bin/sh +set -eu + +CONF_DIR=/root/nut/conf +USERS_CONF="${CONF_DIR}/users.conf" +NUT_CONF="${CONF_DIR}/nut.conf" + +mkdir -p "${CONF_DIR}" /root/nut/_NSPOUT /titles + +# A mounted, empty conf volume hides the bundled config, which makes Nut fall +# back to its defaults (scan of the whole working directory and local drive +# access enabled) +if [ ! -f "${NUT_CONF}" ]; then + cp /opt/nut-defaults/nut.conf "${NUT_CONF}" +fi + +# Without a users.conf, Nut creates an implicit guest/guest account, so the +# server is effectively unauthenticated +if [ ! -f "${USERS_CONF}" ]; then + NUT_USERNAME="${NUT_USERNAME:-nut}" + if [ -z "${NUT_PASSWORD:-}" ]; then + NUT_PASSWORD="$(tr -dc 'A-Za-z0-9' "${USERS_CONF}" + umask 022 +elif grep -qi '^guest|guest' "${USERS_CONF}"; then + echo "nut-server: WARNING ${USERS_CONF} still holds the default guest/guest account, replace it." +fi + +unset NUT_PASSWORD + +chmod 600 "${USERS_CONF}" 2>/dev/null || true + +if [ "$(id -u)" != '0' ]; then + exec "$@" +fi + +chown -R nut:nut /root/nut /titles 2>/dev/null || true + +exec gosu nut "$@"