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 linesparse.y(yacc grammar for the configuration file): 354 linesenv.c(environment-variable handling): 235 linesdoas.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
doas 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.
Ted Unangst wrote doas in July 2015 because the sudo configuration on OpenBSD had become tiresome. Imported 16 July 2015; default in OpenBSD 5.8 in October 2015. Around 1,100 lines of C plus a small yacc grammar; one file
/etc/doas.conf, one rule per line, a grammar that fits on a postcard, no PAM, no LDAP, no plugin loader, no/etc/doas.d/. Sudo's CHANGELOG carries Baron Samedit (CVE-2021-3156, hidden since 2011), Pwfeedback (CVE-2019-18634), sudoedit arbitrary file write (CVE-2023-22809), host-option and chroot (CVE-2025-32462/32463). The upstream OpenBSD doas, in the same period, has carried no comparable critical CVE. The CHANGELOGs are the empirical case.