Commit Graph
8 Commits
Author SHA1 Message Date
Maksim ShulgaandMaxim Shulga 6d32460b13 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>
2026-08-03 17:44:34 +00:00
Maxim Shulga e6034a69a6 Read the environment with typed parsers and apply the documented defaults
set_defaults() assigned os.getenv() results directly, which overwrote the
argparse defaults with None whenever a variable was unset. A default container
therefore ran "seaf-cli config -k delete_confirm_threshold -v None", and the
same for upload_limit and download_limit: none of the three documented
defaults were ever applied.

SEAFILE_UID and SEAFILE_GID had a second problem. argparse type=int applies
only to command line flags, never to set_defaults, so both stayed strings.
The check in setup_uid compares them against pwd integers, and a string is
never equal to one, so usermod ran on every start.

dsc/config.py reads each variable with its documented default and rejects a
malformed or negative value at startup, naming the variable, instead of
passing it on. An empty variable counts as unset.

Booleans are now rendered as true and false rather than Python's True and
False. The daemon reads disable_verify_certificate with its boolean accessor,
which does not recognise the capitalised form. I could not run the daemon to
confirm the previous value's effect, since the AppImage is x86_64 only.

seaf-cli is no longer given a None value for any option.
2026-08-02 17:44:57 -07:00
Maxim Shulga eef0e453bf Authenticate from mounted secrets and stop passing the password to seaf-cli
The password could only be given through the PASSWORD variable, so Docker
Compose and Swarm secrets were unusable and the password sat in the container
environment.

PASSWORD_FILE and TOKEN_FILE read a secret from a mounted file. Only trailing
newlines are stripped, since secret files are usually written with one and a
password that silently carries it just fails to authenticate; everything else,
including surrounding spaces, belongs to the secret. Setting both a variable
and a file for one secret is rejected rather than resolved by precedence,
because the two are meant to be the same value and quietly picking one hides
the mistake until authentication fails.

The installed seaf-cli supports token authentication and ignores the password
when a token is given, so sync now passes -T instead of -p. The account
password no longer appears in any command line. The token does, because
seaf-cli accepts credentials only as arguments, but a token can be revoked on
the server. This is documented in the README.

Both secrets are masked before a command is logged.

Tests cover newline handling, empty and missing files, conflicting sources,
and that neither secret reaches the logs. Requests are now blocked in tests:
the client retries with backoff, so a test that reached the network appeared
to hang instead of failing.
2026-08-02 17:21:46 -07:00
Maxim Shulga c15763d9f8 Stop server-provided values from reaching a shell or escaping /dsc/seafile
Commands were joined into a string and passed to "su -c", so a login shell
parsed every interpolated value. A library name is chosen on the server, and
"sync -d /dsc/seafile/<name>" put it straight into that string: a library
named 'Docs; touch /tmp/pwned; #' ran as an arbitrary command. Confirmed
against the built image before and after this change.

Every command is now an argument list executed by "runuser --", which execs
directly with no shell. runuser -u sets the same HOME and a PATH that still
finds the seaf-cli wrapper, so the daemon environment is unchanged.

The password no longer needs its surrounding quotes, which also fixes
passwords containing quotes, dollar signs, and backticks.

Library names are untrusted, so dsc/paths.py reduces each one to a single
path component and proves the result stays under the libraries directory
with symlinks resolved. Empty, ".", "..", and names containing a separator
are skipped with a warning; names that would collide are suffixed with their
library ID rather than sharing a directory.

Tests cover quotes, command substitutions, separators, newlines, absolute
paths, parent traversal, Unicode, and colliding names. They run in a separate
CI job so application failures stay distinguishable from packaging failures.
2026-08-02 16:53:19 -07:00
snegov b03d7bb1a3 Fix concealing a password in logs 2023-10-20 18:07:54 -07:00
snegov 7f39e0f271 Initialize seafile client during container start 2023-09-16 01:27:46 -07:00
snegov 6af5eb1b26 Wait for seafile daemon to start 2023-09-14 16:22:02 -07:00
snegov 724166b4a4 Support library names in configuration 2022-01-30 18:27:37 +03:00