From 732192547701a95da9bcb9eee7ffe5a35b076c3c Mon Sep 17 00:00:00 2001 From: Michael Vogt Date: Mon, 15 Jun 2026 17:00:56 +0200 Subject: [PATCH] units: add optional varlink-sftp socket and service With the varlink protocol upgrade feature we can use provide file system access via varlink easily by re-using the existing sftp standard. SFTP is independant of ssh and provides a well defined and widely used file transfer protocol. So here we add two new (optional) varlink interfaces: - io.systemd.Sftp.Root.Connect - io.systemd.Sftp.RootReadOnly.Connect They are behind polkit auth as io.systemd.sftp.root{,-read-only}. Note that handing this is out is equivalent for giving root access: even ReadOnly mode will allow to read secrets like /etc/shadow so its inherently risky. The two actions mostly exist to prevent accidents not as a (strong) protection boundary. The unit description warns about this and the polkit prompt also describes the full implications. The use cases are: - file access via the varlink-http-bridge [1] - file access over AF_VSOCK The install is optional and turned off by default. [1] https://github.com/systemd/varlink-http-bridge --- NEWS | 9 +++ meson.build | 28 ++++---- meson_options.txt | 3 + mkosi/mkosi.functions | 2 +- presets/90-systemd.preset | 3 + src/varlinkctl/io.systemd.sftp.policy | 40 +++++++++++ src/varlinkctl/meson.build | 5 ++ test/units/TEST-74-AUX-UTILS.varlink-sftp.sh | 73 ++++++++++++++++++++ units/meson.build | 16 +++++ units/varlink-sftp-root-ro.service.in | 42 +++++++++++ units/varlink-sftp-root-ro.socket.in | 24 +++++++ units/varlink-sftp-root.service.in | 32 +++++++++ units/varlink-sftp-root.socket.in | 24 +++++++ 13 files changed, 287 insertions(+), 14 deletions(-) create mode 100644 src/varlinkctl/io.systemd.sftp.policy create mode 100755 test/units/TEST-74-AUX-UTILS.varlink-sftp.sh create mode 100644 units/varlink-sftp-root-ro.service.in create mode 100644 units/varlink-sftp-root-ro.socket.in create mode 100644 units/varlink-sftp-root.service.in create mode 100644 units/varlink-sftp-root.socket.in diff --git a/NEWS b/NEWS index c52d640dc0e..448cd766963 100644 --- a/NEWS +++ b/NEWS @@ -266,6 +266,15 @@ CHANGES WITH 263 in spe: via polkit that the client is authorized for the specified action before the connection is upgraded and the command is executed. + * New optional varlink-sftp-root.socket and varlink-sftp-root-ro.socket + units expose the local file system via SFTP over an upgraded Varlink + connection, read-write and read-only respectively. Access is + controlled via the polkit actions io.systemd.sftp.root and + io.systemd.sftp.root-read-only. They are built only with + -Dvarlink-sftp=true, and are disabled by default. Note that both + run as root so allowing access is equivalent to r/w root access or + r/o /etc/shadow access. + CHANGES WITH 262: Announcements of Future Feature Removals and Incompatible Changes: diff --git a/meson.build b/meson.build index 00a4f908437..5cf7c296fbc 100644 --- a/meson.build +++ b/meson.build @@ -785,19 +785,20 @@ ln_s = ln.full_path() + ' -frsT -- "${DESTDIR:-}@0@" "${DESTDIR:-}@1@"' # If -Dxxx-path option is found, use that. Otherwise, use the default from the # middle column; a full path is used directly, a relative path is converted to # /usr/bin/foo or /usr/sbin/foo, depending on whether split-bin is enabled. -progs = [['quotaon', 'quotaon' ], - ['quotacheck', 'quotacheck' ], - ['kmod', '/usr/bin/kmod' ], - ['kexec', 'kexec' ], - ['sulogin', 'sulogin' ], - ['swapon', 'swapon' ], - ['swapoff', 'swapoff' ], - ['agetty', 'agetty' ], - ['mount', '/usr/bin/mount', 'MOUNT_PATH'], - ['umount', '/usr/bin/umount', 'UMOUNT_PATH'], - ['loadkeys', '/usr/bin/loadkeys', 'KBD_LOADKEYS'], - ['setfont', '/usr/bin/setfont', 'KBD_SETFONT'], - ['nologin', 'nologin', ], +progs = [['quotaon', 'quotaon' ], + ['quotacheck', 'quotacheck' ], + ['kmod', '/usr/bin/kmod' ], + ['kexec', 'kexec' ], + ['sulogin', 'sulogin' ], + ['swapon', 'swapon' ], + ['swapoff', 'swapoff' ], + ['agetty', 'agetty' ], + ['mount', '/usr/bin/mount', 'MOUNT_PATH'], + ['umount', '/usr/bin/umount', 'UMOUNT_PATH'], + ['loadkeys', '/usr/bin/loadkeys', 'KBD_LOADKEYS'], + ['setfont', '/usr/bin/setfont', 'KBD_SETFONT'], + ['nologin', 'nologin' ], + ['sftp-server', '/usr/libexec/openssh/sftp-server', 'SFTP_SERVER'], ] foreach prog : progs path = get_option(prog[0] + '-path') @@ -1862,6 +1863,7 @@ foreach tuple : [ ['utmp', get_option('libc') != 'musl', 'musl does not support it'], ['userdb'], ['varlink-monitord'], + ['varlink-sftp'], ['vconsole'], ['xdg-autostart'], ] diff --git a/meson_options.txt b/meson_options.txt index 39aaf00396b..0095a83aaf8 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -63,6 +63,7 @@ option('nologin-path', type : 'string', description : 'path to nologin') option('quotacheck-path', type : 'string', description : 'path to quotacheck') option('quotaon-path', type : 'string', description : 'path to quotaon') option('setfont-path', type : 'string', description : 'path to setfont') +option('sftp-server-path', type : 'string', description : 'path to the SFTP server binary') option('sulogin-path', type : 'string', description : 'path to sulogin') option('swapoff-path', type : 'string', description : 'path to swapoff') option('swapon-path', type : 'string', description : 'path to swapon') @@ -286,6 +287,8 @@ option('validatefs', type : 'feature', description : 'install systemd-validatefs and associated files') option('varlink-monitord', type : 'boolean', description : 'install the systemd-varlink-monitord daemon') +option('varlink-sftp', type : 'boolean', value : false, + description : 'install the varlink-sftp sockets and services') option('vconsole', type : 'boolean', description : 'support for vconsole configuration') option('veritysetup', type : 'feature', diff --git a/mkosi/mkosi.functions b/mkosi/mkosi.functions index 352fb5e3344..2455920847c 100644 --- a/mkosi/mkosi.functions +++ b/mkosi/mkosi.functions @@ -24,7 +24,7 @@ if ((LLVM)) && [[ -n "$SANITIZERS" ]]; then MKOSI_RUSTFLAGS="-Clink-arg=-shared-libasan -Clink-arg=-Wl,-rpath=$(realpath "$(clang --print-runtime-dir)")" fi -MKOSI_MESON_OPTIONS="-D mode=developer -D vcs-tag=${VCS_TAG:-true} -D b_sanitize=${SANITIZERS:-none} -Dtime-epoch=1744207869" +MKOSI_MESON_OPTIONS="-D mode=developer -D vcs-tag=${VCS_TAG:-true} -D b_sanitize=${SANITIZERS:-none} -Dtime-epoch=1744207869 -D varlink-sftp=true" if ((WIPE)) && [[ -d "$BUILDDIR/meson-private" ]]; then MKOSI_MESON_OPTIONS="$MKOSI_MESON_OPTIONS --wipe" fi diff --git a/presets/90-systemd.preset b/presets/90-systemd.preset index d9bd7763b76..bf15a4cda9c 100644 --- a/presets/90-systemd.preset +++ b/presets/90-systemd.preset @@ -78,3 +78,6 @@ disable systemd-journal-remote.* disable systemd-journal-upload.* disable systemd-sysext-sysroot.service disable systemd-time-wait-sync.service + +disable varlink-sftp-root.* +disable varlink-sftp-root-ro.* diff --git a/src/varlinkctl/io.systemd.sftp.policy b/src/varlinkctl/io.systemd.sftp.policy new file mode 100644 index 00000000000..4025d51e77a --- /dev/null +++ b/src/varlinkctl/io.systemd.sftp.policy @@ -0,0 +1,40 @@ + + + + + + + + The systemd Project + https://systemd.io + + + Access the local file system via SFTP + Authentication is required to access the local file system via SFTP with full root privileges. + + auth_admin + auth_admin + auth_admin + + + + + Read the local file system via SFTP + Authentication is required to read the local file system via SFTP with root privileges. This allows reading any file, including secrets such as /etc/shadow. + + auth_admin + auth_admin + auth_admin + + + diff --git a/src/varlinkctl/meson.build b/src/varlinkctl/meson.build index 7047b6b4659..38dd4e509a0 100644 --- a/src/varlinkctl/meson.build +++ b/src/varlinkctl/meson.build @@ -15,3 +15,8 @@ executables += [ ] install_emptydir(varlinkbridgesdir) + +if conf.get('ENABLE_VARLINK_SFTP') == 1 + install_data('io.systemd.sftp.policy', + install_dir : polkitpolicydir) +endif diff --git a/test/units/TEST-74-AUX-UTILS.varlink-sftp.sh b/test/units/TEST-74-AUX-UTILS.varlink-sftp.sh new file mode 100755 index 00000000000..4ba7c56a555 --- /dev/null +++ b/test/units/TEST-74-AUX-UTILS.varlink-sftp.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +SFTP_SERVER="$(systemctl cat varlink-sftp-root.service 2>/dev/null | sed -n 's/^ConditionPathExists=//p')" || : +if [[ ! -x "$SFTP_SERVER" ]] || ! command -v sftp >/dev/null; then + echo "sftp-server or sftp not available, skipping." >&2 + exit 77 +fi + +TMPDIR_SFTP="$(mktemp -d)" + +at_exit() { + set +e + systemctl stop varlink-sftp-root.socket varlink-sftp-root-ro.socket varlink-sftp-root.service varlink-sftp-root-ro.service + if [[ -e /etc/polkit-1/rules.d/varlink-sftp-test.rules ]]; then + rm -f /etc/polkit-1/rules.d/varlink-sftp-test.rules + systemctl try-reload-or-restart polkit.service + fi + rm -rf "$TMPDIR_SFTP" +} +trap at_exit EXIT + +sftp_varlink() { + sftp -b - -D "varlinkctl call --upgrade /run/varlink/registry/$1 $1.Connect" +} + +sftp_varlink_testuser() { + run0 --pipe -u testuser sftp -b - -D "varlinkctl call --upgrade /run/varlink/registry/$1 $1.Connect" +} + +systemctl start varlink-sftp-root.socket varlink-sftp-root-ro.socket +chmod 755 "$TMPDIR_SFTP" +echo hello >"$TMPDIR_SFTP/src" + +# Without a polkit rule an unprivileged user is denied on both endpoints +(! sftp_varlink_testuser io.systemd.Sftp.RootReadOnly <<<"get $TMPDIR_SFTP/src /tmp/varlink-sftp-denied") +test ! -e /tmp/varlink-sftp-denied + +# Grant testuser read-only access only and verify the two actions are enforced independently. +# Varlink polkit checks only send a pidfd subject, which needs kernel >= 6.5 and polkit >= 124. +if [[ -f /usr/share/dbus-1/system-services/org.freedesktop.PolicyKit1.service ]] && + [[ -f /usr/share/polkit-1/actions/io.systemd.sftp.policy ]] && + systemd-analyze compare-versions "$(uname -r)" ge 6.5 && + systemd-analyze compare-versions "$(pkcheck --version | awk '{print $3}')" ge 124; then + mkdir -p /etc/polkit-1/rules.d + cat >/etc/polkit-1/rules.d/varlink-sftp-test.rules <<'EOF2' +polkit.addRule(function(action, subject) { + if (action.id == "io.systemd.sftp.root-read-only" && subject.user == "testuser") { + return polkit.Result.YES; + } +}); +EOF2 + systemctl try-reload-or-restart polkit.service + + sftp_varlink_testuser io.systemd.Sftp.RootReadOnly <<<"get $TMPDIR_SFTP/src /tmp/varlink-sftp-testuser-get" + cmp "$TMPDIR_SFTP/src" /tmp/varlink-sftp-testuser-get + rm -f /tmp/varlink-sftp-testuser-get + (! sftp_varlink_testuser io.systemd.Sftp.Root <<<"put $TMPDIR_SFTP/src $TMPDIR_SFTP/testuser-rw") + test ! -e "$TMPDIR_SFTP/testuser-rw" + + rm -f /etc/polkit-1/rules.d/varlink-sftp-test.rules + systemctl try-reload-or-restart polkit.service +fi + +(! sftp_varlink io.systemd.Sftp.RootReadOnly <<<"put $TMPDIR_SFTP/src $TMPDIR_SFTP/ro") +test ! -e "$TMPDIR_SFTP/ro" +sftp_varlink io.systemd.Sftp.RootReadOnly <<<"get $TMPDIR_SFTP/src $TMPDIR_SFTP/ro-get" +cmp "$TMPDIR_SFTP/src" "$TMPDIR_SFTP/ro-get" + +sftp_varlink io.systemd.Sftp.Root <<<"put $TMPDIR_SFTP/src $TMPDIR_SFTP/rw" +cmp "$TMPDIR_SFTP/src" "$TMPDIR_SFTP/rw" diff --git a/units/meson.build b/units/meson.build index 4b27b4c1fab..f2394efb699 100644 --- a/units/meson.build +++ b/units/meson.build @@ -1203,6 +1203,22 @@ units = [ 'conditions' : ['ENABLE_MACHINED'], 'symlinks' : ['remote-fs.target.wants/', 'machines.target.wants/'], }, + { + 'file' : 'varlink-sftp-root.service.in', + 'conditions' : ['ENABLE_VARLINK_SFTP'], + }, + { + 'file' : 'varlink-sftp-root.socket.in', + 'conditions' : ['ENABLE_VARLINK_SFTP'], + }, + { + 'file' : 'varlink-sftp-root-ro.service.in', + 'conditions' : ['ENABLE_VARLINK_SFTP'], + }, + { + 'file' : 'varlink-sftp-root-ro.socket.in', + 'conditions' : ['ENABLE_VARLINK_SFTP'], + }, { 'file' : 'veritysetup-pre.target', 'conditions' : ['ENABLE_VERITYSETUP'], diff --git a/units/varlink-sftp-root-ro.service.in b/units/varlink-sftp-root-ro.service.in new file mode 100644 index 00000000000..05b3bc08742 --- /dev/null +++ b/units/varlink-sftp-root-ro.service.in @@ -0,0 +1,42 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Varlink SFTP Server (root, read-only) +Documentation=man:varlinkctl(1) +Requires=varlink-sftp-root-ro.socket +ConditionPathExists={{SFTP_SERVER}} + +[Service] +Type=notify +# -R places sftp-server in read-only mode +# -e makes it log to stderr (which ends up in the journal) +ExecStart=varlinkctl serve --polkit-action=io.systemd.sftp.root-read-only io.systemd.Sftp.RootReadOnly.Connect -- {{SFTP_SERVER}} -e -R + +# Avoid settings that hide parts of the file system (PrivateDevices=, +# ProtectProc=, ...) so both flavours export the same view. +NoNewPrivileges=yes +PrivateNetwork=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +SystemCallArchitectures=native +TasksMax=64 +# While -R already denies writes we still enforce it (because we can). +ProtectSystem=strict +ProtectHome=read-only +RestrictSUIDSGID=yes +SystemCallFilter=@system-service +SystemCallFilter=~@privileged @resources +SystemCallErrorNumber=EPERM + +[Install] +Also=varlink-sftp-root-ro.socket diff --git a/units/varlink-sftp-root-ro.socket.in b/units/varlink-sftp-root-ro.socket.in new file mode 100644 index 00000000000..98813f13d47 --- /dev/null +++ b/units/varlink-sftp-root-ro.socket.in @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Varlink SFTP Server Socket (root, read-only) +Documentation=man:varlinkctl(1) +ConditionPathExists={{SFTP_SERVER}} + +[Socket] +ListenStream=/run/systemd/io.systemd.Sftp.RootReadOnly +Symlinks=/run/varlink/registry/io.systemd.Sftp.RootReadOnly +SocketMode=0666 +RemoveOnStop=yes +XAttrEntryPoint=user.varlink=entrypoint +XAttrListen=user.varlink=listen + +[Install] +WantedBy=sockets.target diff --git a/units/varlink-sftp-root.service.in b/units/varlink-sftp-root.service.in new file mode 100644 index 00000000000..0fa6c3a520c --- /dev/null +++ b/units/varlink-sftp-root.service.in @@ -0,0 +1,32 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Varlink SFTP Server (root, read-write) +Documentation=man:varlinkctl(1) +Requires=varlink-sftp-root.socket +ConditionPathExists={{SFTP_SERVER}} + +[Service] +Type=notify +# -e makes it log to stderr (which ends up in the journal) +ExecStart=varlinkctl serve --polkit-action=io.systemd.sftp.root io.systemd.Sftp.Root.Connect -- {{SFTP_SERVER}} -e + +NoNewPrivileges=yes +PrivateNetwork=yes +RestrictAddressFamilies=AF_UNIX +RestrictNamespaces=yes +RestrictRealtime=yes +LockPersonality=yes +MemoryDenyWriteExecute=yes +SystemCallArchitectures=native +TasksMax=64 + +[Install] +Also=varlink-sftp-root.socket diff --git a/units/varlink-sftp-root.socket.in b/units/varlink-sftp-root.socket.in new file mode 100644 index 00000000000..b3ee78f459a --- /dev/null +++ b/units/varlink-sftp-root.socket.in @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Varlink SFTP Server Socket (root, read-write) +Documentation=man:varlinkctl(1) +ConditionPathExists={{SFTP_SERVER}} + +[Socket] +ListenStream=/run/systemd/io.systemd.Sftp.Root +Symlinks=/run/varlink/registry/io.systemd.Sftp.Root +SocketMode=0666 +RemoveOnStop=yes +XAttrEntryPoint=user.varlink=entrypoint +XAttrListen=user.varlink=listen + +[Install] +WantedBy=sockets.target