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
This commit is contained in:
Michael Vogt authored and Lennart Poettering committed 2026-10-30 20:06:11 +01:00
1 parent 4a91fd14e5
commit 7321925477
13 files changed
+287 -14

No files matched your search

+9
View File
@@ -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:
+15 -13
View File
@@ -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'],
]
+3
View File
@@ -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',
+1 -1
View File
@@ -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
+3
View File
@@ -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.*
+40
View File
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?> <!--*-nxml-*-->
<!DOCTYPE policyconfig PUBLIC "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"https://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<!--
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.
-->
<policyconfig>
<vendor>The systemd Project</vendor>
<vendor_url>https://systemd.io</vendor_url>
<action id="io.systemd.sftp.root">
<description gettext-domain="systemd">Access the local file system via SFTP</description>
<message gettext-domain="systemd">Authentication is required to access the local file system via SFTP with full root privileges.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
<action id="io.systemd.sftp.root-read-only">
<description gettext-domain="systemd">Read the local file system via SFTP</description>
<message gettext-domain="systemd">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.</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin</allow_active>
</defaults>
</action>
</policyconfig>
+5
View File
@@ -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
+73
View File
@@ -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"
+16
View File
@@ -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'],
+42
View File
@@ -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
+24
View File
@@ -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
+32
View File
@@ -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
+24
View File
@@ -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