While the proper use of groups can almost eliminate the need for root access to edit files, that won’t help with commands that can be run only by root. You could set up a cron job to reload the name server each day at midnight, but every piece of software occasionally needs a manual restart. Because root is an all-or-nothing affair, people who have one minor task to perform have traditionally needed the root password.
OpenBSD includes sudo(8)
and its associated tools, which implement fine-grained access control for commands that can be run only by particular users. When configured properly, sudo
lets normal users run specific programs as other users, including root. Configured improperly, sudo
permits full root access. I’ll explain a basic sudo
setup that covers almost all uses, but remember that many more combinations are possible. And don’t be afraid to read sudo(8)
, sudoers(5)
, and the documentation at the sudo
home page (http://www.gratisoft.us/sudo/).
The sudo
tool provides benefits beyond fine-grained privilege control. Every command run via sudo
is logged, making it very easy to track who did what. The senior sysadmin can change the root password and not give it out, even to people who have root-level access.
The sudo
configuration file is designed to be shared across multiple systems, so one sudo
policy can cover your entire network and every operating system. Admittedly, you’ll have trouble using a single sudo
configuration on operating systems with wildly unique directory layouts, such as Mac OS X, but you can easily share one configuration among OpenBSD, other BSDs, Linux, and even OpenSolaris or AIX.
The most common problem with sudo
is getting your users to accept it. People who have historically had access to the root account think they “lose something” by working through sudo
.
The key to overcoming this is to give users access only to what’s required to perform the tasks for which they’re responsible. A junior administrator who complains about insufficient privileges has either overreached his responsibilities or needs more privileges. One sure way to discover what people actually do is to implement a minimal sudo
permissions scheme and wait for complaints. If no one complains, they’re not working very hard.
The configuration syntax for sudo
can be confusing because its configuration doesn’t closely resemble any other configuration file, and getting everything right can be difficult at first. The configuration file is actually well suited to its purpose, however. Once you understand it, adjusting privileges is quick and easy.
More seriously, a faulty sudo
setup can create the appearance of security while leaving gaps for a user to become root. Be sure to test sudo
every time you make a change, and avoid the common configuration mistakes I document here.
Some users will do their best to push the limits of their access, for no other reason than to see if they can outsmart you. These users are best managed with a combination of careful configuration, administrative policy, and a cricket bat.
The sudo
program is a setuid
root wrapper that can run commands as any other user. Use sudo
by giving it the command you want to run.
$ sudo /etc/rc.d/named restart
The sudo
software compares the desired command (in this case, /etc/rc.d/named restart
) to its internal list of permissions and privileges. If the configuration file allows that particular user to run that command as root, sudo
runs it as root. And, because root can run any command as any user, sudo
can also run commands as any arbitrary system user. You can use this fact to grant any user the ability to run specific commands as chosen users; for example, administrators of certain database servers must frequently run commands as the database user.
The sudo
software is a suite with four pieces. The first piece is the actual sudo(8)
command, the setuid
root wrapper. The second is the configuration file /etc/sudoers, which describes who may run which commands as what user. (/etc/sudoers is fully documented in sudoers(5)
.) Third is the visudo(8)
command that opens /etc/sudoers in an editor and checks the configuration file syntax before exiting. Finally, the sudoedit(8)
program is specifically for editing files as another user.
If /etc/sudoers contains incorrect syntax, sudo
will not run. If you rely on sudo
to provide root-level access to the system and you break your sudoers file, you’ll lock yourself out of the root account and lose the ability to correct your error. That is bad.
Fortunately, the visudo(8)
program provides some protection against this sort of error by locking /etc/sudoers so that only one person can edit the configuration at a time. It then opens /etc/sudoers in a text editor (vi by default, but it respects the $EDITOR
environment variable). Make your changes and save your work. When you exit the editor, visudo
should parse the file for syntactic correctness.
If visudo
detects an error, it prints out the offending line number and asks what you want to do.
>>> /etc/sudoers: syntax error near line 34 <<< What now?
Here, I’ve made an error near line 34. I can reedit the file to fix the error, quit without saving any changes, or force visudo
to accept this file.
Press the E key, and visudo
should return you to the editor. Go to the offending line, fix your error, save the file, and exit the editor again.
Enter the X key, and visudo
should quit and revert the configuration file to its original state. Your changes will be lost, but that might be acceptable. It’s better to have an old, working configuration than a new, broken one.
Pressing Q forces visudo
to accept the file, busted syntax and all. If sudo
can’t parse /etc/sudoers, it will immediately exit. Essentially, you’re telling visudo
to break sudo
until you log in as root to fix the problem. If you think you understand /etc/sudoers better than visudo
does, you’re probably wrong. Even if you’re right, you’re wrong.
The visudo
program doesn’t guarantee that the configuration will do what you desire, only that the configuration parses and is valid. A properly formatted configuration that declares “No one may do anything via sudo
” is perfectly acceptable to visudo
.
The /etc/sudoers file determines who may run which commands as which users. Never edit /etc/sudoers directly, even if you think you know exactly what change you want to make. Always use visudo
to change /etc/sudoers.
The various sudoers sample configurations you’ll find are usually very complicated, as they demonstrate all the nifty things sudo
can do. At this point, however, you want to do only simple, boring things, like giving particular users access to run specific commands. And the bare syntax is very simple. Every sudoers rule follows this format:
Username host=command
The username
is the username of the user who may execute the command, an alias for usernames, or a system group.
The host
is the hostname of the system this rule applies to. You can share /etc/sudoers across multiple systems. This entry permits per-host rules.
The command
space lists the commands this rule applies to. You must list the full path to each command, or sudo
will not recognize it. If this weren’t a requirement, some untrustworthy soul could just adjust his $PATH
to access renamed versions of commands.
For example, suppose I trust user sbaxter
to run any command, on any system, as root. I use the keyword ALL
to match all possible options for host and command:
sbaxter ALL=ALL
As the lead sysadmin, I should know which duties I have assigned sbaxter
, and exactly which commands he needs. Suppose sbaxter
is my DNS minion. I control the actual editing of zone files with group permissions, but there are many legitimate occasions for him to stop, restart, or otherwise slap around the name server program. I want him to use the system script /etc/rc.d/named for this task, and this sudoers entry gives him permission to use the script on all machines.
sbaxter ALL=/etc/rc.d/named
If I share this file across several machines, it’s likely that many of those machines don’t even run a name server. To restrict my minion’s access to only the DNS server, I’ll change the host field.
sbaxter dns1=/etc/rc.d/named
Then again, sbaxter
is the administrator of the email server mail1
. This server is his responsibility, so he needs to run any command. I can set entirely different privileges for him on the mail server and still use the same sudoers file on all the systems.
sbaxter dns1=/etc/rc.d/named sbaxter mail1=ALL
Yes, sbaxter
can use visudo
on mail1
, but he already has full privileges on that machine. I’m comfortable with this, as he knows I’ll hold him responsible for any downtime.
Separate multiple entries in a single field with commas. For example, after a while, I get tired of sbaxter
asking me to mount NFS shares on the DNS server, so I add mount_nfs
to his privileges.
sbaxter dns1=/etc/rc.d/named,/sbin/mount_nfs
He can now mount his own blasted NFS shares and leave me alone.
Specify a username in parentheses before a command to say that the user can use sudo
to run commands as a particular user. For example, my user dwsmith
is a database administrator and needs to run any command as the user _postgresql
on the database server db1
.
dwsmith db1 = (_postgresql) ALL
The _postgresql
user can’t successfully run critical system programs like fdisk
and newfs
, but it can restart the database, back it up, and perform other database-administration tasks. By choosing a specific user, a specific machine, and a specific command, you can define arbitrarily complex sudoers policies.
If you have several commands, usernames, or hosts on a line, that line might become uncomfortably long. Use a backslash () to indicate that a rule continues on the next line.
sbaxter dns1=/etc/rc.d/named,/sbin/mount_nfs, /sbin/reboot, /sbin/dump
Use as many lines as you like to make your sudoers file easier to manage.
Take several machines with different roles, add multiple sysadmins with differing privilege levels, and your /etc/sudoers file will quickly become complicated. When you have a few users with identical privileges and long lists of commands that you would like them to access, maintaining consistency in each user’s privilege list becomes tedious. Aliases simplify these tasks and make /etc/sudoers much more comprehensible, which makes your life easier.
An alias is a group of users, hosts, or commands. You can use aliases anywhere you would normally use users, hosts, or commands. You might, for example, create an alias called DATABASE_COMMANDS
that contains all of the commands your database administrators need to run using sudo
.
Let’s take database administrator dwsmith
and use an alias to specify his commands.
dwsmith db1 = (_postgresql) DATABASE_COMMANDS
This alias might not seem to save us much, but suppose we have several database administrators. We could create an alias called DBAs
that includes all of them.
DBAs db1 = (_postgresql) DATABASE_COMMANDS
Suddenly, this one line represents multiple rules. All of the database admins have identical sudo
privileges, and when you discover that you need to give them access to an additional command, add the command to the alias, and it immediately becomes available to every database admin. There’s no tedious and error-prone copying of entries between users.
You must define an alias before you can use it, so aliases normally appear at the top of the file. Each alias is made up of a label identifying its type, a name, and a list of its items. Alias types include user aliases, run as aliases, host aliases, and command aliases.
A user alias is a group of users, and it is labeled with the string User_Alias
. Put only usernames in this alias.
User_Alias DBAs = dwsmith, kkrusch
Here, the user alias DBAs
contains the users dwsmith
and kkrusch
. By using the alias in my sudoers rules instead of the usernames, I ensure that these users receive exactly the same sudo
privileges.
You can use system groups in user aliases by prefacing them with a percent sign (%
). I might create a group in /etc/groups called databaseteam
, and make dwsmith
and kkrusch
part of that team.
%databaseteam db1 = (_postgresql) DATABASE_COMMANDS
Perhaps the most common usage of this is giving the wheel
group unlimited sudo
access.
%wheel ALL = ALL
This rule permits the wheel
group to run any command as root through sudo
. It doesn’t change the group members’ privileges, but gives them access via sudo
. This is convenient for running single commands.
A run as alias is a list of users that other users can run commands as. For example, on certain application servers, the database admins need to run commands both as the database owner _postgresql
and as the web server owner www
. If the user must run commands as multiple users, however, you would need a separate sudoers entry for each target user.
A run as alias lets you group these accounts:
Runas_Alias APPOWNER = _postgresql, www
You can now write a single rule allowing users to run commands as either _postgresql
or www
.
A host alias is a list of hosts, defined as hostnames, IP addresses, or network blocks. Label host aliases with the string Host_Alias
. Here are examples of all host alias types:
Host_Alias DB = db1, db2, db3 Host_Alias DMZ = 192.0.2.0/24 Host_Alias FIREWALL = 192.0.2.1, 192.0.2.2, 192.0.2.3
A command alias is a list of commands. For example, you might have a command alias that includes all of the commands needed to back up the system or restore from a backup. They’re labeled with the string Cmnd_Alias
.
Cmnd_Alias BACKUPS = /bin/mt, /sbin/restore, /sbin/dump
You can tell a command alias to include everything in a particular directory by using a wildcard.
Cmnd_Alias APPCOMMANDS = /home/appuser/bin/*
You can also list partial command names. For example, most of PostgreSQL’s commands begin with the pg_
prefix. To give a user access to these commands, use a wildcard like so:
Cmnd_Alias APPCOMMANDS = /home/appuser/bin/*,/usr/local/bin/pg_*
If you find yourself writing command aliases that include paths like /sbin/*, stop and reconsider, because you’re essentially giving the user unlimited root access.
Use an alias exactly as you would normally list the user, command, or hostname. In the previous examples, I defined the user alias DBAs
, the run as alias APPOWNER
, the host alias DB
, and the command alias APPCOMMANDS
. Here’s how they might be used:
DBAs DB = ALL
Here, the user group DBAs
can run any command on any server in the DB
group, as any user. The members of the group own the servers, and if they screw them up, it’s not my problem.
Well, this attitude sounds good, but the truth is that when they destroy the server, I must get involved. Even if it’s not my fault that they drove the database server into the ditch, it is my problem. I must lock down the commands that they can run, restricting them to only the commands in the APPCOMMANDS
alias. So, the DBAs
group can now run any command in APPCOMMANDS
on the DB
servers.
DBAs DB = APPCOMMANDS
Then I discover that my database admins are either cleverer or dafter than I thought. They run certain database commands as root, creating log files owned by root. The unprivileged database user _postgresql
cannot write to these log files, and so the application server crashes. Fixing this requires changing the permissions on those log files, but the database admins do not have permission to run chown
. If I give them the ability to change the permissions on arbitrary files, I might as well just give them root access.
To keep this from happening again, I restrict their privileges so they can run their commands only as the application unprivileged users.
DBAs DB = (APPOWNER) APPCOMMANDS
Everyone in the DBAs
group can run any command in APPCOMMANDS
, as any user in APPOWNER
, on any server in DB
. I can change their access by adding entries to the various aliases.
Without aliases, what would this rule look like?
dwsmith,kkrusch db1,db2,db3 = (_postgresql,www) /home/appowner/bin/*,/usr/local/bin/pg_*
That’s ugly, and it does exactly the same thing.
If you name your aliases well, you’ll find rules easier to understand. While these example aliases are fairly short, I’ve used aliases with up to 20 members. The resulting rules are appalling without aliases.
Some of the permissions granted by sudo
in this case are unnecessary. For example, the unprivileged web server user doesn’t need to run the various PostgreSQL utilities, and if www
did try to run the database, nothing much would happen. If you don’t like this, make two separate rules. Either way, it’s tighter security than giving database administrators the root password.
You can include aliases in aliases. Here, I combine two user aliases into a single alias for my application administrators:
User_Alias APPADMINS = DBAs, WEBMASTERS
It’s traditional, but not mandatory, to give aliases names in all capital letters to help differentiate them from users, hosts, and so on. And though it’s valid syntax, it’s best to avoid naming aliases after users or hosts. Here’s an example:
User_Alias MWLUCAS = mwlucas,pkdick,sbaxter,dwsmith
This would quickly drive me batty.[16]
You can also reuse alias names if they are for different types of aliases. For example, the following is perfectly legal, but perfectly offensive.
User_Alias DB = dwsmith,kkrusch Runas_Alias DB = _postgresql,www Host_Alias DB = db1, db2, db3 Cmnd_Alias DB = /usr/local/bin/pg_*, /home/appowner/bin/* DB DB = (DB) DB
If you do this, anyone who must debug your sudo
configuration will curse your name. Even if you consider being cursed a job perk, this naming scheme makes your phone ring at inconvenient times.
You can customize sudo
’s behavior, or its behavior for certain users, hosts, or aliases, with the Defaults
field. For example, one feature of sudo
is that if you enter the wrong password, it insults you.
$ sudo -l
Password:
My pet rat can type better than you!
Password:
I typed my password incorrectly. sudo
insulted me and offered me a chance to enter my password again. If I enter the wrong password three times, sudo
exits.
Insulting the user is just fine in an open source environment, but if you’re in a company, someone will complain to management. You can either go to sensitivity training or proactively disable insults by adding the following line to sudoers:
Defaults !insults
The Defaults
statement indicates that the following item affects one or more sudo
defaults. The insults
option controls insulting the user. The exclamation point (!
) is a negation symbol. By putting an exclamation point in front of the option, you turn off the feature. The system will no longer insult users when they demonstrate that they cannot type as well as my pet rat.
$ sudo -l
Password:
Sorry, try again.
Password:
You can override defaults globally or on a per-alias basis.
To override the defaults on a per-host basis, use an @
symbol after Defaults
and give either a host or a host alias. Here, I want to insult users who can’t type their password on caddis
or on a machine in the alias APPSERVERS
, while leaving insults disabled for all other servers:
Defaults !insults Defaults@caddis insults Defaults@APPSERVERS insults
This lets me enable or disable functions for any combination of servers.
To change sudo
defaults on a per-user basis, use a %
and the user or user alias.
Defaults !insults Defaults%lasnyder insults Defaults%DBAs insults
It doesn’t matter where lasnyder
logs in—I’m going to insult him, as well as the users in the DBAs
alias. But database administrators are used to poor treatment by their software, and to not insult them would confuse and disappoint them.
You can also change how sudo
behaves on a command-by-command basis by putting an exclamation point between Defaults
and the command list.
Defaults !insults Defaults!/sbin/newfs,/sbin/fsck insults Defaults%APPCOMMANDS insults
Anyone who tries to use newfs(8)
or fsck(8)
(discussed in Chapter 8) and cannot type their password needs insulting. The application administration commands might not merit insults, but I can always claim it was an oversight.
Lastly, you can change the defaults based on who the command is being run as. Use a right angle bracket (<
) to indicate changing behavior for a run as alias.
Defaults !insults Defaults<_postgresql insults Defaults<APPOWNER insults
If a user runs a command as _postgresql
, or as any user in the APPOWNER
run as alias, and types his password incorrectly, he gets insulted.
Certain environment variables can cause problems. For example, $HOME
is an obvious one—a user cannot create files in another user’s home directory. Others, such as LD_LIBRARY_PATH
, can cause endless annoyance as well as security issues, as applications try to link against the wrong libraries. The sudo
program can remove suspicious environment variables, completely reset the user’s environment, or be configured to preserve the original user’s environment.
The env_reset
sudoers option is set by default. It purges all environment variables except LOGNAME
, SHELL
, USER
, USERNAME
, and anything beginning with SUDO_
. You can change this behavior by disabling env_reset
, but I strongly recommend against disabling environment purging.
Instead of letting users blindly carry all the random garbage in their environment along with them, create a list of necessary and safe environment variables that they can retain. You’ll see examples in OpenBSD’s default sudoers file using the env_keep
option.
Defaults env_keep +="DESTDIR DISTDIR EDITOR FETCH_CMD FLAVOR FTPMODE GROUP MAKE" Defaults env_keep +="MAKECONF MULTI_PACKAGES NOMAN OKAY_FILES OWNER PKG_CACHE" Defaults env_keep +="PKG_DBDIR PKG_DESTDIR PKG_PATH PKG_TMPDIR PORTSDIR" Defaults env_keep +="RELEASEDIR SHARED_ONLY SSH_AUTH_SOCK SUBPACKAGE VISUAL" Defaults env_keep +="WRKOBJDIR"
The OpenBSD team deems these environment variables safe to pass into a new user account. The +=
means “add these to the existing list of items to keep.” The environment variables themselves are in quotation marks.
If you need to pass your SSH environment around your servers, you can use scp(1)
and sftp(1)
to move files to other servers. Read the documentation, create a list of approved environment variables, and add an entry.
Defaults env_keep += "SSH_CLIENT SSH_CONNECTION SSH_TTY SSH_AUTH_SOCK"