Using npm via Sudo Without Losing Your $HOME or Your Mind

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:

  1. Why is npm running mkdir in Adam’s home directory if the --global flag isn’t used in our npm install command?
  2. Even if --global was 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 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:

  1. that we’re running our commands using deploy_user’s shell program
  2. that the HOME environment 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.