<

Chroot 'em? I Hardly Know 'em!

2026-06-11

Probs shouldn't be running ffmpeg as root...

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
:D