1. Overview
Linux’s sudo system grants security privileges to users. However, we can use it in a way that far exceeds a common idiom, ‘members of sudo group can act as root‘. Thanks to the flexibility of the sudo configuration, we can control various aspects of the command execution. As the bash environment greatly impacts the command’s behavior, we need a way to manage its content.
In this tutorial, we’ll learn how to pass specific variables in specific contexts to the sudo shell.
Throughout this tutorial, we’ll work with sudo version 1.9.9 on Ubuntu 22.04 LTS. Then, we’ll edit the /etc/sudoers with visudo to configure the functionality*.* Finally, we’ll play with two sudoers, joe and john, both belonging to the sudo group.
2. sudo and Environment Variables
Let’s notice that sudo runs a command as the other user, usually root. However, the set of environmental variables is different from that of the calling user or root. Further, we can configure which variables are passed to the target shell. So, let’s find in the /etc/sudoers configuration file:
# ...
Defaults env_reset
# ...
Thus, the Defaults entry with the env_reset causes sudo to create a minimal target environment. In detail, its exact variable content depends on the system setting, e.g., on the PAM configuration. Then, the user’s variables are filtered.
So, let’s review the filtering rules by running sudo -V as root:
# ...
Reset the environment to a default set of variables
Environment variables to check for safety:
TZ
TERM
LINGUAS
LC_*
# ...
Environment variables to remove:
*=()*
RUBYOPT
RUBYLIB
PYTHONUSERBASE
PYTHONINSPECT
PYTHONPATH
PYTHONHOME
# ...
Environment variables to preserve:
XAUTHORIZATION
XAUTHORITY
PS2
PS1
PATH
# ...
Let’s discuss three categories of variables. First, a variable listed in Environment variables to check for safety is passed only if it takes a value that doesn’t contain specific characters. Next, let’s consider the names of the two remaining categories as self-explaining. For example, we can’t pass PYTHONPATH while we succeed with XAUTHORIZATION.
3. Quick Demonstration
Let’s write a simple script sudo_env to print two variables, FOO and BAR:
#!/bin/bash
echo "Test: FOO = $FOO"
echo "Test: BAR = $BAR"
Then, let’s put it into the /usr/local/bin folder and set appropriate permissions to make it available to all users:
$ ls -all /usr/local/bin/sudo_env
-rwxrwxr-x 1 joe joe 61 Nov 2 18:54 /usr/local/bin/sudo_env
Next, we’re going to set both variables and run the script as user joe:
joe $ export FOO=foo BAR=bar
joe $ sudo_env
Test: FOO = foo
Test: BAR = bar
Now, let’s stay in the same shell and increase joe‘s privileges with sudo:
joe $ sudo sudo_env
Test: FOO =
Test: BAR =
So we see that variables aren’t set now.
4. The env_keep Option
Now let’s use the env_keep option in the sudoers file to manage variables’ visibility. In detail, we can regard this option as a list. So, each variable enrolled is passed to the sudo environment. Then, we can compose separate env_keep for users, hosts, commands, and target users. Consequently, we can grant variable access in a fine-grained way.
Let’s notice that env_keep is located in the Defaults entry of the sudoers file. As an example, let’s check the commented-out tips which come with the default sudo policy module:
# While you shouldn't normally run git as root, you need to with etckeeper
#Defaults:%sudo env_keep += "GIT_AUTHOR_* GIT_COMMITTER_*"
# Per-user preferences; root won't have sensible values for them.
#Defaults:%sudo env_keep += "EMAIL DEBEMAIL DEBFULLNAME"
# "sudo scp" or "sudo rsync" should be able to use your SSH agent.
#Defaults:%sudo env_keep += "SSH_AGENT_PID SSH_AUTH_SOCK"
# Ditto for GPG agent
#Defaults:%sudo env_keep += "GPG_AGENT_INFO"
So, these entries would add variables for all members of the sudo group.
4.1. The Global Variable Visibility
Now let’s allow the FOO variable for all users in all circumstances. So, let’s edit the sudoers file and add a line:
Defaults env_keep += FOO
First, let’s notice the syntax. So, with the += operator, we add a variable to the list. Instead, with the sole =, we can replace already existing content.
Next, let’s check the result:
joe $ sudo sudo_env
Test: FOO = foo
Test: BAR =
So, we can find FOO in the sudo environment. In addition, if we want to whitelist BAR too, we need to repeat the Defaults entry:
Defaults env_keep += BAR
Equivalently, we can provide env_keep with a list of names in double quotes:
Defaults env_keep += "FOO BAR"
4.2. Variable to User
Next, let’s narrow the BAR variable accessibility to the user john only. So, we need to add in the sudoers file:
Defaults:john env_keep += "BAR"
Let’s notice that we use : after Defaults to indicate that the entry applies to a user. So, let’s assume that we’ve removed the previous global setting and run as john:
john $ sudo sudo_env
Test: FOO =
Test: BAR = bar
Finally, let’s confirm that joe can’t access this variable:
joe $ sudo sudo_env
Test: FOO =
Test: BAR =
4.3. Variable to Command
Now let’s allow a specific command to see the variable. Thus, we need to use the ! after the Defaults keyword. So, let’s permit only the env command to print both FOO and BAR:
Defaults!/usr/bin/env env_keep += FOO
Defaults!/usr/bin/env env_keep += BAR
Then, let’s check the result:
joe $ sudo env | grep 'FOO\|BAR'
FOO=foo
BAR=bar
Moreover, let’s find out that our script sudo_env reports variables unset:
joe $ sudo sudo_env
Test: FOO =
Test: BAR =
4.4. Removing Variables From the List
As we’ve seen, with =, we can initialize env_keep with a variable, while with +=, we can add a variable. In addition, let’s use the -= operator to remove a variable’s name from the env_keep list:
Defaults env_keep += FOO
Defaults env_keep += BAR
Defaults!/usr/local/bin/sudo_env env_keep -= FOO
With these settings, we’ve made FOO and BAR available for anyone, anywhere, with the exception of the sudo_env script.
4.5. More Security Contexts
So far, we specify allowed variables for users and commands with the : and ! modifiers to the Defaults keyword. Additionally, we can do the same in the context of:
- hosts with the @ modifier – e.g., [email protected] env_keep += “FOO BAR” grants access to anyone working on the server with IP 192.168.1.9
- runas or target user with the > modifier, e.g., Defaults>joe env_keep += “FOO BAR” grants access to anyone acting as joe – sudo -u joe sudo_env
Finally, when setting up a set of rules, we should be aware of the parsing order. So, first, the global rules are parsed, then the hosts and users ones. Then come runas and finally commands settings.
5. More Options for Variables
In addition to env_keep, we have three other options at our disposal. As with env_keep, we can apply these options on the host, user, runas, or command base.
5.1. env_reset
With this option on, we ask the sudo functionality to replace the whole user’s environment. Consequently, sudo provides us with a safe, limited environment. Further, let’s notice that it’s a default setting, and we should be careful when disabling it.
As an example, let’s disable this option for joe only:
Defaults:joe !env_reset
5.2. env_check
With env_check, we can filter out variables by their values. In detail, the variable is ignored if its value contains one of % or / characters. So, let’s check out FOO and BAR when they are passed to the sudo_env script:
Defaults!/usr/local/bin/sudo_env env_check += "FOO BAR"
Now let’s check the result:
joe $ export FOO=foo BAR=bar/
joe $ sudo sudo_env
Test: FOO = foo
Test: BAR =
Now, let’s notice that this option protects against printf vulnerabilities. In addition, the option works regardless of whether env_reset is enabled or disabled.
5.3. env_delete
Finally, we can remove a variable from the environment with env_delete. This option is relevant only if env_reset is off. So, let’s disable this option for joe only and, at the same time, remove FOO:
Defaults:joe !env_reset, env_delete=FOO
Now, let’s test it:
joe $ FOO=foo BAR=bar sudo sudo_env
Test: FOO =
Test: BAR = bar
6. Other Possibilities
So far, we have focused on the capabilities provided by the Defaults entry. **However, we have other ways to pass the user’s environment to the sudo shell.
**
To demonstrate it, let’s first remove john from the sudo group. Instead, let’s give him only the env command and the sudo_env script to run as root:
john ALL=(ALL:ALL) /usr/local/bin/sudo_env, /usr/bin/env
6.1. The E Option to sudo
With the E option, we can ask the sudo command to pass the user’s environment into the sudo shell. So, let’s define variables in the command line as joe:
joe $ FOO=foo BAR=bar sudo -E sudo_env
Test: FOO = foo
Test: BAR = bar
However, this method doesn’t work for john:
john $ FOO=foo BAR=bar sudo -E sudo_env
sudo: sorry, you are not allowed to preserve the environment
So, let’s recall that joe is a member of the sudo group with the %sudo ALL=(ALL:ALL) ALL entry. Consequently, the command match is ALL, so the E option has an effect. On the other hand, john has very limited permissions without any additional variables allowed.
6.2. The SETENV Tag for Commands
Now let’s improve john‘s ability to pass variables. So, let’s move to the commands part of the sudoers file and split john‘s commands. In detail, we’re going to add the SETENV tag to the sudo_env script but not to the env command.
john ALL=(ALL:ALL) SETENV: /usr/local/bin/sudo_env
john ALL=(ALL:ALL) /usr/bin/env
Consequently, john can run the sudo_env script in its original environment now:
john $ FOO=foo BAR=bar sudo -E sudo_env
Test: FOO = foo
Test: BAR = bar
However, this setting doesn’t apply to env:
john $ FOO=foo BAR=bar sudo -E env | grep 'FOO\|BAR'
sudo: sorry, you are not allowed to preserve the environment
7. Special Cases of Variables
Now let’s discuss two special cases where the visibility of a variable depends on other settings of the sudo configuration.
7.1. Variables That Are Bad-To-The-Bone
Let’s notice that we can’t pass some variables. Moreover, it happens regardless if we disable the env_reset option or run sudo with the E option. As an example, let’s try to pass PYTHONPATH:
joe $ export PYTHONPATH=/foo/bar
joe $ env | grep PYTHONPATH
PYTHONPATH=/foo/bar
However, as sudoer, we’re going to obtain the following:
joe $ sudo env | grep PYTHONPATH
# empty
So, we always need to add such a variable explicitly to env_keep. Further, let’s find that this variable is regarded as ‘bad’ and blacklisted in the source code of the sudo module.
7.2. The PATH Variable
Another special variable is PATH. Although we can find this variable on the ‘preserved’ list, this setting may be shadowed by the secure_path option in the sudoers file:
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/
So, let’s disable secure_path for joe only:
Defaults:joe !secure_path
Now, let’s check the result:
joe $ export PATH=/foo/bar:$PATH
joe $ sudo env | grep PATH
PATH=/foo/bar:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
8. Conclusion
In this tutorial, we learned how to pass environmental variables to the sudo shell. First, we examined how the sudo command sets the default environment. Then, we used the env_keep option in the sudoers configuration file to pass variables for certain users or commands. Next, we overviewed other options available in this configuration.
Next, we briefly discussed how to bypass the sudo rules and transfer the environment with the sudo command. Afterward, we looked at the specific variables which needed special handling.
Finally, let’s emphasize that environmental variables have a great impact on system security. So, we should think twice when modifying the sudo shell environment.