sudo from Shell Scripts: Command, Environment, and Policy Boundaries
Use sudo without assuming it preserves your shell environment, shell syntax, or arguments; verify env_reset, secure_path, runas policy, and status.
sudo is a policy-controlled program launcher, not a shell keyword that makes the rest of a line privileged. It authenticates or otherwise authorizes a request, selects a target user and group, constructs an execution environment, and invokes a command. Those steps can differ substantially from the environment in your current shell. Scripts become fragile when they assume that variables survive, that the privileged PATH matches the caller’s, or that a shell builtin such as cd can be run as an independent program.
The secure model is to run one explicit executable with explicit arguments, pass only environment variables the policy allows, and keep shell parsing out of the privileged boundary. Before changing a production sudoers rule, inspect the effective policy and use visudo for syntax validation. The examples below focus on common sudo 1.9 behavior; operating-system packages can compile or configure different defaults, so confirm the installed manual and policy on the host.
sudo executes a command, not a later shell fragment
This is not a useful way to elevate a directory change:
sudo cd /srv/app
cd is a shell builtin because it must change the current shell process’s working directory. sudo normally looks for an executable command named cd; even if a helper or shell is involved, a child process cannot change its parent’s current directory. Keep directory changes in the shell that needs them, or ask sudo to run a fixed shell program when the whole operation must execute in a particular directory.
For a multi-step privileged operation, prefer a root-owned script with a narrow, reviewed interface:
sudo -- /usr/local/sbin/rotate-service-logs --service api
The local shell passes the executable and arguments to sudo. -- ends sudo option parsing; it does not sanitize arguments for the program. The helper must still validate --service, reject unexpected values, and avoid constructing another shell command from user data. Use an absolute path so an untrusted or modified PATH cannot select a different executable. The helper should also use absolute paths for any privileged subprocesses it starts.
Avoid sudo sh -c "$text" when $text contains values from a user, environment variable, file, or network response. The shell interprets metacharacters in that expanded text with the target user’s privileges. A shell is appropriate when the script is fixed and audited, not as a string-quoting workaround. If several commands must be privileged, put them in a dedicated script with input validation and explicit error handling.
The default environment is intentionally not your shell’s environment
With the sudoers env_reset setting enabled, sudo constructs a minimal environment rather than copying every variable from the invoking shell. Common variables such as HOME, USER, LOGNAME, and PATH may be initialized based on policy and target identity. env_keep, env_check, env_delete, PAM, secure_path, and environment files can modify the result. Consequently, sudo command may not see a language runtime path, a cloud profile, a proxy, a locale, or a custom tool directory that works without sudo.
Do not “fix” a missing variable by globally disabling env_reset. Environment variables can change dynamic library loading, executable lookup, configuration selection, credential discovery, and application behavior. A broad inherited environment can therefore change what a privileged command loads or executes. Keep the default restriction and allow only the specific variable required by a reviewed command, with a narrowly scoped sudoers rule.
To set a single variable for a single invocation, the conventional syntax is:
sudo --preserve-env=LANG -- /usr/local/sbin/rebuild-index
This asks sudo to preserve a named variable; policy can refuse it. Another form is sudo NAME=value command, but accepting caller-specified environment values is controlled by sudoers SETENV policy and can have security consequences. Do not assume an assignment is honored because it appears before the command. Verify the effective behavior with an innocuous test command and the local sudo manual, and avoid printing secret values while diagnosing.
The -E/--preserve-env option requests preservation of the caller’s environment, but sudoers can deny the request or restrict it to particular variables. It is not a universal bypass for env_reset, and secure_path may still replace PATH. Never use sudo -E as a generic “make this work like my shell” switch. If a tool needs one environment variable, state it explicitly and have policy decide whether it may cross the privilege boundary.
PATH and executable identity are part of the privilege policy
An unprivileged PATH may contain writable directories, virtual environment bins, package-manager shims, or project-local executables. Running sudo tool asks sudo to resolve a command under its own command search and policy rules; sudoers may configure secure_path to force a known path. That is safer than trusting the caller’s search order, but scripts should still call the intended executable by its absolute path and keep its parent directories protected from unprivileged modification.
Check the actual command and target user rather than inferring them from prompt text:
sudo -l
sudo -u deploy -- /usr/bin/id -un
sudo -u deploy -- /usr/bin/printenv HOME
sudo -l reports the invoking user’s permitted commands, subject to policy and authentication. The latter commands are diagnostics, not a complete proof of environment policy: they show only selected results for that request. Do not publish unredacted env output from a privileged process; it can contain tokens and service credentials. Use a controlled test that prints only required variable names and safely redacted values.
An executable that an unprivileged user can replace, a parent directory writable by that user, or a script that sources a writable configuration file can defeat the intention of an absolute executable path. Audit the complete chain: file owner and mode, parent directory permissions, interpreter path in the shebang, sourced files, configuration inputs, and every subordinate command. The privilege boundary includes all code the privileged process loads, not merely the first pathname after sudo.
Command arguments are not shell code, but the target program still parses them
The safe way to pass a value from a shell variable to a command is to preserve it as one argument:
record_id=${1:?usage: remove-record ID}
case $record_id in
(*[!A-Za-z0-9_-]*|'') printf '%s\n' 'invalid record ID' >&2; exit 64 ;;
esac
sudo -- /usr/local/sbin/record-admin remove -- "$record_id"
The quotes preserve one local argument, and sudo passes the command and arguments through the execution interface rather than asking a shell to re-parse the variable. -- after sudo ends sudo’s own option parsing; the second -- is for the target program if it supports that convention. Validate identifiers against the program’s actual data model. Shell-safe argument passing prevents shell injection, but it does not prevent option injection, path traversal, authorization errors, or dangerous semantics in the target program.
Sudoers rules can restrict commands and, depending on the policy syntax and version, command arguments. Do not rely on a broad rule such as allowing a general-purpose shell, editor, interpreter, package manager, or file-copy utility when the real need is one maintenance action. Such programs can often execute arbitrary commands or modify privileged files. A narrow root-owned helper is usually easier to reason about than a long rule with wildcards that accidentally grants more than intended.
When users need to edit a protected file, sudoedit is generally safer than launching a root editor. It copies the file to a user-owned temporary location for editing and installs the result after the editor exits, subject to sudo policy and implementation. Still validate the resulting file and avoid environment-controlled editor behavior unless policy explicitly permits the selected editor. A privileged editor can spawn shells, load plugins, or execute arbitrary commands with the elevated identity.
Shell options and login behavior are separate policy choices
sudo -s requests a shell using the invoking user’s shell, while sudo -i requests a login-style shell for the target user and applies login environment behavior described by sudoers. Neither means “run the rest of my script in the current process as root.” They create a new shell process with a different identity and environment. Interactive login files can execute commands and change variables, so they are not a substitute for a deterministic automation entrypoint.
For batch jobs, invoke the specific command directly. If a fixed shell script must run under sudo, use a root-owned file and a fixed interpreter invocation rather than an interpolated -c string. Be explicit about -u and -g when a non-root target is intended; default run-as identity can be altered by policy. Verify effective UID, GID, working directory, and environment at the beginning of a privileged maintenance process when those properties are operational requirements.
Be cautious with file descriptors and redirections. In sudo command > /root/output, the calling shell opens the redirection before sudo runs, so an unprivileged caller generally cannot create a root-only destination. sudo sh -c 'command > /root/output' changes who performs the redirection, but it also introduces a privileged shell. Prefer a purpose-built command that writes its own output securely, or use a carefully designed pipeline to a narrow privileged receiver. Do not improvise complex redirection quoting in production.
Preserve failure status and avoid masking policy errors
Sudo’s exit status is the executed command’s status when execution succeeds, with distinct failures for cases such as inability to execute the command or policy/authentication rejection. A script should preserve and report that status rather than continuing as if a privileged operation succeeded:
if sudo -- /usr/local/sbin/rebuild-index --check; then
printf '%s\n' 'index verified'
else
status=$?
printf 'rebuild-index failed (status %s)\n' "$status" >&2
exit "$status"
fi
This explicit branch is clearer than relying exclusively on set -e, whose behavior depends on shell context. Do not add || true to silence a sudo failure unless that exact failure is expected and separately checked. Authentication prompts also require a terminal or configured policy; a CI runner should fail clearly rather than hang indefinitely waiting for a password prompt that no operator can answer.
If the workflow uses sudo -n, sudo exits instead of prompting when authentication would be required. This is useful for noninteractive jobs, but it does not make an authorization failure harmless. Check the result and emit an actionable message without disclosing command secrets. Configure narrowly scoped credentials or policy through the host’s supported mechanism, not by embedding passwords in scripts, environment variables, or command lines.
Validate policy changes without broadening access
Use visudo to edit sudoers and validate syntax. Drop-in file naming, ownership, mode, and include order matter; a syntactically valid fragment can still be shadowed or overridden by another matching rule. Where supported, use visudo -c to validate the main file and included policy files after a change. Test as the intended invoking user, because root’s view of policy is not the same as an ordinary user’s authorization path.
Review positive and negative command rules, run-as users and groups, tags such as NOPASSWD and SETENV, environment keep/delete lists, secure_path, and any wildcard matching. Wildcards can match more than a human expects, especially when they cover a path or an argument string. Prefer exact paths and fixed interfaces, and test both the intended command and nearby commands that should remain prohibited.
Record the sudo version and relevant policy provenance during incident response, but do not dump sensitive environment state. Configuration management should own the policy and deploy it with an auditable diff. After a change, verify that the authorized command works, an adjacent unauthorized command still fails, the executed identity is correct, and the command’s outputs and logs do not expose credentials. The goal is not to make privilege escalation convenient at any cost; it is to create a narrow, observable path where shell syntax, command identity, and environment rules are deliberate.
Related:
- Bash eval: Keep Data Out of the Second Parse
- Git Credential Helpers: Keep Secrets Out of Shell Command Text
Sources: