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:
- No credentials from the caller are inherited. The command runs in a fresh service forked from PID 1.
- Authentication goes through polkit, so the prompt is separate from your terminal where possible.
- The command gets its own pseudo-TTY.
- No SetUID/SetGID bits are used. The man page points at
NoNewPrivileges=insystemd-system.confas a use case.
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
# Members of group svc may restart the web stub as root, no password.%svc ALL=(root) NOPASSWD: /usr/local/bin/restart-webAlways 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 OKdoas.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-webOne line. Groups take a leading colon. Check it with doas -C:
doas -C /etc/doas.conf && echo config okA broken line fails loudly. In the kit, a deliberately bad file prints doas: syntax error at line 1.
polkit rule for run0
// 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:
- Latest sudo release: 1.9.17p2 (July 2025), which is what runs on my desktop here.
- Latest sudo-rs release: v0.2.15, from the sudo-rs GitHub releases page.
- Latest OpenDoas tag: v6.8.2, which is what Debian trixie ships.
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
| Feature | sudo | doas (OpenDoas) | run0 |
|---|---|---|---|
| Setuid binary | yes | yes | no |
| Needs systemd + polkit | no | no | yes |
| Pin one command | yes | yes (cmd, args) | not via polkit details (see above) |
| Password cache | timestamp_timeout | persist | polkit auth caching, -v, -k, -K in v262 |
| Safe file editing | sudoedit | none | none |
| Logging | syslog, plugins, I/O logs | syslog | journal (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
- It needs systemd, PID 1 as init, and polkit. No systemd means no run0. Alpine and Void boxes are out.
- It has no sudo-style environment. The command runs with the service manager’s environment, not yours.
TERMis copied from the caller, and you pass anything else with--setenv=NAME. A script that relied onsudo -Ewill break. - 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-passwordskips the prompt, which means it fails instead of waiting for a password nobody can type. - 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.
- No command pinning. Covered above. For delegated rules, run0 is the wrong tool.
--empoweris dangerous. It gives the process all capabilities and adds theempowergroup. 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.