Run the long-lived process as the configured non-root user (#31)

* Run the long-lived process as the configured non-root user

start.py used to stay root for its whole life; only the individual
seaf-cli/seaf-daemon subprocess calls were unprivileged, via a per-call
runuser wrapper. The orchestrating Python process itself, including the
RPC socket to seaf-daemon and the watch_status() loop, ran as root the
entire time.

main() now drops privileges in-process (dsc.misc.drop_privileges, using
os.initgroups/setgid/setuid) right after setup_uid()/create_dir() -
which still need root to remap the seafile user's UID/GID and chown
/dsc - and before SeafileClient is constructed. Since the process is
already the target user by the time any seaf-cli call happens, runuser
is removed from user_cmd(); it also could not be invoked from an
already non-root process.

Verified against the real image, not just mocked unit tests: a running
container's PID 1 shows Uid: 1000 1000 1000 1000 in /proc/1/status, and
tests/smoke.sh gained a check exercising setup_uid/create_dir/
drop_privileges together against the image's real /etc/passwd.

* Isolate os.environ mutation in drop_privileges test

drop_privileges() writes HOME/USER/LOGNAME directly to os.environ, and
the test asserting that didn't restore them, leaking state into later
tests. monkeypatch.delenv registers the keys so its teardown restores
them regardless of who wrote to os.environ.

---------

Co-authored-by: Maxim Shulga <maxim.shulga@alludo.com>
This commit is contained in:
Maksim ShulgaandMaxim Shulga authored and GitHub committed 2026-08-03 17:44:34 +00:00
1 parent b0dfc1ad74
commit 6d32460b13
5 files changed
+94 -14

No files matched your search

+26 -7
View File
@@ -42,17 +42,36 @@ def create_dir(dir_path: str):
def user_cmd(argv: list) -> list:
"""
Wrap a command so that it runs as the seafile user.
Return a command as-is, having already validated it is a plain argument
list and never a shell string.
``runuser --`` execs the command directly. ``su -c`` would hand it to a
login shell instead, which turns every server-provided value in the
command into shell code. Like ``su``, ``runuser`` sets HOME, USER and a
default PATH for the target user, so seaf-cli still finds its config and
its own wrapper.
The process itself runs as the seafile user for its whole life after
drop_privileges() (see start.py), so subprocess calls no longer need to
be individually re-targeted with ``runuser``.
"""
if not isinstance(argv, list):
raise TypeError(f"Command must be a list of arguments, got {type(argv).__name__}")
return ["runuser", "-u", DEFAULT_USERNAME, "--"] + argv
return argv
def drop_privileges(uid: int, gid: int):
"""
Permanently switch the current process to the given UID/GID.
Must run after setup_uid()/create_dir(), which need root, and before any
other code, since everything from here on (the daemon, the RPC socket,
the sync loop) runs as this user for the rest of the process's life.
Order matters: supplementary groups and the GID must be set before the
UID is dropped, since giving up root removes the permission to change
them.
"""
user_pwinfo = pwd.getpwnam(DEFAULT_USERNAME)
os.initgroups(DEFAULT_USERNAME, gid)
os.setgid(gid)
os.setuid(uid)
os.environ["HOME"] = user_pwinfo.pw_dir
os.environ["USER"] = DEFAULT_USERNAME
os.environ["LOGNAME"] = DEFAULT_USERNAME
def hide_secrets(cmd: list, *secrets: str) -> list: