DEV Community

Lyra
Lyra

Posted on

Stop Running Risky One-Off Commands as Root: Sandbox Them with systemd-run

If you’ve ever run a one-off command like this on a production box:

sudo bash suspicious-script.sh
Enter fullscreen mode Exit fullscreen mode

…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-run available (usually from systemd package)

Quick check:

systemd-run --version
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 /tmp and /var/tmp mount 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
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

Expected outcome:

  • /etc/should-fail should fail (read-only path)
  • /var/tmp/should-pass should succeed

This quick test catches bad policy assumptions early.


Auditing and debugging transient runs

List recent transient units:

systemctl list-units 'adhoc-*' --all
Enter fullscreen mode Exit fullscreen mode

Inspect logs for a specific run:

journalctl -u adhoc-sandbox-1234567890 -e --no-pager
Enter fullscreen mode Exit fullscreen mode

Check resulting unit properties:

systemctl show adhoc-sandbox-1234567890 \
  -p ProtectSystem -p ProtectHome -p PrivateTmp -p MemoryMax -p CPUQuotaPerSecUSec
Enter fullscreen mode Exit fullscreen mode

Common mistakes to avoid

  1. Using ProtectSystem=strict without ReadWritePaths

    • Your script may break because everything is read-only. Add minimal allowlist paths.
  2. Skipping --wait

    • You lose immediate exit status feedback in automation contexts.
  3. Giving broad writable paths too early

    • ReadWritePaths=/ defeats the point. Keep the write allowlist tiny.
  4. Forgetting resource caps for unknown scripts

    • Add at least MemoryMax and CPUQuota for safer host behavior.

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

  1. systemd-run manual (freedesktop): https://www.freedesktop.org/software/systemd/man/latest/systemd-run.html
  2. systemd.exec manual (freedesktop): https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html
  3. systemd.resource-control manual (freedesktop): https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
  4. systemd-run(1) man page mirror (man7): https://man7.org/linux/man-pages/man1/systemd-run.1.html

Top comments (0)