Running a container with a non root user
Running a container with a non root user
TL;DR
One best practice when running a container is to launch the process with a non root user. This is usually done through the usage of the USER instruction in the Dockerfile. But, if this instruction is not present it doesn’t necessary mean the process is run as root.
The rational
By default, root in a container is the same root (uid 0) as on the host machine. If a user manages to break out of an application running as root in a container, he may be able to gain access to the host with the same root user. This access would be ever easier to gain if the container was run with incorrect flags or with bind mouts of host folders in R/W.
Running a MongoDB container
If you do not it yet, I highly recommend to give Play With Docker a try. Aka PWD, it’s a online playground where you can test all the latest Docker features without having to install anything locally. Once in PWD, you can create an instance and you’ll have the feeling to be in the shell of a Linux VM.
Note: under the hood you’ll have a shell but in an Alpine container in which the Docker daemon is installed. That’s what is called DinD, for Docker in Docker, as the Docker daemon runs itself in a container.
Once in the terminal, let’s run a container based on the MongoDB image
[node1] (local) [email protected] ~$ docker container run -d -p 27017:27017 --name mongo mongo:4.08cce38822a23bbacb5349c5af63c50f1d2e371029f5b6332b1144fcc4f8cb723
and check, from the host machine, which user runs the mongod process
[node1] (local) [email protected] ~$ ps aux | grep mongo 1143 999 0:00 mongod --bind_ip_all
From the output above, we can see that the user identified by the uid 999 is the one who owns the mongod process. Let’s check the existing users on the host:
$ cat /etc/passwdroot:x:0:0:root:/root:/bin/bashbin:x:1:1:bin:/bin:/sbin/nologindaemon:x:2:2:daemon:/sbin:/sbin/nologinadm:x:3:4:adm:/var/adm:/sbin/nologinlp:x:4:7:lp:/var/spool/lpd:/sbin/nologinsync:x:5:0:sync:/sbin:/bin/syncshutdown:x:6:0:shutdown:/sbin:/sbin/shutdownhalt:x:7:0:halt:/sbin:/sbin/haltmail:x:8:12:mail:/var/spool/mail:/sbin/nologinnews:x:9:13:news:/usr/lib/news:/sbin/nologinuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologinoperator:x:11:0:operator:/root:/bin/shman:x:13:15:man:/usr/man:/sbin/nologinpostmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologincron:x:16:16:cron:/var/spool/cron:/sbin/nologinftp:x:21:21::/var/lib/ftp:/sbin/nologinsshd:x:22:22:sshd:/dev/null:/sbin/nologinat:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologinsquid:x:31:31:Squid:/var/cache/squid:/sbin/nologinxfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologingames:x:35:35:games:/usr/games:/sbin/nologinpostgres:x:70:70::/var/lib/postgresql:/bin/shcyrus:x:85:12::/usr/cyrus:/sbin/nologinvpopmail:x:89:89::/var/vpopmail:/sbin/nologinntp:x:123:123:NTP:/var/empty:/sbin/nologinsmmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologinguest:x:405:100:guest:/dev/null:/sbin/nologinnobody:x:65534:65534:nobody:/:/sbin/nologindockremap:x:100:101:Linux User,,,:/home/dockremap:/bin/false
There is no user with uid 999, that’s the reason why no user name can be mapped to this uid in the previous command.
Dockerfile
The Dockerfile used to build the MongoDB 4.0 image is the following one :
In this file, there is no USER instruction but we can see a new mongodb user is created in the image, and added to a mongodb group created at the same time. This is what the following instruction is used for:
RUN groupadd -r mongodb && useradd -r -g mongodb mongodb
As it’s not specified through a USER instruction in the Dockerfile, this user is not used during the image construction, everything is done with root.
But, if we have a closer look at the end of the Dockerfile, we can see both ENTRYPOINT and CMD instructions.
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["mongod"]
As you probably know, the concatenation of those two instructions defines the command which is run when the container is started out of the mongo image. The command is then the following one.
$ docker-entrypoint.sh mongod
The ENTRYPOINT
Let’s now take a look into the code of the docker-entrypoint.sh file
The following piece of code at the beginning of the file is very interesting, this is the part where the user executing the process is changed from root to mongodb thanks to the gosu utility.
# allow the container to be started with ` — user# all mongo* commands should be dropped to the correct userif [[ “$originalArgOne” == mongo* ]] && [ “$(id -u)” = ‘0’ ]; then if [ “$originalArgOne” = ‘mongod’ ]; then chown -R mongodb /data/configdb /data/db fi # make sure we can write to stdout and stderr as “mongodb” # (for our “initdb” code later; see “ — logpath” below) chown --dereference mongodb “/proc/$$/fd/1” “/proc/$$/fd/2” || : exec gosu mongodb “$BASH_SOURCE” “[email protected]”fi
Note: we can see in the Dockerfile, that the gosu utility is among the packages installed when the image is created.
Image based on Ubuntu
The first instruction in the Dockerfile indicates that ubuntu:xenial is the base image, the image from which the mongo image is created.
Let’s run an interactive container based on Ubuntu and list the existing users.
$ docker container run -ti ubuntu:[email protected]:/# cat /etc/passwdroot:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologinlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologinproxy:x:13:13:proxy:/bin:/usr/sbin/nologinwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologinbackup:x:34:34:backup:/var/backups:/usr/sbin/nologinlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologinirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologingnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologinnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologinsystemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/falsesystemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/falsesystemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/falsesystemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false_apt:x:104:65534::/nonexistent:/bin/false
Let’s now create a dummy user and group
[email protected]:/# groupadd -r mygrp && useradd -r -g mygrp myuser
and list the users once again:
[email protected]:/# cat /etc/passwdroot:x:0:0:root:/root:/bin/bashdaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinbin:x:2:2:bin:/bin:/usr/sbin/nologinsys:x:3:3:sys:/dev:/usr/sbin/nologinsync:x:4:65534:sync:/bin:/bin/syncgames:x:5:60:games:/usr/games:/usr/sbin/nologinman:x:6:12:man:/var/cache/man:/usr/sbin/nologinlp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologinmail:x:8:8:mail:/var/mail:/usr/sbin/nologinnews:x:9:9:news:/var/spool/news:/usr/sbin/nologinuucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologinproxy:x:13:13:proxy:/bin:/usr/sbin/nologinwww-data:x:33:33:www-data:/var/www:/usr/sbin/nologinbackup:x:34:34:backup:/var/backups:/usr/sbin/nologinlist:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologinirc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologingnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/usr/sbin/nologinnobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologinsystemd-timesync:x:100:102:systemd Time Synchronization,,,:/run/systemd:/bin/falsesystemd-network:x:101:103:systemd Network Management,,,:/run/systemd/netif:/bin/falsesystemd-resolve:x:102:104:systemd Resolver,,,:/run/systemd/resolve:/bin/falsesystemd-bus-proxy:x:103:105:systemd Bus Proxy,,,:/run/systemd:/bin/false_apt:x:104:65534::/nonexistent:/bin/falsemyuser:x:999:999::/home/myuser:
We can see the new user created as the uid 999, which is the uid of the first user created out of a fresh ubuntu:xenial image. This uid is the one used to run the mongod process as we saw before. As a reminder:
[node1] (local) [email protected] ~$ ps aux | grep mongo 1143 999 0:00 mongod --bind_ip_all
Image based on Alpine
Application images are not necessarily based on ubuntu:xenial, a lot of them are based on alpine (tiny distribution focused on security).
Let’s add a new user out of a fresh alpine container and check its uid.
$ docker container run -ti alpine:3.8/ # adduser -D myuser/ # cat /etc/passwdroot:x:0:0:root:/root:/bin/ashbin:x:1:1:bin:/bin:/sbin/nologindaemon:x:2:2:daemon:/sbin:/sbin/nologinadm:x:3:4:adm:/var/adm:/sbin/nologinlp:x:4:7:lp:/var/spool/lpd:/sbin/nologinsync:x:5:0:sync:/sbin:/bin/syncshutdown:x:6:0:shutdown:/sbin:/sbin/shutdownhalt:x:7:0:halt:/sbin:/sbin/haltmail:x:8:12:mail:/var/spool/mail:/sbin/nologinnews:x:9:13:news:/usr/lib/news:/sbin/nologinuucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologinoperator:x:11:0:operator:/root:/bin/shman:x:13:15:man:/usr/man:/sbin/nologinpostmaster:x:14:12:postmaster:/var/spool/mail:/sbin/nologincron:x:16:16:cron:/var/spool/cron:/sbin/nologinftp:x:21:21::/var/lib/ftp:/sbin/nologinsshd:x:22:22:sshd:/dev/null:/sbin/nologinat:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologinsquid:x:31:31:Squid:/var/cache/squid:/sbin/nologinxfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologingames:x:35:35:games:/usr/games:/sbin/nologinpostgres:x:70:70::/var/lib/postgresql:/bin/shcyrus:x:85:12::/usr/cyrus:/sbin/nologinvpopmail:x:89:89::/var/vpopmail:/sbin/nologinntp:x:123:123:NTP:/var/empty:/sbin/nologinsmmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologinguest:x:405:100:guest:/dev/null:/sbin/nologinnobody:x:65534:65534:nobody:/:/sbin/nologinmyuser:x:1000:1000:Linux User,,,:/home/myuser:
As we can see here, the id of the first user in an alpine image is 1000, different from the id 999 of an ubuntu one. If we add a user in an Alpine image and run a process with this user (using the USER instruction in the Dockerfile for instance), we will see the uid 1000 as the owner of the process. Let’s try it.
Let’s use a simple Dockerfile that adds a user to an Alpine image and defines a basic sleep 1000 command.
FROM alpine:3.8RUN adduser -D myuserUSER myuserENTRYPOINT [“sleep”]CMD [“1000”]
Let’s build an image from it
$ docker image build -t sleep:1.0 .Sending build context to Docker daemon 1.775MBStep 1/5 : FROM alpine:3.83.8: Pulling from library/alpine4fe2ade4980c: Pull completeDigest: sha256:621c2f39f8133acb8e64023a94dbdf0d5ca81896102b9e57c0dc184cadaf5528Status: Downloaded newer image for alpine:3.8 — -> 196d12cf6ab1Step 2/5 : RUN adduser -D myuser — -> Running in a7474167f27dRemoving intermediate container a7474167f27d — -> 7a17f0862780Step 3/5 : USER myuser — -> Running in b0a7eea711a4Removing intermediate container b0a7eea711a4 — -> d63533ce5be1Step 4/5 : ENTRYPOINT [“sleep”] — -> Running in f0dfc3ea4495Removing intermediate container f0dfc3ea4495 — -> 763dd8ac4f40Step 5/5 : CMD [“1000”] — -> Running in 14db1ea262f9Removing intermediate container 14db1ea262f9 — -> 978294e76184Successfully built 978294e76184Successfully tagged sleep:1.0
and then run a container from the newly created image
[node1] (local) [email protected] ~$ docker container run -d sleep:1.0534e340780a89b3a86917aff2c20405dadbd7d50cfe5cb03e9cb6786a0517f21
If we check the owner of the sleep process on the host, we can see it belongs to the user with uid 1000, the one that is created in the image.
[node1] (local) [email protected] ~$ ps aux | grep sleep 1181 1000 0:00 sleep 1000
Summary
I hope those examples help to understand some of the ways containers can be run with non root user, either through the image of the USER instruction in the Dockerfile or by changing the user at runtime (usually done within an entrypoint script). Another way we did not cover here would be through the usage of the --user flag when running a container.