Technical Beauty — Episode 34
Open the sudo CHANGELOG and search for the word "security". Make a cup of tea first. The list is rather long for a tool whose entire job is to ask three questions: who are you, what would you like to run, and may you.
In July 2015, Ted Unangst grew tired of negotiating with the sudo configuration on OpenBSD and wrote his own. He called it doas: dedicated OpenBSD application subexecutor. It was imported into the OpenBSD CVS tree on 16 July 2015 and shipped as the default privilege-escalation tool in OpenBSD 5.8 in October 2015, replacing the sudo package that had been the standard until then. The codebase today is roughly 1,100 lines of C plus a small yacc grammar.
A Short Origin Story
Ted Unangst's stated reason was personal. The default sudo configuration on OpenBSD had a "safe environment" rule that decided which shell variables were safe to forward to the elevated process. The rule kept being revised in response to new attack classes, and Unangst found himself unable to run pkg_add or build a flavoured port because some variable he depended on had been excised from the environment that month. He wrote doas not as a research project but as a private remedy. It was small enough that the OpenBSD project accepted it as a base-system replacement when he proposed it.
The name is a deliberately small joke: "do as", in the sense of "do as user". The expansion ("dedicated OpenBSD application subexecutor") was reverse-engineered after the fact.
The Code, in Numbers
Pulled from the OpenBSD master tree at openbsd/src/usr.bin/doas:
-
doas.c(the main program): 498 lines -
parse.y(yacc grammar for the configuration file): 354 lines -
env.c(environment-variable handling): 235 lines -
doas.h(header): a few dozen lines - Two manual pages, one Makefile
Roughly 1,100 lines of C and yacc, total. The whole thing fits on the screen of a modern editor without scrolling much.
The sudo project, by contrast, is a multi-package codebase in the high tens of thousands of lines, including its own logging, plugin loader, policy engines, environment management, audit hooks, LDAP back-end, SSSD integration, and a yacc grammar for sudoers that has its own override semantics. Each line was reasonable when added. Each line is also part of the surface that has to be defended.
The Design
The configuration file is /etc/doas.conf. There is no /etc/doas.d/ directory of fragments. There is no Include directive. There is one file, with one rule per line. The grammar in Backus-Naur form fits on a single page of the manual:
permit | deny [nopass] [keepenv] [setenv {variable=value, ...}] identity [as user] [cmd command [args [...]]]
A typical OpenBSD machine has three or four lines:
permit nopass keepenv :wheel
permit nopass keepenv root as backup cmd /usr/bin/rsync
deny :backup
That is enough to express: members of the wheel group may do anything; the backup user may run rsync only, and only as the root user; nobody else has privilege at all. The default behaviour, with an empty or absent config, is that nobody can run anything as anyone. The privilege does not appear by accident, and the absence of a rule is the safest possible state.
The whole grammar has perhaps fifteen distinct keywords. There is no concept of Defaults, no concept of plugins, no concept of policy back-ends. The configuration says what is permitted and what is denied, and that is the whole vocabulary.
The Elegance
Doas does not load PAM modules. It does not query LDAP. It does not call out to a plugin system that may or may not be installed. It does not parse a config file with five conditional contexts and override semantics. It reads doas.conf top to bottom, applies the first matching rule, and exits.
The runtime environment is similarly direct. By default doas creates a fresh environment: HOME, LOGNAME, PATH, SHELL, and USER are set from the target user's account; DISPLAY and TERM are inherited from the caller; DOAS_USER is set to the name of the user invoking doas. Everything else is wiped. The keepenv keyword keeps the caller's environment minus a small denylist. The setenv keyword sets specific variables explicitly. There is no clever heuristic to exploit, because there is no clever heuristic.
The implementation in usr.bin/doas/ is similarly direct. The configuration is parsed by a yacc grammar of around three hundred lines (parse.y). The rule-matching is a linear scan. The credential drop and the exec are textbook. There is no function in the codebase whose behaviour depends on three different runtime flags. There is, in particular, no string-parsing function that has to handle escape sequences across nested quoting contexts. The class of bug that produced Baron Samedit in sudo would have to be grown from scratch in doas, and there is nowhere obvious to grow it.
The Sudo CHANGELOG
The proof of the design is empirical. Sudo, despite being maintained by careful engineers under active scrutiny, has accumulated a respectable list of high-severity CVEs over its lifetime. Several were exploitable in the default configuration:
CVE-2021-3156 (Baron Samedit, January 2021): a heap-based buffer overflow in the de-escape loop of
sudoedit -s. The bug had been quietly present since commit 8255ed69 in July 2011. Any unprivileged local user could obtain root on a default sudo installation across all major Linux distributions, BSDs, AIX, Solaris, and macOS. Disclosed by Qualys.CVE-2019-18634 (Pwfeedback, October 2019): a heap overflow triggered by long input when the
pwfeedbackoption was enabled. Several distributions enabled it by default for cosmetic reasons.CVE-2023-22809 (sudoedit, January 2023): arbitrary file write via crafted
EDITORorVISUALenvironment variables when sudoedit was invoked.CVE-2025-32462 and CVE-2025-32463 (June 2025): vulnerabilities in sudo's host-option handling and chroot support, both of which are features that doas does not implement at all.
In the same period, the upstream OpenBSD doas in base has carried no comparable critical CVE. There has been no Baron-Samedit-equivalent in doas, because there is no equivalent code path. There has been no pwfeedback CVE in doas, because there is no pwfeedback. There has been no plugin-loading CVE in doas, because there are no plugins. The CHANGELOG is short because the surface is small.
This is not because the doas authors are smarter. It is because doas has fewer places to be wrong. A tool that does less has less to break.
On FreeBSD
A FreeBSD administrator gets doas as a port. pkg install doas pulls in security/doas, a portable fork maintained by slicer69 that tracks the OpenBSD upstream and runs on FreeBSD, Linux, NetBSD and illumos from the same source tree. The configuration file lives at /usr/local/etc/doas.conf rather than /etc/doas.conf, following the FreeBSD ports convention of keeping third-party configuration outside the base-system /etc. The grammar is identical to the OpenBSD original.
A FreeBSD host that would otherwise install sudo from security/sudo can install doas instead and pay a tenth of the binary size, a hundredth of the source surface, and none of the historical CVE list. The two ports coexist; the choice is the admin's. For new builds where the question is open, doas is the calmer answer.
On Linux
Linux distributions do not ship doas in base. A portable fork called OpenDoas, originally maintained by Duncaen, is packaged as security/doas (or doas in package form) on Debian, Ubuntu, Arch, Alpine, Void, and others. The configuration file syntax is identical to OpenBSD's. An administrator who has used doas on OpenBSD for a decade can install OpenDoas on Linux and edit the same /etc/doas.conf lines.
OpenDoas has had its own modest CVE list, smaller in severity than the sudo equivalents but worth naming for accuracy:
-
CVE-2019-25017 (also referenced as CVE-2019-25016): rules permitting any command inherited the caller's
PATHrather than resetting to a default, allowing PATH-poisoning escalation. Patched in OpenDoas 6.8.1. - CVE-2023-28339: a TIOCSTI ioctl issue allowing command injection into a privileged user's terminal under specific conditions. Patched.
The OpenDoas maintenance cadence is slower than upstream OpenBSD, but the design surface is small enough that audits remain tractable.
The Linux ecosystem also has a memory-safe alternative: sudo-rs, written in Rust, originally funded by the Internet Security Research Group's Prossimo project, jointly developed by Tweede Golf and Ferrous Systems from December 2022, with first stable release in August 2023. The project graduated to its long-term home at the Trifecta Tech Foundation in June 2024. In October 2025 it became the default sudo in Ubuntu 25.10. Two of the 2025 sudo CVEs (CVE-2025-32462 and CVE-2025-32463) affected features sudo-rs does not implement, and sudo-rs was therefore not vulnerable: a different demonstration of the same point doas was making in 2015, with a different language and a different author.
The Point
The reason this episode is doas, and not sudo, is not that sudo is poorly written. Todd Miller has maintained sudo with care since 1994; the codebase is comprehensible, the tests are real, the CVE responses are timely. But sudo carries the accumulated weight of three decades of feature requests, distribution-specific patches, plugin systems, environment-variable rules, LDAP back-ends, audit logging integrations, and policy-language extensions. Each was reasonable when added. Each is now part of the surface that has to be defended.
Doas chose a different starting point. The starting point was: what is the smallest tool that lets a wheel-group member run a command as root? From that starting point, the answer is around 1,100 lines, and it has held its ground for over a decade.
A tool that does less has less to break. The CHANGELOGs are the empirical case.
Read the full article on vivianvoss.net →
By Vivian Voss — System Architect & Software Developer. Follow me on LinkedIn for daily technical writing.

Top comments (0)