2

I'm trying to utilize some of the systemd helpers to chroot(2) the process using RootDirectory=. It's a rudimentary Python script that lives under /srv/http that hosts a web server. It has a shebang #!/usr/bin/python (and I've also tried different combinations)

The service file is also quite simple:

[Unit]
Wants=network-online.target
After=network-online.target

AssertPathExists=/srv/http

[Service]
Type=simple
Restart=always
RestartSec=10

RootDirectory=/srv/http
PrivateTmp=true

ExecStart=/server.py
PIDFile=/run/miniweb.pid

[Install]
WantedBy=multi-user.target

The log clearly says it can't find the executable:

Mar 11 19:23:49 bigrigv2 systemd[13213]: testweb.service: Failed to execute /server.py: No such file or directory
Mar 11 19:23:49 bigrigv2 systemd[13213]: testweb.service: Failed at step EXEC spawning /server.py: No such file or directory

It's marked as executable:

-rwxr-xr-x 1 anton anton  650 Mar 11 19:06 server.py

I also tried ExecStart=/bin/python /srv/http/server.py and other variations. I'm not entirely sure I even understand the concept of RootDirectory and how to properly execute for instance Python or other binaries from a chrooted service script. My assumption is that before executing the service, it chroot:s in to /srv/http after which the service won't be able to back out and execute Python in this case. Which would make sense, but then I don't quite get why /server.py isn't found. And how would you execute things that are dependent on other binaries? Most solutions mention utilizing the language (C for instance) chroot and control it from the application, but then I don't understand the point of offering chroot in the service script for other things than very limited bash scripts or standalone binaries.

Probably an extremely easy problem, but I'm quite lost and any help would be appreciated!

1 Answer 1

3
+50

To answer your question directly:

how to properly execute a service with external dependencies outside of the chroot using RootDirectory.

You can't. If something is in a chroot jail, it cannot access anything outside, including binaries and libraries. But there are still ways for you to protect your system.

From man systemd.exec

Takes a directory path relative to the host's root directory (i.e. the root of the system running the service manager). Sets the root directory for executed processes, with the chroot(2) system call. If this is used, it must be ensured that the process binary and all its auxiliary files are available in the chroot() jail.

If you set RootDirectory=/srv/http, then when /server.py is called, it will try to execute /usr/bin/python, but fail because it can't find that path. Even if you could do something like ExecStartPre=/bin/cp /usr/bin/python /src/http/usr/bin/python, you'll still have a problem when you try to import flask or import pyramid as those libraries are not installed in your chroot. I did a test with a setup like yours and without a good shebang I got the same error as you.

Here are three options:

  1. Give your chroot everything it needs. If you're on a debian-based system, then debootstrap is a great tool to set this up deboostrap buster /srv/http should work. It will install a base system, then you can sudo chroot && apt install python python-flask or whatever else you like inside of there.

  2. Use built-in options to sandbox your service instead of chrooting it. Specifically:

  • RemoveIPC=true and PrivateTmp=true. These ensure that the lifetime of the IPC objects and temporary files created by the executed process is bound to the runtime of the service. Since /tmp and /var/tmp are usually the only world-writable directories on a system this ensures that the unit cannot leave files around after termination.
  • NoNewPrivileges=true and RestrictSUIDSGID=true. These ensure that processes invoked cannot take benefit or create SUID/SGID files or directories.
  • ProtectSystem=strict and ProtectHome=read-only prohibits the service from writing to anywhere in your filesystem (exceptions are /dev/ /proc/ and /sys/. In order to allow the service to write to certain directories, they have to be allow-listed using ReadWritePaths=.
  • RuntimeDirectory= to assign a runtime directory to the service, which is owned by the service's user, and removed automatically when the system is terminated.
  1. You can replace everything I said in step two with a single line DynamicUser=true. There's a good dev's explanation here

Either way, if you aren't using User= or DynamicUser= you should be. This will prevent your service from being root and having read permissions on /etc/shadow or other secrets.

I really like option 3. Enabling it really protects your system from services.

1
  • 1
    I had no clue about most of it honestly, but the third option is really neat. I've heard about it in a distro who had unique users by default for every service, but I didn't think systemd supported a similar solution out of the box more or less. And I think the fact that RootDirectory= created a full set of system directories under the chroot dir, threw me off for a loop despite the documentation. All of the things you say makes perfect sense and are solid advice's! Much appreciated that you took the time to write the answer :)
    – Torxed
    Mar 16, 2021 at 18:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .