TL;DR — Short on time and just want an answer? Jump to our solution.
If you’ve used npm even once in your life, there’s a good chance you’ve read all about how you should never use sudo to install Node.js dependencies. Early in npm’s history, developers of every stripe and experience level would use sudo npm install --global package-name
to get around the fact that they don’t own ~/.npm
on their development machine. This was due to the fact that the Node.js installation package did not set the correct permissions when creating that directory, strangely enough.
The reason this is a bad idea is that when you install a package (globally or otherwise), npm also runs that package’s postinstall
and prepublish
scripts. In other words, npm runs arbitrary code which, if compromised or just irresponsibly written, can be dangerous. Combine that with superuser permissions via sudo and you have a disaster waiting to happen.
There is, however, very little discussion about legitimate use cases for running npm via sudo. Recently, Adam Zimmermann and I found ourselves in just such a use case, and we learned a great deal about both of these tools.
The use case
sudo isn’t just for running commands as the root
user; it can also run commands as other users in a given system. This is useful, for example, if you need to run a command on a live server as a deployment user, as was our case.
Specifically, Adam was in the process of setting up an automated deployment job that included running a build script for a static component library website. This build script first runs npm install
(to be sure any new dependencies are available) and then gulp
to produce an optimized build of the static site’s files.
This website is served from a directory owned by deploy_user
on that box and Adam wanted to run our build script himself to check that everything worked as expected. Knowing full well that he shouldn’t run arbitrary code as root
, he used the following command:
sudo -u deploy_user npm run build
This failed with a rather confusing error message:
> project-name@1.0.1 build /var/www/static-site-directory
> npm install && gulp
npm ERR! Linux 4.9.7-x86_64-linode80
npm ERR! argv "/usr/bin/nodejs" "/usr/bin/npm" "install"
npm ERR! node v6.10.1
npm ERR! npm v3.10.10
npm ERR! path /home/loggedinuser/.npm
npm ERR! code EACCES
npm ERR! errno -13
npm ERR! syscall mkdir
The following lines are of particular note:
npm ERR! path /home/loggedinuser/.npm
npm ERR! code EACCES
[…]
npm ERR! syscall mkdir
This raises the following questions:
- Why is npm running
mkdir
in Adam’s home directory if the--global
flag isn’t used in ournpm install
command? - Even if
--global
was used, why attempt to install anything on Adam’s home directory and notdeploy_user
’s home directory?
This was quite a perplexing problem, to say the least, so we set out to find answers. However, unlike most cases, Google wasn’t much help. Try a search query with the terms “sudo” and “npm” and you’ll get dozens of blog posts, Stack Overflow answers, and tutorials illustrating why you should never run npm install
with sudo, and how to fix your permissions so that you never have to.
That’s when I decided to do some sleuthing of my own.
What on Earth is npm trying to do?
The first thing I dove into was the issue of npm running mkdir
on Adam’s home directory despite the fact that our npm install
command doesn’t include the --global
flag. To take a peek into what all goes on when npm installs packages locally, I navigated to my local repository of the same project and ran npm install --verbose
. The --verbose
flag indicates to npm that it should print information about what it’s doing, which npm typically hides from the end user. Here is what the output looks like (I’ve removed a few irrelevant lines for tidiness):
[…]
npm verb correctMkdir /Users/alfonso/.npm/_locks correctMkdir not in flight; initializing
npm verb lock using /Users/alfonso/.npm/_locks/staging-4e551b4f5e16699e.lock for /Users/alfonso/Projects/project-name/node_modules/.staging
npm verb unlock done using /Users/alfonso/.npm/_locks/staging-4e551b4f5e16699e.lock for /Users/alfonso/Projects/project-name/node_modules/.staging
[…]
As you can see in the output above, despite the fact that I’m asking npm to install packages locally, it looks like it checks on a _locks/
directory in /Users/alfonso/.npm/
. I looked at npm’s source for that correctMkdir
command and, as the name suggests, it tries to make a directory when it can’t find it. Likewise, the lock
command referenced in that output attempts to write a file to that directory.
So that answers our first question above. Despite asking it to install packages locally, npm needs write access to the invoking user’s ~/.npm/
directory, which leads us to the next question.
Why is npm writing to Adam’s home directory when called with sudo?
After doing a bit of spelunking into sudo’s man
page, I learned that that while the -u
option of the sudo program is indeed described as running a command as the target user, this impersonation is limited to only that user’s permissions. That is, the environment is still the same as the environment for the user invoking sudo in the first place. This means that common environment variables, such as PATH
or HOME
will retain the values for your own user, despite your use of sudo to impersonate another user. So in our original attempt, npm install
was being invoked with deploy_user
’s permissions, but in Adam’s environment. This explains why npm is attempting to access /home/loggedinuser/.npm/
in our error.
Location, location, location
Like so many things, this is best illustrated with some examples. I created a dummy deploy_user
on my laptop and ran this command, which asks sudo
to impersonate deploy_user
and run some shell commands as that user:
$ sudo -u deploy_user sh -c 'whoami && echo $HOME'
> deploy_user
> /Users/alfonso
As you can see, whoami
returns what we expect: the command is running as deploy_user
. However, the result of echo $HOME
points to my home directory, not the one belonging to deploy_user
. sudo’s man
page describes an option that makes the HOME
variable be set to the target user’s HOME
environment variable, using the -H
flag. Let’s try our command with that option enabled:
sudo -H -u deploy_user sh -c 'whoami && echo $HOME'
> deploy_user
> /Users/deploy_user
The result is what we’re looking for: the HOME
environment variable now points to /User/deploy_user
.
Why shell ('sh')?
In the examples above, we are telling sudo to use a specific shell program (sh
) to interpret the string 'echo $HOME'
. If you wanted sudo to pass your commands to the target user’s shell, you can use the -s
flag, like so:
sudo -s -H -u deploy_user echo '$HOME'
> /Users/deploy_user
Notice that I didn’t have to wrap my entire command in single quotes ('
), although I did wrap $HOME
to keep it from being interpreted by my own shell before it was passed to sudo. Were it not for that variable, no quotes would be necessary:
sudo -s -H -u deploy_user whoami
> deploy_user
Our solution
Getting back to our original mission, we were attempting to run npm run build
as deploy_user
and getting an error because npm was attempting to write a .lock
file to /home/loggedinuser/.npm/_locks/
, a directory to which deploy_user
has no access. Using our newfound knowledge, we adapted our original command to include the flags we were missing:
sudo -s -H -u deploy_user npm run build
In addition to running npm run build
as deploy_user
, this command ensures two things for us:
- that we’re running our commands using
deploy_user
’s shell program - that the
HOME
environment variable is set todeploy_user
’s home directory, and not our own
This was one of those challenges that starts out with a perplexing error and for which a simple search online didn’t prove useful, forcing us to read the manual. Since we had such a hard time finding accessible help online about this problem, I thought it would be useful to document our findings for others to find and learn from our mistakes.
I hope I’ve demonstrated a valid use case for running npm via sudo, illustrated some of the challenges you may encounter with such use and how to overcome these challenges in your own work.