diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41080be5d9..398bc9e09f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,61 @@ jobs: tags: ${{ steps.setup.outputs.image-name }}-amd64 cache-from: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64 cache-to: type=registry,ref=${{ steps.setup.outputs.cache-name }}-amd64,mode=max + smoke_test: + runs-on: ubuntu-22.04 + name: AMD64 Smoke Test + needs: + - amd64_build + steps: + - name: Check out code + uses: actions/checkout@v6 + with: + persist-credentials: false + - name: Set up QEMU and Buildx + id: setup + uses: ./.github/actions/setup + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Start container + run: | + mkdir -p /tmp/frigate-config + printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config/config.yml + docker run -d --name frigate --shm-size 256m \ + -v /tmp/frigate-config:/config \ + -p 5000:5000 -p 8971:8971 \ + ${{ steps.setup.outputs.image-name }}-amd64 + - name: Wait for API + run: | + for i in $(seq 1 60); do + curl -fs http://127.0.0.1:5000/api/version && exit 0 + sleep 5 + done + echo "API never came up"; docker logs frigate; exit 1 + - name: Assert security headers and permissions + run: | + headers=$(curl -ksI https://127.0.0.1:8971/) + echo "$headers" + echo "$headers" | grep -qi "x-content-type-options: nosniff" + echo "$headers" | grep -qi "referrer-policy: strict-origin-when-cross-origin" + # server_tokens off: Server header must not include a version. + # written as an if rather than "! grep", because bash exempts a + # negated command from set -e and the assertion would never fail + if echo "$headers" | grep -qiE "^server: nginx/[0-9]"; then + echo "Server header leaks the nginx version; server_tokens is not off" + exit 1 + fi + # Frigate never ships frame-ancestors: HA's Webpage card and iframe + # panels frame it cross-origin and it would break them silently + if echo "$headers" | grep -qi "frame-ancestors"; then + echo "response carries frame-ancestors, which breaks cross-origin iframe embedding" + exit 1 + fi + docker exec frigate /usr/local/nginx/sbin/nginx -t + docker exec frigate stat -c %a /etc/letsencrypt/live/frigate/privkey.pem | grep -qx 600 + docker exec frigate stat -c %a /dev/shm/go2rtc.yaml | grep -qx 640 + - name: Teardown + if: always() + run: docker rm -f frigate || true arm64_build: runs-on: ubuntu-22.04-arm name: ARM Build diff --git a/docker/main/Dockerfile b/docker/main/Dockerfile index bf17146e30..095eec91af 100644 --- a/docker/main/Dockerfile +++ b/docker/main/Dockerfile @@ -60,10 +60,10 @@ ARG DEBIAN_FRONTEND RUN --mount=type=bind,source=docker/main/build_intel_media_driver.sh,target=/deps/build_intel_media_driver.sh \ /deps/build_intel_media_driver.sh -FROM scratch AS go2rtc +FROM wget AS go2rtc ARG TARGETARCH -WORKDIR /rootfs/usr/local/go2rtc/bin -ADD --link --chmod=755 "https://github.com/AlexxIT/go2rtc/releases/download/v1.9.14/go2rtc_linux_${TARGETARCH}" go2rtc +RUN --mount=type=bind,source=docker/main/install_go2rtc.sh,target=/deps/install_go2rtc.sh \ + /deps/install_go2rtc.sh FROM wget AS tempio ARG TARGETARCH diff --git a/docker/main/install_deps.sh b/docker/main/install_deps.sh index e197ce1b68..b66d914f00 100755 --- a/docker/main/install_deps.sh +++ b/docker/main/install_deps.sh @@ -28,7 +28,13 @@ update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 mkdir -p -m 600 /root/.gnupg # install coral runtime +# sha256 digests of the release debs; update when bumping the libedgetpu release. +declare -A edgetpu_checksums=( + ["amd64"]="63fd00989d29160fa9894e115156a9abe456e88751fc9be89d26e4696200441b" + ["arm64"]="eab8aa4576b4dbf738135d8094f32270b24117f77147d25cbe0f49d0144d85f2" +) wget -q -O /tmp/libedgetpu1-max.deb "https://github.com/feranick/libedgetpu/releases/download/16.0TF2.17.1-1/libedgetpu1-max_16.0tf2.17.1-1.bookworm_${TARGETARCH}.deb" +echo "${edgetpu_checksums[${TARGETARCH}]} /tmp/libedgetpu1-max.deb" | sha256sum -c - unset DEBIAN_FRONTEND yes | dpkg -i /tmp/libedgetpu1-max.deb && export DEBIAN_FRONTEND=noninteractive rm /tmp/libedgetpu1-max.deb @@ -45,36 +51,41 @@ if [[ "${TARGETARCH}" == "arm64" ]]; then fi fi +# sha256 digests of the ffmpeg builds, keyed "-". +# Upstream publishes no checksums; these come from a one-time fetch and guard +# against later substitution. Update when bumping a build URL. +declare -A ffmpeg_checksums=( + ["5.0-amd64"]="377abec133f9d9e8014dee1b91c9684ac8bb0b5b7d80100a57116ff837c4c0d4" + ["7.0-amd64"]="e13860eb90409c8218319c928067834ce450128e86f24cfed5cfe91ce6e31037" + ["8.0-amd64"]="9bac85054d351cdc89c0a4f45c8ea5c44df94009aabd964b719bbadd56aedae9" + ["5.0-arm64"]="57ee475407bad49910ba9b946428396e30cf075ea28a7912fbe1aa2578085af0" + ["7.0-arm64"]="16c8b04e9d0ea9c769ad964c4c453fcf05121a1947237329d2e9d8a5e43e2a3c" + ["8.0-arm64"]="cd91948468d0f11ce795a2cdaa0c69911bd1db313b49bb19c22512beb88cde69" +) + +# the tarballs nest their binaries under a directory named for the arch, which +# matches TARGETARCH for both builds we consume +install_ffmpeg() { + local dir="$1" url="$2" + mkdir -p "/usr/lib/ffmpeg/${dir}" + wget -qO ffmpeg.tar.xz "${url}" + echo "${ffmpeg_checksums[${dir}-${TARGETARCH}]} ffmpeg.tar.xz" | sha256sum -c - + tar -xf ffmpeg.tar.xz -C "/usr/lib/ffmpeg/${dir}" --strip-components 1 "${TARGETARCH}/bin/ffmpeg" "${TARGETARCH}/bin/ffprobe" + rm -f ffmpeg.tar.xz +} + # ffmpeg -> amd64 if [[ "${TARGETARCH}" == "amd64" ]]; then - mkdir -p /usr/lib/ffmpeg/5.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux64-gpl-5.1.tar.xz" - tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe - rm -rf ffmpeg.tar.xz - mkdir -p /usr/lib/ffmpeg/7.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz" - tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe - rm -rf ffmpeg.tar.xz - mkdir -p /usr/lib/ffmpeg/8.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linux64-gpl-8.1.tar.xz" - tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 amd64/bin/ffmpeg amd64/bin/ffprobe - rm -rf ffmpeg.tar.xz + install_ffmpeg 5.0 "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linux64-gpl-5.1.tar.xz" + install_ffmpeg 7.0 "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linux64-gpl-7.0.tar.xz" + install_ffmpeg 8.0 "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linux64-gpl-8.1.tar.xz" fi # ffmpeg -> arm64 if [[ "${TARGETARCH}" == "arm64" ]]; then - mkdir -p /usr/lib/ffmpeg/5.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linuxarm64-gpl-5.1.tar.xz" - tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/5.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe - rm -f ffmpeg.tar.xz - mkdir -p /usr/lib/ffmpeg/7.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz" - tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/7.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe - rm -f ffmpeg.tar.xz - mkdir -p /usr/lib/ffmpeg/8.0 - wget -qO ffmpeg.tar.xz "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linuxarm64-gpl-8.1.tar.xz" - tar -xf ffmpeg.tar.xz -C /usr/lib/ffmpeg/8.0 --strip-components 1 arm64/bin/ffmpeg arm64/bin/ffprobe - rm -f ffmpeg.tar.xz + install_ffmpeg 5.0 "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2022-07-31-12-37/ffmpeg-n5.1-2-g915ef932a3-linuxarm64-gpl-5.1.tar.xz" + install_ffmpeg 7.0 "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2024-09-19-12-51/ffmpeg-n7.0.2-18-g3e6cec1286-linuxarm64-gpl-7.0.tar.xz" + install_ffmpeg 8.0 "https://github.com/NickM-27/FFmpeg-Builds/releases/download/autobuild-2026-06-02-14-20/ffmpeg-n8.1.1-9-g58d4114d36-linuxarm64-gpl-8.1.tar.xz" fi # arch specific packages @@ -120,27 +131,56 @@ if [[ "${TARGETARCH}" == "amd64" ]]; then apt-get -qq install -y libtbb12 # install legacy and standard intel compute packages + # sha256 digests of the driver debs, taken from the ww.sum asset + # compute-runtime ships per release and the checksum.sha256 on npu-driver + # v1.19.0; intel-graphics-compiler and level-zero publish none, so those + # five are hash-what-you-get. Refresh after a version bump with + # `curl -sL | sha256sum`, cross-checking upstream's sum where the + # release still has one. npu-driver stopped publishing them after v1.19.0. + declare -A intel_checksums=( + ["libigdgmm12_22.9.0_amd64.deb"]="9d712f71c18baee076de9961dda71e8089291e1bd0deb5d649ab5ba5de114f97" + ["intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb"]="bbe71e4f414259e06a10cde72c29a2bd78d41b2bb2f6f8463b1806797fe66e85" + ["intel-level-zero-gpu-legacy1_1.5.30872.36_amd64.deb"]="40dfbd15ab62de036a00824b304a2aa1fa2d81ad60ef83da09cfe3c5a80c429f" + ["intel-igc-opencl_1.0.17537.24_amd64.deb"]="dd016400f87fa2b6a9fa9fbcca7eb4a2629174a29de679709f9bec5cede88b0e" + ["intel-igc-core_1.0.17537.24_amd64.deb"]="c1e1ecdfe2064c047c552651cfdcdafc504f2033afafba65654338b880048b67" + ["intel-opencl-icd_26.14.37833.4-0_amd64.deb"]="2e15eeb4fe9c1bba467a655967373eec6a20dd04cc7159de53c359f17ab53e41" + ["libze-intel-gpu1_26.14.37833.4-0_amd64.deb"]="34ce5791160d87ce6d54edb558a4030858ee1dad2afb067b9c5c58d4cde774c6" + ["intel-igc-opencl-2_2.32.7+21184_amd64.deb"]="3c9bddbfe558279402bbeaabcf9c63b8de46b956b0ad9625415fd35dda53ad52" + ["intel-igc-core-2_2.32.7+21184_amd64.deb"]="64e5230788e3a31e611e8d815a141b1facb91e5f0ef239233ef3f0614bfe3fd6" + ["level-zero_1.28.2+u22.04_amd64.deb"]="9015a579abef960166f8e943858d5c81fd4199a960f07260c1da66038257effb" + ["intel-driver-compiler-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb"]="8087bfcc0872d7976d0163203c7c783a4176f813c473766587e86c7b34135dff" + ["intel-fw-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb"]="740219c03495f8812c03ab74baf8199acf17d13929001105418d4ba226ba2290" + ["intel-level-zero-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb"]="f4f5eb97aa7da52c7fec97e4ddfb43aae01703bbadc767bae1f2d4faf342ba42" + ) + + fetch_intel_deb() { + local url="$1" name + name=$(basename "$url") + wget -q "$url" + echo "${intel_checksums[${name}]} ${name}" | sha256sum -c - + } + # see https://github.com/intel/compute-runtime/blob/master/LEGACY_PLATFORMS.md for more info # needed core package - wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb + fetch_intel_deb https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libigdgmm12_22.9.0_amd64.deb dpkg -i libigdgmm12_22.9.0_amd64.deb rm libigdgmm12_22.9.0_amd64.deb # legacy compute-runtime packages - wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb - wget https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-level-zero-gpu-legacy1_1.5.30872.36_amd64.deb - wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb - wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb + fetch_intel_deb https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb + fetch_intel_deb https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-level-zero-gpu-legacy1_1.5.30872.36_amd64.deb + fetch_intel_deb https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb + fetch_intel_deb https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb # standard compute-runtime packages - wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb - wget https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libze-intel-gpu1_26.14.37833.4-0_amd64.deb - wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb - wget https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb + fetch_intel_deb https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/intel-opencl-icd_26.14.37833.4-0_amd64.deb + fetch_intel_deb https://github.com/intel/compute-runtime/releases/download/26.14.37833.4/libze-intel-gpu1_26.14.37833.4-0_amd64.deb + fetch_intel_deb https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-opencl-2_2.32.7+21184_amd64.deb + fetch_intel_deb https://github.com/intel/intel-graphics-compiler/releases/download/v2.32.7/intel-igc-core-2_2.32.7+21184_amd64.deb # npu packages - wget https://github.com/oneapi-src/level-zero/releases/download/v1.28.2/level-zero_1.28.2+u22.04_amd64.deb - wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-driver-compiler-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb - wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-fw-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb - wget https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-level-zero-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb + fetch_intel_deb https://github.com/oneapi-src/level-zero/releases/download/v1.28.2/level-zero_1.28.2+u22.04_amd64.deb + fetch_intel_deb https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-driver-compiler-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb + fetch_intel_deb https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-fw-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb + fetch_intel_deb https://github.com/intel/linux-npu-driver/releases/download/v1.19.0/intel-level-zero-npu_1.19.0.20250707-16111289554_ubuntu22.04_amd64.deb dpkg -i *.deb rm *.deb diff --git a/docker/main/install_go2rtc.sh b/docker/main/install_go2rtc.sh new file mode 100755 index 0000000000..0912d43f0a --- /dev/null +++ b/docker/main/install_go2rtc.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +set -euxo pipefail + +go2rtc_version="1.9.14" + +# sha256 digests of the release binaries; update when bumping go2rtc_version. +declare -A go2rtc_checksums=( + ["amd64"]="32d616af226bd731678ffde328b94cfb94e30339bfefc469cfb76323144615a6" + ["arm64"]="359fabade8a7a51e81a55fe6df6b0ef81764a5e1d63179577534eaaa71904b50" +) + +dest_dir="/rootfs/usr/local/go2rtc/bin" +mkdir -p "${dest_dir}" + +wget -qO "${dest_dir}/go2rtc" \ + "https://github.com/AlexxIT/go2rtc/releases/download/v${go2rtc_version}/go2rtc_linux_${TARGETARCH}" +echo "${go2rtc_checksums[${TARGETARCH}]} ${dest_dir}/go2rtc" | sha256sum -c - +chmod 755 "${dest_dir}/go2rtc" diff --git a/docker/main/install_hailort.sh b/docker/main/install_hailort.sh index 2e568a14ed..efbc5fdbdc 100755 --- a/docker/main/install_hailort.sh +++ b/docker/main/install_hailort.sh @@ -4,11 +4,29 @@ set -euxo pipefail hailo_version="4.21.0" +# sha256 digests of the release artifacts; update when bumping hailo_version. +# The runtime tarball is keyed by TARGETARCH, the wheel by the python arch tag. +declare -A hailort_checksums=( + ["amd64"]="0a57ac5f7cc8c2c3668133189d9285b55f498e8cb219797e203f6f5015fec4b3" + ["arm64"]="dd840548eb5d0d147c99aee2cb013d39d64be09c5bc63061171fcfacf4547b3f" + ["x86_64"]="8112a973ab48095399b29d883f31987828df5861b8553f614c89f098a67b3fb6" + ["aarch64"]="658432a43573280d472f6402d7934669effe7f163ba3dffa31c50bbeeaa7c01d" +) + if [[ "${TARGETARCH}" == "amd64" ]]; then arch="x86_64" elif [[ "${TARGETARCH}" == "arm64" ]]; then arch="aarch64" fi -wget -qO- "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" | tar -C / -xzf - -wget -P /wheels/ "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" +# downloaded rather than streamed into tar because streaming and verifying the +# digest before extraction are mutually exclusive +wget -qO /tmp/hailort.tar.gz "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-debian12-${TARGETARCH}.tar.gz" +echo "${hailort_checksums[${TARGETARCH}]} /tmp/hailort.tar.gz" | sha256sum -c - +tar -C / -xzf /tmp/hailort.tar.gz +rm -f /tmp/hailort.tar.gz + +wheel="/wheels/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" +mkdir -p /wheels +wget -qO "${wheel}" "https://github.com/frigate-nvr/hailort/releases/download/v${hailo_version}/hailort-${hailo_version}-cp311-cp311-linux_${arch}.whl" +echo "${hailort_checksums[${arch}]} ${wheel}" | sha256sum -c - diff --git a/docker/main/install_s6_overlay.sh b/docker/main/install_s6_overlay.sh index 3ea387c9b2..34bbc465df 100755 --- a/docker/main/install_s6_overlay.sh +++ b/docker/main/install_s6_overlay.sh @@ -4,6 +4,15 @@ set -euxo pipefail s6_version="3.2.1.0" +# sha256 digests of the release artifacts, from the .sha256 files published at +# https://github.com/just-containers/s6-overlay/releases/tag/v3.2.1.0 +# Update these when bumping s6_version. +declare -A s6_checksums=( + ["noarch"]="42e038a9a00fc0fef70bf0bc42f625a9c14f8ecdfe77d4ad93281edf717e10c5" + ["x86_64"]="8bcbc2cada58426f976b159dcc4e06cbb1454d5f39252b3bb0c778ccf71c9435" + ["aarch64"]="c8fd6b1f0380d399422fc986a1e6799f6a287e2cfa24813ad0b6a4fb4fa755cc" +) + if [[ "${TARGETARCH}" == "amd64" ]]; then s6_arch="x86_64" elif [[ "${TARGETARCH}" == "arm64" ]]; then @@ -12,8 +21,15 @@ fi mkdir -p /rootfs/ -wget -qO- "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-noarch.tar.xz" | - tar -C /rootfs/ -Jxpf - +download_and_extract() { + local arch="$1" + local tarball="/tmp/s6-overlay-${arch}.tar.xz" + wget -qO "${tarball}" \ + "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-${arch}.tar.xz" + echo "${s6_checksums[${arch}]} ${tarball}" | sha256sum -c - + tar -C /rootfs/ -Jxpf "${tarball}" + rm -f "${tarball}" +} -wget -qO- "https://github.com/just-containers/s6-overlay/releases/download/v${s6_version}/s6-overlay-${s6_arch}.tar.xz" | - tar -C /rootfs/ -Jxpf - +download_and_extract "noarch" +download_and_extract "${s6_arch}" diff --git a/docker/main/install_tempio.sh b/docker/main/install_tempio.sh index 743a122889..468afad4a3 100755 --- a/docker/main/install_tempio.sh +++ b/docker/main/install_tempio.sh @@ -4,6 +4,14 @@ set -euxo pipefail tempio_version="2021.09.0" +# sha256 digests of the release binaries; update when bumping tempio_version. +# Upstream publishes no checksums, so these come from a one-time fetch and +# guard against later substitution rather than the original download. +declare -A tempio_checksums=( + ["amd64"]="b7b93ebfd24c1161cec7aecfad62ab51f2241149358cef354b86cdbc6a60546f" + ["aarch64"]="3a5c32981ba68b75ed9b28497429e5a5cecbeb74c3b821b035a48b37609bb895" +) + if [[ "${TARGETARCH}" == "amd64" ]]; then arch="amd64" elif [[ "${TARGETARCH}" == "arm64" ]]; then @@ -13,4 +21,5 @@ fi mkdir -p /rootfs/usr/local/tempio/bin wget -q -O /rootfs/usr/local/tempio/bin/tempio "https://github.com/home-assistant/tempio/releases/download/${tempio_version}/tempio_${arch}" +echo "${tempio_checksums[${arch}]} /rootfs/usr/local/tempio/bin/tempio" | sha256sum -c - chmod 755 /rootfs/usr/local/tempio/bin/tempio diff --git a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run index a3c7b32484..9a86e7c879 100755 --- a/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run +++ b/docker/main/rootfs/etc/s6-overlay/s6-rc.d/nginx/run @@ -77,15 +77,20 @@ if [ ! \( -f "$letsencrypt_path/privkey.pem" -a -f "$letsencrypt_path/fullchain. openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 \ -subj "/O=FRIGATE DEFAULT CERT/CN=*" \ -keyout "$letsencrypt_path/privkey.pem" -out "$letsencrypt_path/fullchain.pem" 2>/dev/null + chmod 600 "$letsencrypt_path/privkey.pem" + chmod 644 "$letsencrypt_path/fullchain.pem" fi +# nginx settings are read once; both templates consume them +nginx_settings=$(python3 /usr/local/nginx/get_nginx_settings.py) + # build templates for optional FRIGATE_BASE_PATH environment variable -python3 /usr/local/nginx/get_nginx_settings.py | \ +echo "$nginx_settings" | \ tempio -template /usr/local/nginx/templates/base_path.gotmpl \ -out /usr/local/nginx/conf/base_path.conf # build templates for additional network settings -python3 /usr/local/nginx/get_nginx_settings.py | \ +echo "$nginx_settings" | \ tempio -template /usr/local/nginx/templates/listen.gotmpl \ -out /usr/local/nginx/conf/listen.conf diff --git a/docker/main/rootfs/usr/local/go2rtc/create_config.py b/docker/main/rootfs/usr/local/go2rtc/create_config.py index 0dee057ff5..7e6ff8ce78 100644 --- a/docker/main/rootfs/usr/local/go2rtc/create_config.py +++ b/docker/main/rootfs/usr/local/go2rtc/create_config.py @@ -189,3 +189,6 @@ if config.get("birdseye", {}).get("restream", False): # Write go2rtc_config to /dev/shm/go2rtc.yaml with open("/dev/shm/go2rtc.yaml", "w") as f: yaml.dump(go2rtc_config, f) + +# config contains camera credentials; do not leave it world-readable +os.chmod("/dev/shm/go2rtc.yaml", 0o640) diff --git a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf index 262828f372..f177ffd88a 100644 --- a/docker/main/rootfs/usr/local/nginx/conf/nginx.conf +++ b/docker/main/rootfs/usr/local/nginx/conf/nginx.conf @@ -11,6 +11,7 @@ events { http { map_hash_bucket_size 256; + server_tokens off; include mime.types; default_type application/octet-stream; @@ -62,6 +63,7 @@ http { server { include listen.conf; + include security_headers.conf; # enable HTTP/2 for TLS connections to eliminate browser 6-connection limit http2 on; @@ -123,6 +125,7 @@ http { secure_token $args; secure_token_types application/vnd.apple.mpegurl; + include security_headers.conf; add_header Cache-Control "no-store"; expires off; @@ -139,6 +142,7 @@ http { location /stream/ { include auth_request.conf; + include security_headers.conf; add_header Cache-Control "no-store"; expires off; @@ -160,6 +164,7 @@ http { } expires 7d; + include security_headers.conf; add_header Cache-Control "public"; autoindex on; root /media/frigate; @@ -252,6 +257,7 @@ http { location /api/ { include auth_request.conf; + include security_headers.conf; add_header Cache-Control "no-store"; expires off; proxy_pass http://frigate_api/; @@ -318,29 +324,34 @@ http { location / { # do not require auth for static assets + include security_headers.conf; add_header Cache-Control "no-store"; expires off; location /assets/ { access_log off; expires 1y; + include security_headers.conf; add_header Cache-Control "public"; } location /fonts/ { access_log off; expires 1y; + include security_headers.conf; add_header Cache-Control "public"; } location /locales/ { access_log off; + include security_headers.conf; add_header Cache-Control "public"; } location ~ ^/.*-([A-Za-z0-9]+)\.webmanifest$ { access_log off; expires 1y; + include security_headers.conf; add_header Cache-Control "public"; default_type application/json; proxy_set_header Accept-Encoding ""; diff --git a/docker/main/rootfs/usr/local/nginx/conf/security_headers.conf b/docker/main/rootfs/usr/local/nginx/conf/security_headers.conf new file mode 100644 index 0000000000..f210d789b6 --- /dev/null +++ b/docker/main/rootfs/usr/local/nginx/conf/security_headers.conf @@ -0,0 +1,5 @@ +# Deliberately no X-Frame-Options or CSP frame-ancestors: HA's Webpage card and +# iframe panels frame Frigate cross-origin, and either would break them +# silently. Bind-mount this file to add your own. +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; diff --git a/docs/docs/configuration/hardware_acceleration_video.md b/docs/docs/configuration/hardware_acceleration_video.md index b9312c327f..26276f02ba 100644 --- a/docs/docs/configuration/hardware_acceleration_video.md +++ b/docs/docs/configuration/hardware_acceleration_video.md @@ -312,8 +312,9 @@ ffmpeg: :::note -If running Frigate through Docker, you either need to run in privileged mode or -map the `/dev/video*` devices to Frigate. With Docker Compose add: +If running Frigate through Docker, map the relevant `/dev/video*` devices into +the container. Running in privileged mode also works but grants far more access +than needed. With Docker Compose add: ```yaml {4-5} services: diff --git a/docs/docs/frigate/installation.md b/docs/docs/frigate/installation.md index 3e1b4869c2..6105981f94 100644 --- a/docs/docs/frigate/installation.md +++ b/docs/docs/frigate/installation.md @@ -514,7 +514,7 @@ Generate a Frigate Docker Compose configuration based on your hardware and requi services: frigate: container_name: frigate - privileged: true # this may not be necessary for all setups + # privileged: true # ONLY enable if your hardware requires it (see hardware-specific docs); prefer the device mappings below restart: unless-stopped stop_grace_period: 30s # allow enough time to shut down the various services image: ghcr.io/blakeblackshear/frigate:stable @@ -546,6 +546,33 @@ services: +### Recommended security options + +Frigate does not need elevated container privileges for most setups. The +following hardens the container; add the `devices`/`group_add` entries your +hardware requires (see the hardware acceleration docs): + +```yaml +services: + frigate: + ... + security_opt: + - no-new-privileges:true + cap_drop: + - ALL +``` + +:::note + +`telemetry.stats.network_bandwidth` uses nethogs, which requires root with +NET_ADMIN/NET_RAW capabilities. If you enable that stat, omit `cap_drop: [ALL]` +or add `cap_add: [NET_ADMIN, NET_RAW]`. + +Platforms that genuinely require `privileged: true` (MemryX, some QNAP setups) +are called out in their own sections and are unaffected by this guidance. + +::: + **Docker CLI** If you can't use Docker Compose, you can run the container with something similar to this: diff --git a/docs/src/components/DockerComposeGenerator/config/config.yaml b/docs/src/components/DockerComposeGenerator/config/config.yaml index 42199ffca1..b06e0342f9 100644 --- a/docs/src/components/DockerComposeGenerator/config/config.yaml +++ b/docs/src/components/DockerComposeGenerator/config/config.yaml @@ -219,6 +219,8 @@ hardware: - host: "/run/mxa_manager" container: "/run/mxa_manager" comment: "MemryX manager" + privileged: true + privilegedReason: "required by MemryX to reach the max-manager" - id: "axera" label: "AXERA Accelerator" diff --git a/docs/src/components/DockerComposeGenerator/config/types.ts b/docs/src/components/DockerComposeGenerator/config/types.ts index 87bcb608dd..d45691d623 100644 --- a/docs/src/components/DockerComposeGenerator/config/types.ts +++ b/docs/src/components/DockerComposeGenerator/config/types.ts @@ -104,6 +104,10 @@ export interface DeviceConfig { extraHosts?: string[]; /** Security options, e.g. ["apparmor=unconfined"] */ securityOpt?: string[]; + /** Set only when this device type cannot work without full privileged mode */ + privileged?: boolean; + /** Why privileged mode is required, rendered as an inline comment */ + privilegedReason?: string; /** Whether this device type needs the NVIDIA GPU config UI */ needsNvidiaConfig?: boolean; } @@ -127,6 +131,10 @@ export interface HardwareOption { volumes?: VolumeMapping[]; /** Extra environment variables */ env?: Record; + /** Set only when this hardware cannot work without full privileged mode */ + privileged?: boolean; + /** Why privileged mode is required, rendered as an inline comment */ + privilegedReason?: string; } /** Port definition */ diff --git a/docs/src/components/DockerComposeGenerator/generator/index.ts b/docs/src/components/DockerComposeGenerator/generator/index.ts index f6091f7c01..d94d792558 100644 --- a/docs/src/components/DockerComposeGenerator/generator/index.ts +++ b/docs/src/components/DockerComposeGenerator/generator/index.ts @@ -1,6 +1,7 @@ import type { DeviceConfig, DeviceMapping, + HardwareOption, VolumeMapping, } from "../config/types"; import { hardwareMap } from "../config"; @@ -194,13 +195,32 @@ function buildExtraHosts(device: DeviceConfig): string[] { } function buildSecurityOpt(device: DeviceConfig): string[] { - if (!device.securityOpt?.length) return []; + // no-new-privileges is the baseline for every setup; device-specific entries + // are appended so only one security_opt key is ever emitted return [ " security_opt:", - ...device.securityOpt.map((s) => ` - ${s}`), + " - no-new-privileges:true", + ...(device.securityOpt ?? []).map((s) => ` - ${s}`), ]; } +/** + * Emit privileged mode only for hardware that genuinely cannot work without it. + * Everything else gets device mappings, which grant far less access. + */ +function buildPrivileged( + device: DeviceConfig, + selectedHardware: HardwareOption[] +): string[] { + const requiring = [device, ...selectedHardware].filter((c) => c.privileged); + if (!requiring.length) return []; + const reasons = requiring + .map((c) => c.privilegedReason) + .filter((r): r is string => Boolean(r)); + const comment = reasons.length ? ` # ${reasons.join("; ")}` : ""; + return [` privileged: true${comment}`]; +} + // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- @@ -217,11 +237,14 @@ export function generateDockerCompose(input: GeneratorInput): string { const hwVolumes: VolumeMapping[] = []; const hwEnv: Record = {}; + const selectedHw: HardwareOption[] = []; + for (const hwId of input.selectedHardware) { const hw = hardwareMap.get(hwId); if (!hw) continue; // Skip GPU device mapping for tensorrt images (it uses deploy instead) if (hw.id === "gpu" && device.imageTag === "stable-tensorrt") continue; + selectedHw.push(hw); hwDevices.push(...(hw.devices ?? [])); hwVolumes.push(...(hw.volumes ?? [])); Object.assign(hwEnv, hw.env ?? {}); @@ -231,7 +254,7 @@ export function generateDockerCompose(input: GeneratorInput): string { "services:", " frigate:", " container_name: frigate", - " privileged: true # This may not be necessary for all setups", + ...buildPrivileged(device, selectedHw), " restart: unless-stopped", " stop_grace_period: 30s # Allow enough time to shut down the various services", ...buildImage(device), diff --git a/frigate/api/auth.py b/frigate/api/auth.py index a1997c02e2..6d6ff30d4e 100644 --- a/frigate/api/auth.py +++ b/frigate/api/auth.py @@ -859,9 +859,12 @@ def login(request: Request, body: AppPostLoginBody): user = body.user password = body.password + remote_addr = get_remote_addr(request) + try: db_user: User = User.get_by_id(user) except DoesNotExist: + logger.warning(f"Login failed for unknown user '{user}' from {remote_addr}") return JSONResponse(content={"message": "Login failed"}, status_code=401) password_hash = db_user.password_hash @@ -889,6 +892,10 @@ def login(request: Request, body: AppPostLoginBody): request.app.frigate_config.auth.admin_first_time_login = False return response + + logger.warning( + f"Login failed for user '{user}' (invalid password) from {remote_addr}" + ) return JSONResponse(content={"message": "Login failed"}, status_code=401) diff --git a/frigate/test/http_api/test_http_auth.py b/frigate/test/http_api/test_http_auth.py new file mode 100644 index 0000000000..3a3a986ced --- /dev/null +++ b/frigate/test/http_api/test_http_auth.py @@ -0,0 +1,45 @@ +"""Tests for authentication endpoints.""" + +import os +from unittest.mock import patch + +from frigate.api.auth import hash_password +from frigate.const import JWT_SECRET_ENV_VAR +from frigate.models import User +from frigate.test.http_api.base_http_test import AuthTestClient, BaseTestHttp + + +@patch.dict(os.environ, {JWT_SECRET_ENV_VAR: "test-secret"}) +class TestHttpAuth(BaseTestHttp): + def setUp(self): + super().setUp([User]) + self.app = super().create_app() + + def tearDown(self): + User.delete().execute() + super().tearDown() + + def test_login_unknown_user_logs_warning(self): + with self.assertLogs("frigate.api.auth", level="WARNING") as logs: + with AuthTestClient(self.app) as client: + response = client.post( + "/login", json={"user": "ghost", "password": "irrelevant"} + ) + assert response.status_code == 401 + assert any("Login failed" in m and "ghost" in m for m in logs.output) + + def test_login_bad_password_logs_warning(self): + password_hash = hash_password("correct-horse-battery", iterations=1000) + User.insert( + username="admin", + password_hash=password_hash, + role="admin", + notification_tokens=[], + ).execute() + with self.assertLogs("frigate.api.auth", level="WARNING") as logs: + with AuthTestClient(self.app) as client: + response = client.post( + "/login", json={"user": "admin", "password": "wrong"} + ) + assert response.status_code == 401 + assert any("Login failed" in m and "admin" in m for m in logs.output)