It’s been a year since the introduction of doas, so it’s clearly time to write a book. Or maybe a pamphlet.
UNIX systems have two classes of user, the super user and regular users. The super user is super, and everybody else is not. This concentration of power keeps things simple, but also means that often too much power is granted. Usually we only need super user powers to perform one task. We would rather not have such power all the time. Think of the responsibility that would entail! Like the sudo command, doas allows for subdivision of super user privileges, granting them only for specific tasks.
The doas command itself has a few options, which we’ll discuss somewhat later, but the most interesting part is the configuration file. This is where the real magic happens.
The simplest possible doas.conf config really is quite simple. Don’t blink or you’ll miss it.
The empty config doesn’t do anything, but allows us to discuss the default rule set. There is none. doas starts evaluating rules in the DENY state. If no rules match, then the action is denied. That’s right. Even root will receive a permission denied error. This is not particularly useful, per se, but one can be assured that reading the installed doas.conf file is sufficient to fully understand the system behavior.
If there’s no configuration file at all, doas will instead exit after printing a message that it is not enabled.
The simplest useful configuration probably looks more like this.
This lets any user in the wheel group run any command as root. It’s approximately equivalent to the su command. For a more complete emulation, we’d like to allow root to run commands without a password.
We add the root rule second because doas evaluates rules in a last match manner. root is in the wheel group, so the first rule will match, and then we need to override that with a second rule. Remember to always start with general rules, then make them more specific.
Why even run doas as root? Because sometimes you’d like to switch to a less privileged user. Or you have a script which uses doas to elevate privileges for an important operation, but you’re already root when you run it.
The default behavior of doas is to require authentication every time the user runs a command. Normally this means entering their password. This can become tedious if multiple commands are executed. There are two keywords that can be added to a doas.conf rule to alter this requirement.
Adding the nopass keyword to a rule means the user is always permitted to run the command without entering a password. We’ve seen this above in the rule for root. The user is already root, so they can do anything they like and there’s no reason to require a password.
By adding the persist keyword, doas will remember that the user authenticated previously and not require further confirmation for a timeout of five minutes.
This rule recreates the common sudo configuration of requiring a password for wheel users the first time a command is run.
The authentication information doas uses is recorded in the kernel and attached to the current session. Unlike filesystem tickets, it is not accessible to other users and difficult to fake. The timeout will always take place in real time, not computer time, meaning that adjusting the system clock backwards can not grant new life to an expired ticket. Repeated executions will reset the timeout, but only if the rule is marked persist. Rules cannot be both persist and nopass, nor will nopass rules extend the timeout.
If you have multiple shell logins to a machine, each login will require authentication. Additionally, the authentication information includes the parent shell process ID. This means that executing doas again in a shell script will require authentication. Or, to repeat that another way, if you run a script or program of uncertain quality, it won’t be able to silently elevate privileges. (That’s the theory anyway. In practice the check leaves quite some wiggle room.)
Authentication checks are only skipped for rules marked persist. To prevent mistakes, important commands can be relisted without persist to always require a password.
The -L command line option to doas is analagous to logout and immediately deletes any persisted authentication information in this terminal without waiting for the timeout.
There are two basic ways information is provided to executed commands. The first and most obvious is the command line. The second and less visible way is via environment variables. Despite being mostly invisible, environment variables can have dramatic effects on a program’s behavior. Therefore it is important that doas provide some filtering to prevent unintended consequences. By default, only a short list of variables is copied.
There are two config keywords related to environment, keepenv and setenv. The first is very simple. We’ve seen it before, in the rule for root above. keepenv simply means that the entire environment should be retained and passed as is to the executed command. This is a convenient shortcut for trusted users.
Using setenv is a little more complicated, because it allows a combination of adding, modifying, and deleting environment variables. Let’s look at an example.
This rule allows zoltan to run commands as root. The PKG_PATH environment is retained. The ENV environment, which points to a configuration file for ksh, is redirected to one owned by root. Finally, we override the PS1 setting which controls the shell’s prompt display. We don’t specify the new value in this configuration file, but instead use whatever value is in DOAS_PS1. This lets zoltan adjust the root prompt as desired.
users and targets
Each rule in doas.conf applies to an individual or group, specified after permit and any options. There’s no keyword to distinguish between users and groups. Instead, syntax similar to chown is used, with user names by themselves or :group names with a leading colon. Now is a good time for a reminder that rules are always evaluated in order with the last matching rule winning. Specific user rules are not automatically preferred over group rules unless they come later in the file.
In most situations, doas is used to run commands as root. This requires no additional syntax. However, we may also wish to restrict some rules to targeting certain users.
This rule will let zoltan run commands as the database administrator without entering a password, but by itself doesn’t permit zoltan to run anything as root.
We’re nearing the end of our tour of the doas.conf syntax. doas can also be restricted so that rules only apply for certain commands, or even certain commands with particular arguments.
Normally, reboot requires root privileges. It is instead executed indirectly by the setuid program shutdown, whose execution is restricted to the operator group. The above rule allows these users to run reboot directly. However, operators won’t be able to run other commands as root or obtain a shell.
Here we allow zoltan to rerun the netstart script that configures network interfaces. We don’t give zoltan permission to run any shell command, only the netstart script.
In both of these examples, the cmd was specified with only the base name. In these cases, doas will restrict itself to only executing commands in the system PATH (/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin). zoltan won’t be able to install a sh binary in his home directory and set PATH to ~/bin to subvert our intentions. Absolute pathnames can also be specified, however the user will also be required to type them out in full.
Any command arguments specified must be specified in their entirety.
These two rules will allow zoltan to turn the wifi interface on and off, but not change any of its other parameters.
Some userland utilities that gather information from the kernel only present a restricted subset of information when as regular users. To see the full information requires running as root. For example, fstat will only print minimal information about unix domain sockets.
But when run again as root, we see much more information.
This allows us to match these sockets up with the process on the other end.
These kernel addresses are normally hidden because they reveal information about the kernel’s memory layout which can be used to facilitate exploits, but if we trust tedu (and who doesn’t, really?) then we can change this with a simple config rule.
Using fstat, one can always see the open files of other users’ processes, but we specify arguments here to prevent tedu from matching up connections between processes.
In contrast to all the permit rules we’ve seen so far, it’s also possible to create a deny rule that specifically denies command execution. This feature is most useful as a safeguard against accidental typos by trusted operators. It should not be used as a security feature because an exhaustive blacklist is exhausting to create. Better to craft a ruleset that doesn’t grant unintentional privileges.
Assuming that zoltan is in group wheel, we’ll let him run any command. Just not reboot. Maybe zoltan is a little trigger happy and has a habit of typing into the wrong terminal. This ruleset won’t actually prevent zoltan from rebooting the machine by any means; the first rule grants sufficient privilege to edit doas.conf and remove the second rule, among many other possibilities. The assumption is that everybody is on the same team.
The doas command itself has a few options.
Since we just finished looking at the config file syntax, the -C option can be used to syntax check a new file before installing. It also permits checking the result of rule set evaluation for a given command and arguments without actually running the command. Continuing with the fstat example from earlier, we can check that only the command with arguments is matched.
When writing possibly noninteractive scripts that incorporate doas, the -n option is helpful to prevent future failures. Only nopass rules will successfully execute. Any rule that requires a password will instead immediately fail. This includes any rules with the persist keyword, regardless of whether the user recently authorized.
It would not have been possible to finish doas without the support of many other OpenBSD developers and users. In particular, Vadim Zhukov contributed immensely to the config parser and regress testsuite; Todd Miller, Damien Miller, and Martijn van Duren provided ideas and inspiration; Theo de Raadt provided backup to rejecting feature requests; Henning Brauer gave me the idea for tying authorization persistence to the terminal; and I owe Michael Lucas for stealing a catchy title.