Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RRFC] Clean up file ownership story #546

Open
ruyadorno opened this issue Mar 3, 2022 · 30 comments
Open

[RRFC] Clean up file ownership story #546

ruyadorno opened this issue Mar 3, 2022 · 30 comments

Comments

@ruyadorno
Copy link
Contributor

ruyadorno commented Mar 3, 2022

We currently have an entire category of issue reports around unexpected changing of file ownership. We need a place to start the discussion on what we can do to improve the story around file ownership management: Maybe we need less of it these days?

Opening this RRFC so that we can have a place to share thoughts, link related issues to and hopefully eventually evolve towards an RFC if we come up to the decision that changes are necessary.

cc @nlf @fritzy

@ljharb
Copy link
Contributor

ljharb commented Mar 3, 2022

Auditing and aligning this sounds great.

I would expect that npm has full ownership (and thus, full freedom to change whatever it likes) of any node_modules, and $HOME/.cache, and things inside npm root -g, and its own installation folder.

Things a user is explicitly asking npm to change - .npmrc, package.json, lockfiles - npm of course can change, but shouldn't change ownership (it should error, if that is required; warn if not)

I would also expect that any other file is something npm is not "allowed" to change in any way.

@nlf
Copy link
Contributor

nlf commented Mar 3, 2022

AFAIK the existing file ownership changes are so that things like sudo npm install in a directory that's owned by your user doesn't write a bunch of files owned by root, both to your project and potentially to your cache.

This doesn't feel like something we should be attempting to forcibly fix on a user's behalf. The stat calls to determine ownership, and the chown calls to modify the ownership when it differs from what we infer are not free. We are most certainly taking a performance hit with these, especially in environments like Docker where filesystem i/o can be significantly slower.

Attempting to change permissions of things like the directory global bin scripts are located in can be absolutely disastrous.

Is there prior art for the type of permissions changes we attempt? I suspect there's not, but personally I also pretty strongly believe we shouldn't be attempting to change ownership ourselves.

@ljharb
Copy link
Contributor

ljharb commented Mar 3, 2022

Is it possible to make sudo npm install immediately exit with an error?

@nlf
Copy link
Contributor

nlf commented Mar 3, 2022

potentially, but that wouldn't stop things like a user elevating their permissions to root manually and installing there. we absolutely can't explode when npm is run as root unconditionally, as that's often the case in a containerized environment.

@nlf
Copy link
Contributor

nlf commented Mar 3, 2022

a suggestion: what if npm doctor can help diagnose this class of problem? i.e. if some other npm command errors with something related to filesystem permissions, our error output can suggest the user runs npm doctor. that would then do things like examine the permissions of files in node_modules and the cache directory and either link to docs or display in-line how the user can correct the permissions themselves

@nlf
Copy link
Contributor

nlf commented Mar 3, 2022

after actually running npm doctor myself for the first time in quite a while, it already does permissions checks

Perms check on cached files         ok
Perms check on local node_modules   ok
Perms check on global node_modules  ok
Perms check on local bin folder     ok
Perms check on global bin folder    ok

so all that's missing is making our error output suggest running doctor, and doctor telling you how to fix the problem

@fritzy
Copy link
Contributor

fritzy commented Mar 4, 2022

We could have a --no-root-user default for non-global installs, and then ci users would have to run --root-user. In the same release, we'd drop all ownership checks, and just write files as the current user. This would, of course, be a major breaking change.

ENOROOT: running `npm` as root is discouraged. Use `--root-user`to override. [link to docs]

@nlf
Copy link
Contributor

nlf commented Mar 8, 2022

adding this as another data point npm/cli#4312

@wesgarland
Copy link

wesgarland commented Mar 8, 2022 via email

@theredcat
Copy link

theredcat commented Mar 15, 2022

Rather than dropping perms to nobody in this circumstance (which breaks compilation or yields surprising ownership issues), can we drop perms to $SUDO_USER instead?

I think that's not a good idea, not everyone is using sudo. If I ssh root@myserver this var won't be set because I'm "really" root. This is not a good practice, but it's the default on many hosters

