If you’ve ever run a one-off command like this on a production box:
sudo bash suspicious-script.sh
…you already know the risk: it has your full filesystem, full network, full privileges, and no guardrails.
For long-running services, we usually harden unit files. But for ad-hoc commands, people often skip safety.
This is where systemd-run is underrated: it lets you launch a transient unit with hardening flags and resource limits without writing a permanent service file.
In this guide, I’ll show a practical pattern you can reuse.
Why systemd-run for one-off tasks?
systemd-run creates transient .service or .scope units and passes normal unit properties via -p/--property.
That means you can apply the same controls you’d use in hardened service files, including:
- Filesystem restrictions (
ProtectSystem,ProtectHome,ReadWritePaths) - Privilege hardening (
NoNewPrivileges) - Namespace isolation (
PrivateTmp) - Resource caps (
MemoryMax,CPUQuota)
This gives you a “safer blast radius” for temporary jobs.
Prerequisites
- Linux host with systemd
- Root or sudo privileges
-
systemd-runavailable (usually from systemd package)
Quick check:
systemd-run --version
Pattern 1: Safe default sandbox for an untrusted script
Assume you need to execute ./vendor-maintenance.sh, but you don’t fully trust what it might touch.
sudo systemd-run \
--unit=adhoc-sandbox-$(date +%s) \
--wait --collect \
-p NoNewPrivileges=yes \
-p PrivateTmp=yes \
-p ProtectHome=read-only \
-p ProtectSystem=strict \
-p ReadWritePaths=/var/tmp \
-p MemoryMax=1G \
-p CPUQuota=50% \
--service-type=exec \
/usr/bin/bash ./vendor-maintenance.sh
What these settings do
-
ProtectSystem=strict: makes most of the filesystem read-only. -
ReadWritePaths=/var/tmp: explicitly allow write access only where needed. -
ProtectHome=read-only: prevents arbitrary writes to user home dirs. -
PrivateTmp=yes: isolated/tmpand/var/tmpmount namespace. -
NoNewPrivileges=yes: blocks privilege escalation via setuid/capabilities. -
MemoryMax/CPUQuota: keeps runaway jobs from starving the host. -
--wait --collect: wait for completion and clean up transient unit metadata.
Tip: start restrictive, then open only what the task truly needs.
Pattern 2: Allow a specific writable work dir (and nothing else)
For backup or report scripts that must write artifacts:
sudo install -d -m 0750 /var/lib/adhoc-jobs/output
sudo systemd-run \
--unit=adhoc-report-$(date +%s) \
--wait --collect \
-p ProtectSystem=strict \
-p ProtectHome=yes \
-p PrivateTmp=yes \
-p NoNewPrivileges=yes \
-p ReadWritePaths=/var/lib/adhoc-jobs/output \
--service-type=exec \
/usr/local/bin/generate-report.sh
If the script fails with permission errors, that’s often good news: your policy is actually blocking unexpected writes.
Pattern 3: Dry-run your policy with a harmless probe
Before running the real script, validate that writes are constrained.
sudo systemd-run --wait --collect \
-p ProtectSystem=strict \
-p ReadWritePaths=/var/tmp \
--service-type=exec \
/usr/bin/bash -lc 'touch /etc/should-fail && touch /var/tmp/should-pass'
Expected outcome:
-
/etc/should-failshould fail (read-only path) -
/var/tmp/should-passshould succeed
This quick test catches bad policy assumptions early.
Auditing and debugging transient runs
List recent transient units:
systemctl list-units 'adhoc-*' --all
Inspect logs for a specific run:
journalctl -u adhoc-sandbox-1234567890 -e --no-pager
Check resulting unit properties:
systemctl show adhoc-sandbox-1234567890 \
-p ProtectSystem -p ProtectHome -p PrivateTmp -p MemoryMax -p CPUQuotaPerSecUSec
Common mistakes to avoid
-
Using
ProtectSystem=strictwithoutReadWritePaths- Your script may break because everything is read-only. Add minimal allowlist paths.
-
Skipping
--wait- You lose immediate exit status feedback in automation contexts.
-
Giving broad writable paths too early
-
ReadWritePaths=/defeats the point. Keep the write allowlist tiny.
-
-
Forgetting resource caps for unknown scripts
- Add at least
MemoryMaxandCPUQuotafor safer host behavior.
- Add at least
When to use this vs a normal unit file
Use systemd-run when:
- you need an ad-hoc or infrequent operation,
- you want hardening without maintaining permanent unit files,
- you’re testing an execution policy quickly.
Use a persistent unit file when:
- the task is repeated/scheduled long-term,
- you need version-controlled service definitions,
- multiple operators need stable, named config.
Final takeaway
If a command is risky enough that you’d hesitate to run it as root directly, run it in a transient sandbox instead.
systemd-run gives you the speed of one-off execution with much better safety boundaries.
References
-
systemd-runmanual (freedesktop): https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html -
systemd.execmanual (freedesktop): https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html -
systemd.resource-controlmanual (freedesktop): https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html -
systemd-run(1)man page mirror (man7): https://man7.org/linux/man-pages/man1/systemd-run.1.html
Top comments (0)