Chroot 'em? I Hardly Know 'em!
Recently, I've switched some of my home lab over to OpenBSD, a more traditional UNIX-like system focused on security. At around the same time, I became interested in hosting my own security camera setup using some spare webcams and an RPi 3B+.
This post goes over my ffmpeg script, including an overview of how one might automate chroot creation on OpenBSD.
Chroot?
OpenBSD provides a syscall called
chroot that allows a program to set a
directory as its new root directory. That directory becomes the new starting
point for all path names starting with /.
We'll use this to isolate a process, limiting the amount of damage it can do to the file system.
Keep in mind that a chroot is only filesystem isolation. Processes running in a chroot will still have access to the loopback interface (127.0.0.1) so could absolutely still cause a lot of damage.
So what's hard about running programs in a chroot?
At a basic level, programs expect stuff to be in certain places. In particular; other
programs, dynamically linked libraries, and other general unixy stuff like
/etc/localtime. We'll have to handle some of
this manually, but we can automate the process of collecting the required libraries.
The two scripts we'll be running in the chroot are included below
# security_cameras.sh
while true
do
ROOT_DIR=/recordings
BY_YEAR="$ROOT_DIR/$(date +'%Y')"
BY_MONTH="$BY_YEAR/$(date +'%Y-%m')"
BY_DAY="$BY_MONTH/$(date +'%Y-%m-%d')"
mkdir -p $BY_DAY
NOW="$BY_DAY/$(date +'%Y-%m-%d_%H-%M-%S')"
/usr/local/bin/security_cameras_record_segment.sh $NOW.mp4
done
# security_cameras_record_segment.sh
timeout -s INT -f 1h /usr/local/bin/ffmpeg \
-i 'http://10.0.0.191:1984/api/stream.mjpeg?src=front-yard' \
-vf "drawtext=fontfile=/FreeUniversal-Bold.ttf:text='%{localtime}':fontsize=32:box=1:boxcolor=black@0.8:fontcolor=white@0.8:x=7:y=7,select='gt(scene\,0.0007)',setpts=N/(25*TB)" \
-an "$1"
To be specific; we'd like it running in a tmux session, in a chroot, as a user other than root.
Building out a chroot
Because the binaries we'll be running will need to be updated when the server
is updated, we'll rework the security_cameras.sh to build out its own chroot
from the host system, then restart itself inside the chroot using the
chroot command. As the user inside
the chroot is non-root, we'll leverage that.
# security_cameras.sh
if [ $(whoami) = root ]; then
# Todo:
# - Set up the chroot directory
# - Restart the script inside the chroot
else
# Do the stuff
while true
do
ROOT_DIR=/recordings
BY_YEAR="$ROOT_DIR/$(date +'%Y')"
BY_MONTH="$BY_YEAR/$(date +'%Y-%m')"
BY_DAY="$BY_MONTH/$(date +'%Y-%m-%d')"
mkdir -p $BY_DAY
NOW="$BY_DAY/$(date +'%Y-%m-%d_%H-%M-%S')"
/usr/local/bin/security_cameras_record_segment.sh $NOW.mp4
done
fi
We'll create the new root directory in /var/security_cameras. As we'll be
storing our recordings in /var/security_cameras/recordings, we should be
careful not to delete that in our script. The following should build out the
basic directory structure, then copy our scripts and other files into it.
# security_cameras.sh
# ...
CHROOT_DIR=/var/security_cameras
mkdir -p $CHROOT_DIR/bin \
$CHROOT_DIR/usr/local/bin \
$CHROOT_DIR/recordings
cp /home/madelineb/Cvs/openbsd-server/misc/FreeUniversal-Bold.ttf $CHROOT_DIR/
mkdir -p $CHROOT_DIR/usr/local/bin
cp /usr/local/bin/security_cameras.sh \
/usr/local/bin/security_cameras_record_segment.sh \
$CHROOT_DIR/usr/local/bin
# ...
We'll also need the timezone so ffmpeg can get the right time for the timestamps.
cp /etc/localtime $CHROOT_DIR/etc/localtime
Being not root
Next we'll need to make sure we have a user for our script to run as. We can
create one in the script with the following, making sure it can access the
recordings directory.
# ...
useradd \
-s /sbin/nologin \
-d /nonexistent \
-c "Security camera daemon" \
_security_cameras
chown -R _security_cameras $CHROOT_DIR/recordings
chgrp -R _security_cameras $CHROOT_DIR/recordings
# ...
The fun bit?
Now we've got to round up all the runtime dependencies of the programs we'd like to include in the chroot. We can use ldd to check out the dynamically-linked libraries for a binary.
ldd $(which ffmpeg) | head -n 10
gives
/usr/local/bin/ffmpeg:
Start End Type Open Ref GrpRef Name
000005e92af39000 000005e92afa3000 exe 2 0 0 /usr/local/bin/ffmpeg
000005eb8509c000 000005eb850b4000 rlib 0 1 0 /usr/local/lib/libavdevice.so.15.1
000005ec1571d000 000005ec15c61000 rlib 0 2 0 /usr/local/lib/libavfilter.so.13.1
000005ebba036000 000005ebba309000 rlib 0 3 0 /usr/local/lib/libavformat.so.24.1
000005ec098b5000 000005ec0ae40000 rlib 0 4 0 /usr/local/lib/libavcodec.so.27.1
000005ec16616000 000005ec16639000 rlib 0 3 0 /usr/local/lib/libswresample.so.6.1
000005eb4cfef000 000005eb4d16d000 rlib 0 2 0 /usr/local/lib/libswscale.so.9.1
000005ebdae0c000 000005ebdbf14000 rlib 0 7 0 /usr/local/lib/libavutil.so.17.1
...
...
The column on the right-hand side shows us the shared objects needed to run ffmpeg.
We'll use awk to parse the output from ldd.
We'll need to skip the first two lines with NR>2, then we can grab the entries of
the 7th column with {print $7}.
# e.g.
ldd /usr/local/bin/ffmpeg | awk 'NR>2 {print $7}'
Using which to get the paths of the dependencies, we can put together a
loop to set everything up.
for PROG in ffmpeg timeout sh date mkdir whoami; do
echo $PROG
for DEP in $(ldd $(which $PROG) | awk 'NR>2 {print $7}'); do
COPY_TO=$(echo $DEP | cut -c 2-)
DIR=$(dirname $COPY_TO)
mkdir -p $CHROOT_DIR/$DIR
cp $DEP $CHROOT_DIR/$COPY_TO
done
done
The complete script
Putting this all together, we get the following script
#!/bin/sh
# /usr/local/bin/security_cameras.sh
# If we're the root user, set up the chroot then
# restart the script as the _security_cameras user inside the chroot
if [ $(whoami) = root ]; then
# Set up the chroot
CHROOT_DIR=/var/security_cameras
mkdir -p $CHROOT_DIR/bin \
$CHROOT_DIR/usr/local/bin \
$CHROOT_DIR/recordings
cp /home/madelineb/Cvs/madelineb/openbsd-server/misc/FreeUniversal-Bold.ttf $CHROOT_DIR/
mkdir -p $CHROOT_DIR/usr/local/bin
cp /usr/local/bin/security_cameras.sh \
/usr/local/bin/security_cameras_record_segment.sh \
$CHROOT_DIR/usr/local/bin
# Ensure ld.so.hints is correct
mkdir -p $CHROOT_DIR/var/run
cp /var/run/ld.so.hints $CHROOT_DIR/var/run
# Ensure the user exists
useradd \
-s /sbin/nologin \
-d /nonexistent \
-c "Security camera daemon" \
_security_cameras
chown -R _security_cameras $CHROOT_DIR/recordings
chgrp -R _security_cameras $CHROOT_DIR/recordings
# Set up the programs in the chroot
for PROG in ffmpeg timeout sh date mkdir whoami; do
echo $PROG
for DEP in $(ldd $(which $PROG) | awk 'NR>2 {print $7}'); do
COPY_TO=$(echo $DEP | cut -c 2-)
DIR=$(dirname $COPY_TO)
mkdir -p $CHROOT_DIR/$DIR
cp $DEP $CHROOT_DIR/$COPY_TO
done
done
# Restart the script inside the chroot as the '_security_cameras' user
tmux new-session -d -s "Security Cameras" \
chroot -u _security_cameras /var/security_cameras \
/usr/local/bin/security_cameras.sh
else
while true
do
ROOT_DIR=/recordings
BY_YEAR="$ROOT_DIR/$(date +'%Y')"
BY_MONTH="$BY_YEAR/$(date +'%Y-%m')"
BY_DAY="$BY_MONTH/$(date +'%Y-%m-%d')"
mkdir -p $BY_DAY
NOW="$BY_DAY/$(date +'%Y-%m-%d_%H-%M-%S')"
/usr/local/bin/security_cameras_record_segment.sh $NOW.mp4
done
fi