@nlf
Copy link
Contributor

nlf commented May 2, 2022

how about this for a default behavior:

flowchart TD
A[root runs command] --> B{SUDO_UID/SUDO_GID are set?};
B -- Yes --> C[setuid/setgid to SUDO_UID/SUDO_GID] --> E;
B -- No --> D{--allow-root passed?};
D -- Yes --> Z[log warning] --> E[proceed running command];
D -- No --> F[log error, exit];
Loading

when logging an error and exiting, we could use a message along the lines of

npm ERR Running npm commands as root is considered dangerous.
To run as a different user, you may provide the '--uid' and '--gid' flags.
If you're absolutely sure you want to run this command as root use the '--allow-root' flag.
For more information: https://some/link/to/the/docs

and the warning could read something like

npm WARN Running npm as root. For more information: https://some/link/to/the/docs

@theredcat
Copy link

@nlf You didn't wrote it, but I assume that you are suggesting the above default behavior + removing npm chmod ?

If that's the case it seems pretty nice ! It will break a lot of misconfigured docker containers but I think this is not a problem since no use application should be run as root, even in a container.

People will discover the USER docker directive and that's for the best :)

@robertsLando
Copy link

robertsLando commented May 4, 2022

IMO a flag would be a simple yet effective solution to this problem. I'm thinking about something like --no-infer flag that simply skip the inferOwner call here

@darcyclarke
Copy link
Contributor

Leaving this on the agenda for today, @nlf would be great if you could speak bit to your diagram/approach discussed here in the call but understand if you can't make it.

@javabudd
Copy link

javabudd commented May 12, 2022

I'm extremely frustrated with this permission decision. Docker volume mounts use the UID:GID of the host machine. The changes npm has made to infer the execution user from the UID:GID effectively break docker setups where the host users UID:GID does not match the node user on the container. Setting UID:GID in a .env file for each developer in our application is cumbersome and overall ridiculous. Stop trying to infer best security practices when it comes to running scripts, your job is to be a package manager

@nlf
Copy link
Contributor

nlf commented May 12, 2022

I'm extremely frustrated with this permission decision. Docker volume mounts use the UID:GID of the host machine. The changes npm has made to infer the execution user from the UID:GID effectively break docker setups where the host users UID:GID does not match the node user on the container. Setting UID:GID in a .env file for each developer in our application is cumbersome and overall ridiculous. Stop trying to infer best security practices when it comes to running scripts, your job is to be a package manager

to be very clear this permissions decision was made many years ago, it is by no means a new thing, and that's exactly why this issue exists. this is something that we know to cause problems, so we're trying to determine the best way to fix that without immediately causing builds to break for people who have worked around the problem already. we'll be starting to roll out some changes to help folks in your situation in the near future.

@darcyclarke darcyclarke removed the Agenda will be discussed at the Open RFC call label May 18, 2022
@darcyclarke
Copy link
Contributor

Actions

@jstasiak
Copy link

jstasiak commented Jun 9, 2022

Hey all,

having spent a whole day debugging an issue caused by silently setting the effective UID of npm subprocesses to <owner of the current directory> yesterday I'd like to propose an alternative to this behavior:

we absolutely can't explode when npm is run as root unconditionally, as that's often the case in a containerized environment

That's fair, but how about exploding when npm is run as root and the current directory is owned by non-root? This seems strictly preferable to silently switching users.

@papb
Copy link

papb commented Jul 28, 2022

I also agree that silently switching users is a great source of pain. I for example lost about one hour because of this. Please find a solution that stops doing this, thank you all very much :)

@ateijelo
Copy link

I think npm shouldn't be looking at its own UID, it should not modify file ownerships and permissions, and it should specially not switch users after it's called. That last point, when run as root, breaks long standing assumptions of a process behavior.

It's up to the caller of npm to decide what user they want to run it as, even if their decision is bad.

If I run npm install as root, npm will create files in node_modules and in my cache that belong to root. That is not a bug in npm, and it's not something for npm to fix. If I don't want to have those files owned by root, then I shouldn't be running npm install as root to begin with.

