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 joesudo -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.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.