Docker User

Use non-root user inside Docker container.

The problem

Docker makes local environment same as CI and production, right? Well, in theory. Recently we had a single test failing in CI (dockerized CircleCI v2) but passing in a Docker container running locally. What the hell!

The test basically checked file permissions. We were creating a folder, then changing its permission to disallow listing its contents, and then made an attempt to list the contents. We were expecting the attempt to fail - and when running in plain Mac OSX terminal it did.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// the setup
const fs = require('fs')
const path = require('path')
const execa = require('execa')
const name = path.resolve('foo')
if (!fs.existsSync(name)) {
fs.mkdirSync(name)
console.log('made folder %s', name)
}
console.log('working with folder %s', name)
fs.chmodSync(name, '111')
console.log('changed folder permissions')
console.log(execa.shellSync(`ls -ld "${name}"`).stdout)
// the failure
try {
console.log(execa.shellSync(`ls "${name}"`).stdout)
console.error('😡 could list directory without permissions')
process.exit(-1)
} catch (err) {
console.log('✅ caught permissions exception!')
console.log(err.message)
}

We placed this code inside a Docker image using the following Dockefile. You can find the example code in repo bahmutov/docker-file-permissions

  • the problematic Dockerfile is in branch problem.
1
2
3
4
5
6
FROM node:6
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
COPY index.js .

I built the container and ran it

1
2
3
4
5
6
7
8
9
> docker run --name perms -it gleb/docker-file-permissions /bin/bash

root@97d0341957e8:/usr/src/app# node .
made folder /usr/src/app/foo
working with folder /usr/src/app/foo
changed folder permissions
d--x--x--x 2 root root 4096 Jun 1 04:19 /usr/src/app/foo

😡 could list directory without permissions

We can list the current user in the container (which is root with id 0) and the permissions on the folder foo. I am using commands id, ls -ld and stat present on Linux and Mac to inspect user and file details.

1
2
3
4
5
6
7
8
9
10
11
12
13
root@97d0341957e8:/usr/src/app# id
uid=0(root) gid=0(root) groups=0(root)
root@97d0341957e8:/usr/src/app# ls -ld foo
d--x--x--x 2 root root 4096 Jun 1 04:19 foo
root@97d0341957e8:/usr/src/app# stat foo
File: 'foo'
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 2fh/47d Inode: 141 Links: 2
Access: (0111/d--x--x--x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2017-06-01 04:19:16.970982104 +0000
Modify: 2017-06-01 04:19:16.950996565 +0000
Change: 2017-06-01 04:19:16.960989334 +0000
Birth: -

So we see that we are user with id=0 from group gid=0 and the file belongs to this user and group. Despite changed permissions, the folder is still accessible.

The solution

After reading about Docker treats user ids I changed the Docker container to have new non-root user. This is a good Docker security practice in general. The Dockefile required only a few changes:

  1. Create a new user person and switch to it
  2. Copy and install app inside the new user's home folder.
1
2
3
4
5
6
7
8
9
10
11
12
FROM node:6
# run as non-root user inside the docker container
# see https://vimeo.com/171803492 at 17:20 mark
RUN groupadd -r regular-users && useradd -m -r -g regular-users person
# now run as the new "non-root" user
USER person
# Now we are restricted to /home folder
RUN mkdir -p /home/person/app
WORKDIR /home/person/app
COPY package.json .
RUN npm install
COPY index.js .

If we had global NPM installs, we would need to give user person permissions to /usr/local/node folder. In our example everything was local (as it should be in general).

Rebuilding the image and running the container catches the expected exception

1
2
3
4
5
6
7
[email protected]:~/app$ node .
made folder /home/person/app/foo
working with folder /home/person/app/foo
changed folder permissions
d--x--x--x 2 person regular-users 4096 Jun 1 04:27 /home/person/app/foo
✅ caught permissions exception!
ls: cannot open directory /home/person/app/foo: Permission denied

Inspecting the user and the folder shows the non-root owner

1
2
3
4
5
6
7
8
9
10
11
12
13
[email protected]:~/app$ id
uid=999(person) gid=999(regular-users) groups=999(regular-users)
[email protected]:~/app$ ls -ld foo
d--x--x--x 2 person regular-users 4096 Jun 1 04:27 foo
[email protected]:~/app$ stat foo
File: 'foo'
Size: 4096 Blocks: 8 IO Block: 4096 directory
Device: 2fh/47d Inode: 145 Links: 2
Access: (0111/d--x--x--x) Uid: ( 999/ person) Gid: ( 999/regular-users)
Access: 2017-06-01 04:27:48.789991546 +0000
Modify: 2017-06-01 04:27:48.789991546 +0000
Change: 2017-06-01 04:27:48.799984304 +0000
Birth: -

Glad it is working!

Update 1

Geoff Goodman has pointed out that our problem was not with user namespaces (how Docker can map internal user to external user), but with root user permission. In fact, he asked, how could our local Docker test pass when we were running as root?!

The full picture: in order to save time and avoid building Docker image with full source code, we only build the run environment and mapped source folder as data volume at runtime. The Dockerfile has nothing, just plain Node image (I pushed this code as mapped branch)

1
2
3
4
5
FROM node:6
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app
# Nothing to copy!

# The source code will be mapped at runtime

Running container with mapped current folder brings source

1
$ docker run --name perms -v $PWD:/usr/src/app -it gleb/docker-file-permissions /bin/bash
1
2
3
4
5
6
root@50415a48be51:/usr/src/app# node .
✅ caught permissions exception!
root@50415a48be51:/usr/src/app# id
uid=0(root) gid=0(root) groups=0(root)
root@50415a48be51:/usr/src/app# ls -ld foo
d--x--x--x 2 root root 68 May 31 14:01 foo

So running as root with mapped data volume on Mac OSX (Docker 17.06.0-rc1-ce-mac13) hides the problem, which becomes apparent when running Docker on true Linux box.

Update 2

Same Geoff Goodman also reminded me that Node Docker image has a non-root user already named node (surprise, surprise). I could have used it and not create my own person. In my Docker file I could have switched to this user with USER node or when running the container -u node - this is assuming we gave this user permission to create folders in the container.