@slickss-nsul
Copy link

@javabudd We face(d) a similar issue.

I'm extremely frustrated with this permission decision. Docker volume mounts use the UID:GID of the host machine. The changes npm has made to infer the execution user from the UID:GID effectively break docker setups where the host users UID:GID does not match the node user on the container. Setting UID:GID in a .env file for each developer in our application is cumbersome and overall ridiculous. Stop trying to infer best security practices when it comes to running scripts, your job is to be a package manager

echo "module.exports.sync = function() { return { uid: 0, gid: 0 } };" >> $(npm root -g)/npm/node_modules/infer-owner/index.js

This is the workaround we cooked up on a ubuntu based docker (so your path may be different)

It modifies the behavior of how npm detects which user it should run as. We this issue when using webpack in a VOLUME mount, while writing the output to a another, in-container only folder owned by root. In our case, forcing UID=0 is enough, so we echo append that to the module.

(We are still evaluating this solution, but looks workable)

@gnida-rada
Copy link

Still unclear, why NPM tries to lchown files that have nothing to do with Node. In my case, it is a link in /usr/local/bin to Microsoft Defender Antivirus.
Please fix it.

% sudo npm install -g yarn@1.22.19
Password:
npm ERR! code EPERM
npm ERR! syscall lchown
npm ERR! path /usr/local/bin/mdatp
npm ERR! errno -1
npm ERR! Error: EPERM: operation not permitted, lchown '/usr/local/bin/mdatp'
npm ERR! [Error: EPERM: operation not permitted, lchown '/usr/local/bin/mdatp'] {
npm ERR! errno: -1,
npm ERR! code: 'EPERM',
npm ERR! syscall: 'lchown',
npm ERR! path: '/usr/local/bin/mdatp'
npm ERR! }

% ls -l /usr/local/bin/mdatp
lrwxr-xr-x 1 root admin 78 Aug 23 13:37 /usr/local/bin/mdatp -> /Applications/Microsoft Defender.app/Contents/Resources/Tools/wdavdaemonclient

@x-yuri
Copy link

x-yuri commented Oct 15, 2022

From what I can see, it goes back to the times when it was suggested that npm should be run as root. The idea was that then npm can switch to nobody when running package scripts. A couple more of relevant links.

Was that a good idea or not?.. I personally think that npm shouldn't decide under which user to run whatever a user wants to run (or at least provide an escape hatch, at some point it existed). That makes npm more complex, its behavior more nuanced, and it's not npm's responsibility (npm is not in a position to do that).

I was beaten by it a couple of times, and it always took me a while to figure out what's going on. Today it was like this (in a docker container):

$ npx serve
sh: serve: Permission denied

sh? Permission denied?

If that's the case it seems pretty nice ! It will break a lot of misconfigured docker containers but I think this is not a problem since no use application should be run as root, even in a container.

People will discover the USER docker directive and that's for the best :)

So let's say I have a docker-compose project that bind mounts . into a container (to sync what's on the host and what's inside a container). And to make it work I create a user in Dockerfile with UID 1000 (which happens to be my UID on the host), give it to my colleague (which has UID 1001 on his/her host), and now what? I have to make people specify their UID to run a project? Doesn't it look like npm takes too much responsibility only to make peoples' lives harder? Compare it to the case where npm doesn't switch users and I have nothing to do on my part.

how about exploding when npm is run as root and the current directory is owned by non-root? This seems strictly preferable to silently switching users.

see the above case

echo "module.exports.sync = function() { return { uid: 0, gid: 0 } };" >> $(npm root -g)/npm/node_modules/infer-owner/index.js

That's a rather radical way to deal with it, but in a way I like it :)

we're trying to determine the best way to fix that without immediately causing builds to break for people who have worked around the problem already

