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
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:
> firstname.lastname@example.org 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
mkdirin Adam’s home directory if the
--globalflag isn’t used in our
- Even if
--globalwas used, why attempt to install anything on Adam’s home directory and not
deploy_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
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
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
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
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
HOMEenvironment variable is set to
deploy_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.