Skip to content
Go back

sudo vs doas vs run0 on Linux

By KingPin 12 min read
sudo vs doas vs run0 on Linux
Contents

Three Ways to Become Root, One of Them Has No Setuid Bit

You type sudo forty times a day and never think about it. Then a CVE lands, and you remember that sudo is a setuid-root binary with a config language older than some of your coworkers. That is a lot of trust for a tool you stopped reading about in 2014.

My verdict, so you can stop reading early. On a home-lab server, keep sudo (or sudo-rs if your distro ships it) and write tight rules. On a desktop that already runs systemd, try run0, because it removes the setuid binary entirely. Use doas when you want a config file you can read in ten seconds, or when you run a small system without systemd. The rest of this post is the evidence, as of November 2026.

Full example: Clone the working files at github.com/KingPin/sumguy-examples/linux/sudo-vs-doas-vs-run0

The kit has a Dockerfile, the sudoers and doas configs, a polkit rules file, and a test.sh that proves the sudo, sudo-rs, and doas rules behave. The run0 half cannot run in a container, and I say why below.

How Each One Actually Works

sudo: a setuid binary that reads a policy

/usr/bin/sudo is owned by root with the setuid bit set. When you run it, the kernel starts it with root’s effective UID. It reads /etc/sudoers and /etc/sudoers.d/*, decides if you may proceed, authenticates you through PAM, and then forks your command as the target user. Everything happens inside a process that started life with your untrusted arguments, environment, and terminal. That is the core design risk.

doas: the same idea with the features cut

doas comes from OpenBSD. It is also a setuid binary, but its parser and policy engine are small. OpenDoas is the Linux port. Debian trixie packages it as doas (and opendoas), at version 6.8.2. It reads /etc/doas.conf, authenticates through PAM, and runs your command. It has no plugin system and no sudoedit-style editing.

run0: no setuid, PID 1 does the work

run0 ships inside systemd (ls -l /usr/bin/run0 shows a symlink to systemd-run). It never gains privilege itself. It asks the service manager over D-Bus to start a transient service unit as the target user, and the manager asks polkit whether that is allowed. The man page lists the differences from sudo:

Your shell never becomes root. A root service is created, and your terminal is attached to it.

Same Job, Three Config Files

The job: let members of group svc restart one thing as root without a password, and nothing else. These are the exact files from the kit, and the test run validated the sudo and doas ones.

sudoers drop-in

/etc/sudoers.d/50-svc
# Members of group svc may restart the web stub as root, no password.
%svc ALL=(root) NOPASSWD: /usr/local/bin/restart-web

Always check with visudo -c. A syntax error in a live sudoers file is how you lock yourself out at 2 AM. The kit’s test run prints:

PASS visudo -c validates the drop-in
/etc/sudoers: parsed OK
/etc/sudoers.d/50-svc: parsed OK
/etc/sudoers.d/README: parsed OK

doas.conf

/etc/doas.conf
# Members of group svc may run the web restart stub as root, no password.
permit nopass :svc as root cmd /usr/local/bin/restart-web

One line. Groups take a leading colon. Check it with doas -C:

Terminal window
doas -C /etc/doas.conf && echo config ok

A broken line fails loudly. In the kit, a deliberately bad file prints doas: syntax error at line 1.

polkit rule for run0

/etc/polkit-1/rules.d/50-run0-homelab.rules
// Members of group svc may restart nginx.service without a password.
// This matches the polkit check systemctl triggers. It does not scope run0.
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("unit") == "nginx.service" &&
action.lookup("verb") == "restart" &&
subject.isInGroup("svc")) {
return polkit.Result.YES;
}
});

Read the comment in that file twice, because this is the biggest gotcha in the whole comparison. The action org.freedesktop.systemd1.manage-units is real (I confirmed it in the local policy file, where it defaults to auth_admin_keep for an active local session). But run0 does not run “a restart of nginx”. It starts a brand new transient unit that runs whatever command you typed. The polkit check for that request is “start a transient unit with a generated run-*.service name” (systemd 262 prompts with “Authentication is required to start transient unit ’$(unit)’.”). The details polkit sees are the generated unit name and the verb start. The command line is not among them, and the caller can pick the unit name with --unit=. So no rule can say “this group may run only this command via run0.”

What the rule does cover is plain systemctl restart nginx.service for the group, with no password. If you hand the group a blanket YES on manage-units to make run0 passwordless, you have given them root. sudo and doas can pin a command. run0 cannot, today, and that is the main reason I would not use it as a command-scoped delegation tool.

Attack Surface: Where the Bodies Are Buried

sudo is the big one. It has a large feature set (plugins, sudoedit, chroot, per-host rules, LDAP), and every feature is code that runs as root and parses attacker-influenced input.

The recent proof: CVE-2025-32463. An attacker could abuse the -R (--chroot) option to run arbitrary commands as root even without a sudoers entry. The sudo project’s advisory says versions 1.9.14 through 1.9.17 inclusive were affected, and it was fixed in 1.9.17p1. The cause was a 1.9.14 change that resolved paths using the user-supplied chroot while sudoers was still being evaluated. The advisory also marks the chroot feature as deprecated, with removal planned.

A few facts for calibration, as of November 2026:

doas has less code and fewer features, so it has fewer places to hide a bug. I will not give you line counts, because I did not measure them. Less surface is an argument, not a guarantee.

sudo-rs is the memory-safe rewrite in Rust. Ubuntu made it the default sudo in 25.10, keeping the original available as sudo.ws. Ubuntu 26.04 LTS ships sudo-rs too, as its own security notices show. The Rust version removes a class of memory bugs. It does not remove logic bugs, and sudo-rs has shipped its own security fixes: Ubuntu’s USN-8708-1 (September 2026) covers a sudoedit time-of-check/time-of-use flaw in sudo-rs 0.2.13 that could lead to privilege escalation, on systems that grant fine-grained sudoedit permissions.

The kit installs Debian’s sudo-rs 0.2.5 and runs the same sudoers drop-in through it. It passes without edits. Not every sudoers feature is supported in sudo-rs, so read its docs before swapping on a box with complicated rules.

run0 removes the setuid binary entirely, so nothing on disk starts as root on your behalf. The price is that you now trust PID 1, D-Bus, and polkitd with the decision. Polkit rules are JavaScript, which is a stranger place to write security policy than a one-line config.

What Each One Lacks

Featuresudodoas (OpenDoas)run0
Setuid binaryyesyesno
Needs systemd + polkitnonoyes
Pin one commandyesyes (cmd, args)not via polkit details (see above)
Password cachetimestamp_timeoutpersistpolkit auth caching, -v, -k, -K in v262
Safe file editingsudoeditnonenone
Loggingsyslog, plugins, I/O logssyslogjournal (the unit is in the journal)

doas

persist works in OpenDoas, and doas -C accepted permit persist :svc in my container test. There is no sudoedit. To edit a root-owned file you either run an editor as root or copy the file out, edit, and copy it back. Environment handling is stricter by default. You can allow variables with setenv.

sudo

You get everything: sudoedit (which runs the editor as you and only writes the file as root), I/O logging, per-command rules, and a decade of Stack Overflow answers. You also get the sprawl. Wildcards in sudoers argument lists are where people hurt themselves, and I covered that in the sudoers mistakes post. One note: reports say sudo-rs handles wildcard arguments differently from sudo, so recheck old rules after a swap.

run0

The niceties are real. man run0 says the terminal background tints red when operating as root, and yellow for other privileged users, and you can set your own with --background=44 for blue. The default prompt prefix is a superhero emoji, controlled by --shell-prompt-prefix or SYSTEMD_RUN_SHELL_PROMPT_PREFIX. You can see at a glance that you are root, which prevents the classic mistake of running a destructive command in the wrong shell.

The version on my machine is systemd 262, which adds -v (ask polkit for temporary authorization without running a command), -k (revoke it for this terminal), -K (revoke all for this session), and -n/--non-interactive (a short alias for the older --no-ask-password).

Where run0 Bites

  1. It needs systemd, PID 1 as init, and polkit. No systemd means no run0. Alpine and Void boxes are out.
  2. It has no sudo-style environment. The command runs with the service manager’s environment, not yours. TERM is copied from the caller, and you pass anything else with --setenv=NAME. A script that relied on sudo -E will break.
  3. TTY behavior changes. The man page says that if stdin, stdout, and stderr are all TTYs, run0 allocates a pseudo-TTY. Otherwise the file descriptors pass straight through. A cron job or a piped command gets different behavior than an interactive shell. --no-ask-password skips the prompt, which means it fails instead of waiting for a password nobody can type.
  4. SSH sessions. I did not test run0 over SSH on a live login, since this build box has no TTY and no password. Polkit authentication needs an agent to ask for the password. Check that your SSH session can prompt before you rely on it on a headless server.
  5. No command pinning. Covered above. For delegated rules, run0 is the wrong tool.
  6. --empower is dangerous. It gives the process all capabilities and adds the empower group. The man page warns that other processes of the same user have privileges over it.

The Verdict

Home-lab server: sudo or sudo-rs with a short drop-in per job. You need command pinning, logging, and sudoedit more than you need to shave off a setuid binary. Patch fast, but know its limit: CVE-2025-32463 sat in releases 1.9.14 through 1.9.17 for about two years (June 2023 to June 2025). Patching only helps once a fix exists, so the features you never use (like -R) are attack surface you carry for free. If you run only a few admin accounts and a few one-command delegations, doas is a fine, smaller replacement. The one-line rule above does the same job as the sudoers drop-in.

Desktop with systemd: try run0 for interactive root shells. The red background is worth it alone, and the setuid binary goes away. Keep sudo installed for scripts until you have proven nothing depends on it. Using run0 for interactive work and sudo for automation is a reasonable split, like using the forklift for pallets and a hand truck for the couch.

Do not rip out sudo on a server because an article told you to. Test the replacement with the kit first, and keep a root console path (a local TTY or the hypervisor) in case you lock yourself out.

Common Questions

Is doas safer than sudo?

Doas has a smaller attack surface because it has far fewer features (no plugins, no sudoedit, no chroot option), so fewer code paths run as root. It is still a setuid binary, so a parser bug in doas still runs as root. Keep doas patched and write narrow rules, the same as with sudo.

Does run0 work over SSH?

Run0 can work over SSH if the session has a TTY and a polkit agent that can prompt for a password. I did not test this on a live host. Without a TTY, run0 passes file descriptors straight through, and with --no-ask-password it fails instead of prompting.

Can I remove sudo and use only run0 or doas?

Yes, on most systems, but test first. Run0 requires systemd and polkit, so it fails on non-systemd distros. Doas lacks sudoedit and plugins. Before removing sudo, confirm your scripts, Ansible become settings, and installers do not call sudo, and keep a local console available.

Is sudo-rs the default in Ubuntu?

Yes. Ubuntu 25.10 made sudo-rs the default sudo command, with the original C sudo still installable and callable as sudo.ws. Ubuntu 26.04 LTS also ships sudo-rs. Debian trixie still ships the original sudo by default and offers sudo-rs as a separate package.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
K8s Requests vs Limits in a Home Lab
Next Post
Self-Hosted Excalidraw, Minus the Cloud

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts