--- title: "Building a Debian rootfs from an unprivileged user with debootstrap" slug: debootscrap authors: "Florian Maury" description: "Faking fakechroot to build a debian rootfs from an unprivileged account, using user namespaces" date: 2022-09-08T18:00:00+02:00 type: posts draft: false layout: "singletoc" categories: - sysadmin tags: - debian - unprivileged - security lang: en --- At Gatewatcher[^GW], we put efforts in making our building system reproducible and working offline, so that we can reduce the risk of supply chain attacks. Some efforts are also made so that our building system run with as few privileges as possible. [^GW]: https://www.gatewatcher.com/ One of the few things we were still running as a privileged user recently was the build of our initial Debian root filesystem, for our base system and for our containers. Indeed, the official Debian Docker container from Docker Hub was not generated in a way that we can consider secure for our need. It basically downloads a blob from a web server, does no verification whatsoever of that blob and ships it as the root filesystem[^debdock]. Even though the root filesystem they are using can be rebuilt in a reproducible way, downloading the result from Internet without verifying it against the expected hash is sort of missing the point of reproducible builds. Also, debuerreotype uses `debootstrap`, which is problematic in itself, as explained hereafter. [^debdock]: https://github.com/debuerreotype/docker-debian-artifacts/blob/master/download.sh#L7 To create such root filesystem, multiple tools are provided by the Debian team, among which `debootstrap`, and `multistrap`. Multistrap has not been updated in many years[^multistrap], and suffers from some limitations that were show-stoppers for us, but it is capable to create root filesystems from an unprivileged user without hacks. [^multistrap]: https://browse.dgit.debian.org/multistrap.git/log/ On the other hand, deboostrap is not really friendly with the idea of building a system from an unprivileged user. First of, there is a check to ensure we are running it with UID 0[^checkUID]. This can be bypassed in several documented ways, including using `fakeroot`, which overloads some libc calls, using `LD_PRELOAD`. An other, less hacky, way is to run the program in a user namespace. [^checkUID]: https://salsa.debian.org/installer-team/debootstrap/-/blob/90747310f8722ca7e3b6a13af3f0c0e76cf7dd74/debootstrap#L605 Unfortunately, this is not sufficient to run `debootstrap`, since it performs another check consisting of trying to create a "/dev/null" node[^checkNode]. This is more problematic since nodes cannot be created from a user namespace, as this would create a easy way of escaping the namespace. [^checkNode]: https://salsa.debian.org/installer-team/debootstrap/-/blob/90747310f8722ca7e3b6a13af3f0c0e76cf7dd74/functions#L1664 As it seems, though, there is a way to build an unprivileged Debian root filesystem that is even built into deboostrap, using the installation variant "fakechroot". Alternate code paths exist in deboostrap when this variant is selected that side-step some checks, and fake some calls. This variant also adds a check to ensure this variant is run only if the `fakechroot` utility is in use. Therefore, you are expected to run debootstap as followed, as documented in the `fakechroot` manpage: ```sh # apt update && apt install -y debootstrap fakeroot fakechroot $ fakechroot fakeroot debootstrap --variant=fakechroot bullseye $HOME/rootfs ``` `fakechroot` works by overloading some functions with `LD_PRELOAD`, and has some documented limitations regarding symlinks. As it happens, these limitations include rewrites of absolute symlinks, by prefixing them with the path of the faked chroot. As a result, within the chroot, you will find links that are broken when actually chrooting, such as when you would use that directory hierarchy as a root filesystem on a container or a virtual machine. With `fakechroot`: ```sh $ readlink /path/to/my/chroot/usr/sbin/telinit /path/to/my/chroot/bin/systemctl ``` Without `fakechroot` (this is what you want to see, in a normal system): ```sh $ readlink /path/to/my/chroot/usr/sbin/telinit /bin/systemctl ``` After some verifications, we decided that it was safe to fake the use of `fakechroot`, while using the "fakechroot" installation variant. For this, we set the environment variable `FAKECHROOT` to `true`, which fakechroot is supposed to set and which is controlled by `debootstrap` to authorize the use of the "fakechroot" variant. And it worked. So to build a working root filesystem from an unprivileged user, we are now doing the following: ```sh $ podman unshare # FAKECHROOT=true debootstrap --variant=fakechroot bullseye chroot/ # tar -C chroot/ --exclude=dev/* -czf ./chroot.tgz . # exit $ cat < Containerfile FROM scratch ADD chroot.tgz . CMD ["/bin/bash"] EOF $ podman build -f Containerfile ``` This series of commands builds a Debian container from an unprivileged user. More work needs to be done to achieve offline reproducible builds, of course, but none require hacks like this, thankfully.