mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-09-25 22:58:58 +03:00
Container security hardening (phase 4) (#24140)
* Support read-only rootfs with self-signed certs in /config/tls * Support read-only rootfs in s6 and pre-compile bytecode * Assert read-only rootfs support in CI * Document hardened read-only deployment * keep certsync's cert selection identical to nginx's * note the uid trade-off in user: mode * fail fast when EXTRA_GROUPS or a missing media volume meets read_only * keep nosuid and nodev on the /run tmpfs * support read_only in the default mode * don't take go2rtc down when the homekit file isn't writable * lead with the hardware consequence of switching to user: * refuse to write TLS material through a symlink as root * note that memryx writes models to the root filesystem * certsync watches whichever cert path nginx loaded
This commit is contained in:
committed by
Nicolas Mowen
parent
b99c87f272
commit
41bc1a5844
+124
-1
@@ -101,7 +101,7 @@ jobs:
|
||||
# directive user; stdout discarded because -t reopens the config's
|
||||
# /dev/stdout logs and the docker exec pipe is root-owned
|
||||
docker exec frigate /command/s6-setuidgid frigate bash -c '/usr/local/nginx/sbin/nginx -e stderr -t -c /tmp/nginx/conf/nginx.conf >/dev/null'
|
||||
docker exec frigate stat -c %a /etc/letsencrypt/live/frigate/privkey.pem | grep -qx 600
|
||||
docker exec frigate stat -c %a /config/tls/privkey.pem | grep -qx 600
|
||||
docker exec frigate stat -c %a /dev/shm/go2rtc.yaml | grep -qx 640
|
||||
- name: Assert services run as non-root
|
||||
run: |
|
||||
@@ -293,6 +293,129 @@ jobs:
|
||||
done
|
||||
if [ "$ok" -ne 1 ]; then echo "sentinel skip never logged"; docker logs frigate-puid; exit 1; fi
|
||||
docker rm -f frigate-puid
|
||||
- name: Assert read-only rootfs with --user works
|
||||
run: |
|
||||
mkdir -p /tmp/frigate-config-ro /tmp/frigate-media-ro
|
||||
printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config-ro/config.yml
|
||||
sudo chown -R 1000:1000 /tmp/frigate-config-ro /tmp/frigate-media-ro
|
||||
# /run must allow exec: S6_READ_ONLY_ROOT has s6 copy its service
|
||||
# scripts there and run them, and --tmpfs defaults to noexec
|
||||
docker run -d --name frigate-ro --shm-size 256m \
|
||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||
--user 1000:1000 \
|
||||
--security-opt no-new-privileges:true \
|
||||
-v /tmp/frigate-config-ro:/config \
|
||||
-v /tmp/frigate-media-ro:/media/frigate \
|
||||
${{ steps.setup.outputs.image-name }}-amd64
|
||||
up=0
|
||||
for i in $(seq 1 60); do
|
||||
docker exec frigate-ro curl -fs http://127.0.0.1:5000/api/version && up=1 && break
|
||||
sleep 5
|
||||
done
|
||||
if [ "$up" -ne 1 ]; then echo "read-only container never healthy"; docker logs frigate-ro; exit 1; fi
|
||||
# an if, not "! grep": bash exempts a negated command from set -e and
|
||||
# the assertion would never fail
|
||||
if docker logs frigate-ro 2>&1 | grep -i "read-only file system"; then
|
||||
echo "a service tried to write to the read-only rootfs"; exit 1
|
||||
fi
|
||||
# the self-signed cert has to land in /config, the only writable path
|
||||
docker exec frigate-ro test -f /config/tls/privkey.pem
|
||||
# and nginx must serve it, which is what proves the templated cert path
|
||||
docker exec frigate-ro curl -ksSI https://127.0.0.1:8971/ >/dev/null
|
||||
# logging must work via the s6-log fallback (no logutil-service as non-root)
|
||||
docker exec frigate-ro test -s /dev/shm/logs/frigate/current
|
||||
# runtime user can write recordings storage
|
||||
docker exec frigate-ro touch /media/frigate/.write-probe
|
||||
docker exec frigate-ro rm /media/frigate/.write-probe
|
||||
docker rm -f frigate-ro
|
||||
- name: Assert PUID with read-only fails fast with clear error
|
||||
run: |
|
||||
docker run -d --name frigate-ro-puid --shm-size 256m \
|
||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||
-e PUID=1500 -e PGID=1500 \
|
||||
-v /tmp/frigate-config-ro:/config \
|
||||
${{ steps.setup.outputs.image-name }}-amd64
|
||||
found=0
|
||||
for i in $(seq 1 12); do
|
||||
if docker logs frigate-ro-puid 2>&1 | grep -q "not compatible with read_only"; then found=1; break; fi
|
||||
sleep 5
|
||||
done
|
||||
if [ "$found" -ne 1 ]; then
|
||||
echo "no fail-fast error for PUID with a read-only rootfs"; docker logs frigate-ro-puid; exit 1
|
||||
fi
|
||||
docker rm -f frigate-ro-puid
|
||||
- name: Assert EXTRA_GROUPS with read-only fails fast with clear error
|
||||
run: |
|
||||
docker run -d --name frigate-ro-groups --shm-size 256m \
|
||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||
-e EXTRA_GROUPS=44 \
|
||||
-v /tmp/frigate-config-ro:/config \
|
||||
-v /tmp/frigate-media-ro:/media/frigate \
|
||||
${{ steps.setup.outputs.image-name }}-amd64
|
||||
found=0
|
||||
for i in $(seq 1 12); do
|
||||
if docker logs frigate-ro-groups 2>&1 | grep -q "EXTRA_GROUPS needs a writable /etc"; then found=1; break; fi
|
||||
sleep 5
|
||||
done
|
||||
if [ "$found" -ne 1 ]; then
|
||||
echo "no fail-fast error for EXTRA_GROUPS with a read-only rootfs"; docker logs frigate-ro-groups; exit 1
|
||||
fi
|
||||
docker rm -f frigate-ro-groups
|
||||
- name: Assert read-only rootfs in the default mode works
|
||||
run: |
|
||||
mkdir -p /tmp/frigate-config-rod /tmp/frigate-media-rod
|
||||
printf 'mqtt:\n enabled: false\ncameras: {}\n' > /tmp/frigate-config-rod/config.yml
|
||||
docker run -d --name frigate-rod --shm-size 256m \
|
||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||
--security-opt no-new-privileges:true \
|
||||
-v /tmp/frigate-config-rod:/config \
|
||||
-v /tmp/frigate-media-rod:/media/frigate \
|
||||
${{ steps.setup.outputs.image-name }}-amd64
|
||||
up=0
|
||||
for i in $(seq 1 60); do
|
||||
docker exec frigate-rod curl -fs http://127.0.0.1:5000/api/version && up=1 && break
|
||||
sleep 5
|
||||
done
|
||||
if [ "$up" -ne 1 ]; then echo "read-only default-mode container never healthy"; docker logs frigate-rod; exit 1; fi
|
||||
if docker logs frigate-rod 2>&1 | grep -i "read-only file system"; then
|
||||
echo "a service tried to write to the read-only rootfs"; exit 1
|
||||
fi
|
||||
# the point of this mode over docker's user:: the drop still happens
|
||||
# and go2rtc still gets its own separate user
|
||||
ps_out=$(docker exec frigate-rod ps -eo user=,comm=)
|
||||
echo "$ps_out"
|
||||
for svc in python3 nginx; do
|
||||
if echo "$ps_out" | grep -w "$svc" | grep -q '^root'; then
|
||||
echo "$svc is running as root"; exit 1
|
||||
fi
|
||||
done
|
||||
echo "$ps_out" | grep -w go2rtc | grep -q '^go2rtc'
|
||||
# the ownership sweep still ran and recorded itself in /config
|
||||
docker exec frigate-rod cat /config/.permissions_version | grep -qx "2:1000:1000"
|
||||
# setfacl under a read-only rootfs, which nothing else covers:
|
||||
# init-devices exits early under --user, so that path is never reached
|
||||
docker exec frigate-rod mknod /dev/apex_9 c 120 99
|
||||
docker exec frigate-rod /etc/s6-overlay/s6-rc.d/init-devices/run
|
||||
docker exec frigate-rod getfacl -p /dev/apex_9 | grep -q "user:frigate:rw-"
|
||||
docker rm -f frigate-rod
|
||||
- name: "Assert switching that install to user: still starts"
|
||||
run: |
|
||||
# the config dir above now holds a go2rtc-owned go2rtc_homekit.yml,
|
||||
# which user: keeps readable but not writable (no supplementary groups)
|
||||
docker run -d --name frigate-rod-user --shm-size 256m \
|
||||
--read-only --tmpfs /tmp:rw,size=1g --tmpfs /run:exec,nosuid,nodev,mode=0755 \
|
||||
--user 1000:1000 \
|
||||
-v /tmp/frigate-config-rod:/config \
|
||||
-v /tmp/frigate-media-rod:/media/frigate \
|
||||
${{ steps.setup.outputs.image-name }}-amd64
|
||||
up=0
|
||||
for i in $(seq 1 60); do
|
||||
docker exec frigate-rod-user curl -fs http://127.0.0.1:5000/api/version && up=1 && break
|
||||
sleep 5
|
||||
done
|
||||
if [ "$up" -ne 1 ]; then echo "container did not survive the switch to user:"; docker logs frigate-rod-user; exit 1; fi
|
||||
docker logs frigate-rod-user 2>&1 | grep -q "HomeKit pairing changes will not persist"
|
||||
docker rm -f frigate-rod-user
|
||||
- name: Teardown
|
||||
if: always()
|
||||
run: docker rm -f frigate || true
|
||||
|
||||
Reference in New Issue
Block a user