First option: make executable only to start, then undo
You're using a wrapper already in your own answer – namely ld-linux.so – to invoke your program. Follow that idea through!
Your systemd service file can ExecStartPre=
to chmod u+x
and ExecStartPost=
to chmod u-x
as soon as the service runs (nb: not only after it stops, which makes this a lot more robust against e.g. power failures).
Another option that I like: mount only for service life
Put that file (executable as it is, including all its setcap'ed capabilities stored in xattrs) into it's own little file system image. To start the executable, make the .service file depend on that file system being mounted in a location where nothing would look to execute files. (If something maliciously specifically looks everywhere to execute files, it'd probably use the ld-linux.so trick anyways.)
- I'd recommend
mksquashfs dubiousexecutable /var/lib/dubiousapplication/fs-image.squash
- Create a systemd .mount unit, e.g.
run-dubiousapplication.mount
, with What=/var/lib/dubiousapplication/fs-image.squash
and Where=/run/dubiousapplication
(note: .mount file name must match Where
, with /
being replaced by -
and the leading /
omitted), and put it where systemd looks for these (probably /etc/systemd/system/). It might need Options=suid
(can't remember whether capabilities set on files are respected without). - Create a systemd .service file for your service. It can run as system service with
User=
specifying the user it runs at, or as user service from your user's session. It must RequiresMountsFor=/run/dubiousapplication
That way, the file system image gets mounted for the live time of this service only.
The "usual" option: containers
If you need isolation, you'd typically want to run a service in a container, so that it only sees the parts of your overall file system that you explicitly specify it to see. Yay to Linux namespaces!
Podman makes integrating containerized services in systemd especially easy, so that's a route I'd take.
You'd want to write a Dockerfile (there's other ways as well, but why?). I typically base minimal services simply on debian, i.e. my Dockerfiles start with FROM debian:12
. You can basically pick any distro that publishes docker images (that is, all major).
Then, you use the COPY <source file> <target path within>
directive to have your executable inside, e.g. COPY dubiousexecutable /usr/bin/
.
You EXPOSE <portnumber>[/udp|/tcp]
internal ports.
You set the thing that gets started when the container gets started to ENTRYPOINT /usr/bin/dubiousexecutable
. So, in total, your Dockerfile would have four lines (unless you first need to install more stuff inside, which you could do, e.g. with RUN apt-get update; apt-get install foobar; apt-get clean
)
Then, you podman build -t dubiouscontainerimage .
a container image.
Then, you podman create -p <externalport>:<internalport> --name dubiouscontainer dubiouscontainerimage
a container of the name dubiouscontainer
from the image dubiouscontainerimage
. (you can do that as many times you like with different ports, for example).
Now you could podman start dubiouscontainer
manually – OR you could use podman generate-systemd dubiouscontainer
to generate systemd service files, which you can then use with systemd.