This was done on Debian 13, but should work on any modern systemd distro. The main advantage here is that you can enable per-user containers / jails for SSH and rsync.
First let’s create a minimal install with debootstrap
debootstrap --variant=minbase trixie /var/lib/machines/jail http://deb.debian.org/debian
We’ll create the nspawn config next in /etc/systemd/nspawn/jail.nspawn
[Exec]
Boot=yes
[Files]
BindReadOnly=/Videos
Reload systemd and enable the container, making sure we install systemd
systemctl daemon-reload
systemctl enable --now [email protected]
systemd-nspawn -D /var/lib/machines/jail /bin/bash
# apt-get install systemd dbus
Create a minimal wrapper, mostly so we can preserve rsync functionality. We’re using /usr/local/bin/nspawn-ssh-wrapper
#!/bin/bash
set -euo pipefail
MACHINE="jail"
USER="jail"
CMD="${SSH_ORIGINAL_COMMAND:-}"
# Find the container leader PID (host PID of container init)
LEADER="$(/usr/bin/machinectl show "$MACHINE" -p Leader --value)"
if [ -z "$LEADER" ] || [ "$LEADER" = "0" ]; then
echo "Container $MACHINE not running" >&2
exit 1
fi
if [ -z "$CMD" ]; then
# Interactive: machinectl shell is fine (banner doesn't matter)
exec /usr/bin/machinectl shell "${USER}@${MACHINE}"
else
# Non-interactive (rsync/scp): MUST be banner-free.
# Enter namespaces and run the SSH_ORIGINAL_COMMAND as the container user.
exec /usr/bin/nsenter -t "$LEADER" -a \
/usr/sbin/runuser -u "$USER" -- /bin/sh -c "$CMD"
fi
In your sshd_config
Match User jail
ForceCommand /usr/bin/sudo -n --preserve-env=SSH_ORIGINAL_COMMAND /usr/local/bin/nspawn-ssh-wrapper
PermitTTY yes
X11Forwarding no
AllowTcpForwarding no
And in /etc/sudoers
Defaults:jail env_keep += "SSH_ORIGINAL_COMMAND"
jail ALL=(root) NOPASSWD: /usr/local/bin/nspawn-ssh-wrapper