Do you have an idea how to work around it? (Or you're just trying to make it as close to the previous behavior as possible?) After dropping unsafe-perm the only way is probably the one above. So, if we do it quick, noone will notice :) Or why not just drop the whole infer-owner/switch-users thing in the next release? I bet the builds that'll break... that'll make people happy that one workaround less is needed.

@wraithgar
Copy link
Member

Or why not just drop the whole infer-owner/switch-users thing in the next release?

This is going to be the behavior in npm 9. Wednesday's prerelease should have this available for testing.

zanna-37 pushed a commit to zanna-37/hass-swipe-navigation that referenced this issue Dec 8, 2022
@nlf
Copy link
Contributor

nlf commented Dec 13, 2022

we have stopped altering file ownership at runtime on the user's behalf in npm@9 entirely.

i think we have some follow up work to do here to try to help users not end up in a situation where they have files unexpectedly owned by root. things like disabling the cache and log files if the cache directory is not owned by the same user that's running npm would go a long way towards not making users have to keep running chown -R to fix ownership

@koresar
Copy link

koresar commented Feb 14, 2023

Thanks for implementing this long overdue change. At last npm is more predictable if ran from root.

However, this is caused us a lot of pain. :) Indeed, our docker builds started failing as soon as we started using npm v9.
The official Node documentation does not work any more in some scenarios: https://nodejs.org/en/docs/guides/nodejs-docker-webapp/

For others, who stumbled upon Docker build errors like

npm WARN tar TAR_ENTRY_ERROR EINVAL: invalid argument, fchown

or (for multi stage builds)

failed to copy files: failed to copy directory: Error processing tar file(exit status 1): Container ID 166037 cannot be mapped to a host ID

here is how I fixed it.

The build container should start from this:

FROM node:18 as builder

# Where all the commands are going to be executed
WORKDIR /usr/src/app

# Create user `node`
USER node

# The npm v9 does not automatically chown directofy for us, so we have to do it ourselves
USER root
RUN chown -R node /usr/src/app

# This would run any following npm commands NOT from root. It is highly recommended to never run npm as the root user.
USER node

# Copy all the files from your host (laptop?) current directory (except .dockerignore file matches)
COPY . .

RUN npm ci

And end with this:

# This will make sure the the next COPY --from=build docker command can be successfully executed
USER root
RUN chown -R root:root /usr/src/app

The next container in the multi-stage docker build should be like this:

FROM node:18-alpine

WORKDIR /usr/src/app

# ... other commands go here ...

# Copy files from previous build stage to this (last) build stage
COPY --from=builder /usr/src/app .

# Run the process under this user (because it is root by default)
USER node

Hope this helps someone in the future.

@x-yuri
Copy link

x-yuri commented Feb 15, 2023

@koresar Copying files from Debian to Alpine? That won't work if any of your packages contain anything binary.

USER node doesn't create a user, it switches to a user if it exists. Try USER non-existent-user.

A lot of comments (Clean Code, chapter 4) but the really unclear part is uncommented:

# This will make sure the the next COPY --from=build docker command can be successfully executed
USER root
RUN chown -R root:root /usr/src/app

That is, there's a comment but it doesn't tell why one should avoid doing that. From what I can see, there should be no problem with copying files owned by non-root. And the owner of the destination files is determined on the receiving end.

The way I'd do it (a gist):

FROM node:18-alpine as node_modules
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

FROM node:18-alpine
WORKDIR /app
RUN chown node: .
COPY --chown=node --from=node_modules /app/node_modules node_modules
COPY --chown=node . .
USER node

But okay. That's npm@8. It however still works with npm@9:

FROM alpine:3.17 AS node_modules
RUN apk add nodejs npm
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install

FROM alpine:3.17
RUN apk add nodejs shadow && useradd node
WORKDIR /app
RUN chown node: .
COPY --chown=node --from=node_modules /app/node_modules node_modules
COPY --chown=node . .
USER node

All in all, I don't rule out that I'm missing something here, but the way it looks to me at the moment, in addition to the first issue (Debian -> Alpine), there's a lot of unnecessary movement in your solution. And mine is a better starting point. But if I'm missing something, can you possibly tell under which conditions it'll fail? How to reproduce the issues you mentioned? Imagine a person running into them. Should he/she change the whole file to fix them? Instead of "add this line," "change this line", or "don't do this because..."?

@koresar
Copy link

koresar commented Feb 16, 2023

For past 9 years of using Dockerfile(s) I thought USER node would crate me a user and a group.

I tried USER nonexistentuser as you suggested. And it worked flawlessly. Did you try it yourself? :)

My base image is node:18 (plus its alpine version) because it's the one recommended by OpenJS Foundation. I'm not going to bake my own images with apk and whatnot. Too laborious.

Thank you for your COPY --chown=node tip. That's handy.

Let's not drive away the discussion in this issue from the npm to my coding challenges.

I appreciate your help.

@x-yuri
Copy link

x-yuri commented Feb 21, 2023

@koresar

I tried USER nonexistentuser as you suggested. And it worked flawlessly. Did you try it yourself? :)

FROM node:18-alpine
USER nonexistentuser
RUN set -x && whoami && id
$ docker build .
...
Step 3/3 : RUN set -x && whoami && id
 ---> Running in 5eb31625a61b
unable to find user nonexistentuser: no matching entries in passwd file

Also, from the official docs:

The USER instruction sets the user name (or UID) and optionally the user group (or GID) to use as the default user and group for the remainder of the current stage.

--

My base image is node:18 (plus its alpine version) because it's the one recommended by OpenJS Foundation. I'm not going to bake my own images with apk and whatnot. Too laborious.

As you probably noticed, my main suggestion was to use node:18-alpine. But apparently my node:18-alpine image had npm@8, and I didn't think to try anything more specific (like node:18.14.0-alpine), so for the sake of the argument I used alpine:3.17. But generally yes, stick to node:... until you can't.

Let's not drive away the discussion in this issue from the npm to my coding challenges.

Coding habits probably? Actually, I was planning to give a more detailed reply back than, but then decided to focus on what I thought was important. Also, I think it's often hard to draw the line between what one thinks is important and what is considered important. So everything here should be taken as my personal opinion, which you might share or not.

One more thing that came to mind. You said, "For those who stumbled into the following issues... Here's how I fixed it: ..." And then you provide what looks like a Dockerfile to start from. Not a diff.

The point being, if you wanted to help people who stumbled into specific issues, you should have told what you changed. Like, "I had this line here, and after I changed it to this, it resolved." Ideally, you should have provided "steps to reproduce the issues," or something along those lines.

If you wanted to provide a Dockerfile to start with (a starting-point Dockerfile), then that is a different story.

And to me it looks like you more or less failed at both (no offense meant). The solution is not clear (what exactly solves the issues). And it's not clear what's missing in my version of Dockerfile. But well, it can probably provide some ideas to both ends.

jonathanhefner added a commit to jonathanhefner/railbarge that referenced this issue Mar 8, 2023
With NPM v7 and v8, when running as root, NPM uses the UID and GID of
the current working directory owner to execute scripts (see
[npm/cli#4095][]).

This can cause a problem if `npm install` is run as root in a directory
owned by another user because module installation scripts will instead
use that user's UID and GID but may still try to write to root's home
directory.  For example, Puppeteer will try to download Chromium to
`/root/.cache/puppeteer/chrome`, raising "EACCES: permission denied".

Similarly, this can cause a problem if `npm install` is run as root in a
directory owned by root, and then that directory is later `chown`ed to
another user.  Subsequent NPM commands (e.g. `npm run build`) run as
root will instead use that user's UID and GID, and will raise "EACCES:
permission denied" when touching anything in the root-owned
`node_modules` directory (e.g. `node_modules/.cache`).

NPM v9 removed this behavior (see [npm/rfcs#546][]), but v9 was first
released on 2022-10-24, and v8 is still widespread.

Therefore, as a workaround, instead of `chown`ing the application
directory to a non-root user, this commit `chmod`s the application
directory to grant owner-equivalent permissions to non-root users.
Additionally, this commit `chown`s and `chmod`s the `PACKAGE_JSON_DIR`
directory in the same way.

[npm/cli#4095]: npm/cli#4095
[npm/rfcs#546]: npm/rfcs#546
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests