Ignore Sudo in a Shell Script If You're Running as Root
This can be used to run a command as root outside and inside of Docker where sudo might not be available.
Prefer video? Here it is on YouTube. It covers running some of the commands seen in this post and adds more context to the examples.
This is a pretty specific use case but let’s say you have a script that
installs tools to /usr/local/bin
as well as installs tools with something
like pip
where you may want to use --user
so your tools end up in
~/.local/bin
as a non-root user.
Not only that but you want this script to work on your main system as well as
working inside of Docker where sudo
won’t be available.
This came up recently where I wanted to install tools like kubectl
, argocd
and others to /usr/local/bin
but also pip install a few things as the local
non-root user. It was an install script to help onboard developers with macOS
and various distros of Linux but it also needed to work inside of a Docker base
image to run in CI.
So you kind of have to solve the problem of wanting to run certain commands with sudo but you only want to invoke sudo if you’re not already escalated as the root user.
# This Could Have Been More Straight Forward
If the local pip install use case wasn’t there this would be no problem at all.
Your script could install and copy files to /usr/local/bin
and it would be
expected that you run it with sudo ./install-tools
on your personal machine
and then inside of Docker you’d run ./install-tools
since you’re already the
root user.
The advantage here is your script doesn’t need to use sudo
for a bunch of
commands and I like this because it feels less rude. Either the script fails
because you don’t have root privileges and can’t write to /usr/local/bin
or
you’ve explicitly given the script permission to write to /usr/local/bin
by
using sudo
as the end user.
# Overriding sudo in Your Script
Sometimes real life happens and you can’t use the ideal solution because of a specific use case, in which case you can do something like this.
You can create this file as demo
, chmod +x demo
and then run it with ./demo
:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
# Source: https://askubuntu.com/a/937596
sudo() {
[[ "${EUID}" == 0 ]] || set -- command sudo "${@}"
# If you're the root user then short circuit out of this OR condition. This
# is the Docker use case.
#
# $EUID is the effective user. Unlike $USER, $EUID should be defined in most
# Docker images. I've seen it available in every Debian Slim based image.
#
# Otherwise, take whatever arguments you passed into this function and then
# run it through the sudo command. This is the personal machine use case and
# is the other side of the OR condition.
#
# `command` is a bash command to execute or display info about a command and
# we're using `set` to modify the args of ${@}.
"${@}"
# Run whatever arguments you passed into this function. This would be whatever
# the command was but without `sudo`.
}
sudo cp demo /usr/local/bin
echo "Hello world"
You can also confirm it works in Docker by running:
docker container run --rm -v "${PWD}:/app" -w "/app" debian:bookworm-slim ./demo
That will copy this demo
script to /usr/local/bin
and now you can run
demo
to run it. This is meant to be used for demonstration purposes,
installing “real” tools may involve curling them down, unzipping them and then
moving their binary to /usr/local/bin
.
What’s neat about this approach is:
- On your personal system it will work and it’ll prompt you to either enter your user password or passively work if you have passwordless sudo enabled
- You don’t need to litter your script with a bunch of if conditions to check if
the
sudo
command exists, this is a problem if you’re installing 10 tools - On systems without sudo such as within Docker, it will run the command without sudo and “just work” if it’s being run as the root user
That gives us the best of both worlds in the sense that it runs the same in
both cases. The downside is you need to internally use sudo
within the script
where ever you need it which feels a little anti-user in behavior.
By the way, if you’re interested in learning more about how set --
works I
made a separate post and video about that here.
# Maybe There’s Another Way With SUDO_USER
If you invoke a command with sudo
then the $SUDO_USER
environment variable
will be set to the user who invoked the command such as nick
.
This could let you avoid needing to override sudo
like we did above and also
avoid using sudo
in your script. Instead you’d run the script itself with
sudo
like the first option.
The basic idea here is if $SUDO_USER
is undefined then we know the script was
run without sudo so we can expect it’s the root user. This would be the case
where things are running in Docker, but of course it’s not limited to Docker.
If that env var is set then on your personal box things will install to
/usr/local/bin
no problem because you’re acting as the root user.
Then for the ~/.local/bin
pip install commands with --user
or whatever else
you need to do as the local non-root user, you can use su
to switch to the
$SUDO_USER
beforehand.
# Which Method Should You Use?
Like most things, it’ll depend on your use case. The first option is ideal in my opinion if all you have is system wide tools to install.
If you have a lot of locally installed tools mixed in then I think the 2nd
option is more maintainable since you only need to define that sudo
function
once and the rest of the script can use sudo
as normal. You don’t need to
think about it much beyond that.
If you have a small handful of locally installed tools then the 3rd option feels more “correct”.
It also depends on where this script is being used. If it’s internally within an organization where everyone is being onboarded in the same way that’s a bit different than a script that’s open source.
Personally on a moral / ethical level I like option 3 the best because this shows the most clear intent that your script needs root privileges before you run it. You may also want to consider splitting out your logic into separate scripts to differentiate what needs and doesn’t need root access.
I rolled out option 2 for a client because it made the most sense for their use case. The good news is if our use case changes then we can change the implementation. In the grand scheme of things this isn’t a big deal.
# Demo Video
Timestamps
- 0:43 – A use case
- 3:16 – This could have been more straight forward
- 4:12 – Overriding sudo in your script
- 5:53 – UID vs EUID and USER
- 9:00 – Using command and set to dynamically run commands as sudo
- 11:26 – Confirming it works with Docker
- 12:34 – The override approach is pretty neat
- 13:47 – There’s another way using SUDO_USER
- 16:00 – Which method should you use?
Which option would you pick? Let me know below.