File Permissions: the painful side of Docker

0

The source of all evil:

The whole issue with file permissions in docker containers comes from the fact that the Docker host shares file permissions with containers (at least, in Linux). Let me remind you here that file permissions on bind mounts are shared between the host and the containers (of course, there are also a few other ways that file permissions are transferred between host and containers). Whenever we create a file on host using a user with UID x, this files will have x as owner UID inside the container, too. And this will happen no matter if there is a user with UID equal to x inside the container. The same holds whenever a file is created inside the container by a user with UID equal to y (or a process running under this user). This file will appear on the host with owner UID equal to y.

If this sounds strange, it is probably because you lack some basic understanding of how docker works. Remember that Docker containers share the same kernel with the host (the server where Docker daemon runs). Since there is only one kernel, there is also only one set of UIDs and GIDs for the host and all the containers. I am saying “UIDs and GIDs” instead of “users and groups” because the second one is a more generic terminology and I want to point out the difference. User names and group names are not shared! They are not part of the kernel. They are managed by external (to the kernel) tools (like /etc/passwd) who map user names to UID and group names to GIDs. So, you can have the same UID mapped to different user name in each container or the host. And because of that the root user on host is the same with the root user in any container (they have the same UID).

This, in combination with the fact that the default user under which a container is executed is root, can lead to many different kind of complications or troubles. Let’s have a look in a few cases.

First of all, security issues may rise in a production system. If a container is compromised and the container is executed as root (uid = 0), then the intruder has access to any file of the host filesystem that has been loaded to the container filesystem through a mount. The owner UID of files that belong to the host root will be 0 in the container. So, they will be accessible to the intruder.

Another issue is related to the user under which the build process of a docker image is executed. This user is the user under which RUN, CMD and ENTRYPOINT directives of Dockerfile are executed. So, it determines the permissions of files and directories that are created during the build process (e.g when we use composer or Node to retrieve files from external repositories into the container). If this user is the “root”, then these files will not be accessible from web server or the CGI server, except if the server is running as root (something rare and undesirable). What is more, if we use bind mounts for development, these files will probably be not accessible from our IDE (which runs on the host under our local user).

And, of course, files created by the developer (created on host and mounted to the container) will have as owner uid the one of the developer’s host account and will probably be not accessible by the web server that runs inside the container. And this is true not only for the image build process but also for changes that happen in run-time.

Here is a brief example:

Assume that your Apache/PHP container is mounting the host’s /home/alexandros/myapp/ application directory to the container’s /var/www/html directory. Now, let’s assume that the user we are using on host is named alexandros (with UID/GID equal to 1000). Every time we create a new file on host’s application directory (either manually or using our IDE) this new file appears in the container with an owner/group id equal to 1000. This may not be a problem if the container is running under the (default) root user (since root user can access anything) but the Apache in our container runs as user www-data. So, Apache will not be able to modify this file. In case it is not a file but a directory, it may not be able to create a new file inside this directory.

On the other hand, if our application creates a new file inside the container, this file will have www-data as owner. Or, in case our container is running under root and a CMD command (in Dockerfile) or entrypoint script creates new files, these files will be owned by root. In this case, we will not able to modify these files from the host because our user (alexandros) has not write permission to files owned by root.

Reminder: Most files are deployed to a container through:

(a) a COPY directive in dockerfile , (during the image build process)

(b) through a docker cp command, (usually after a docker create command that creates but doesn’t start yet the container)

(c) mounting of a host directory (e.g a bind mount defined in docker run command or in the docker-compose.yml),

(d) a entry script that is executed during the image build process.

In cases (a) and (d), the onwer of the files in the container will be the user under which the container is running. In cases (b) and (c), the file permissions are “inhereted” from host’s filesystem.

Now, let’s see how we can deal with these problems.

Solutions for Production environment

Main goal: All application files should be accessible (readable/writeable) by the container service (e.g web/cgi server).

Peculiarities: The code deployment usually happens through methods (a) or (d). So, the owner will be the user under which the container is running (or “was running” during the build process, e.g when a COPY command was executed).

By default, a container is executed under root. As we already said, for security reasons, it is advised to run the container as a non-privileged user. Starting from Dockerfile, we can change the user under which the container (and so, the image build process) is executed by using the “USER” directive. So, if we have an Apache container:

e.g 
FROM ubuntu:16.04
...
# switch to a non-privileged user
USER apache
...

This will also affect all RUN, CMD and ENTRYPOINT directives located after the USER directive. Remember to create the user, if not already exists:

e.g   RUN groupadd -r apache && adduser apache

Some services (like Apache or PHP-FPM, for those working with PHP) create a user during their installation, so we don’t have to do it ourselves.

As you may see, in case code is deployed through a COPY command. the solution is already there as long as we place the USER directive before the COPY. Otherwise, we can just use a RUN directive and changes the ownership of the application files manually.

e.g 
ARG user=jenkins
...
RUN chown -R ${user} "$JENKINS_HOME" /usr/share/jenkins/ref

Solutions for Local Development Environment

Main goal: All application files should be accessible (readable/writeable) by both the container service and the developer (who lives on the host).

Peculiarities: The code deployment usually happens through methods (c) in order to be able to make changes in run-time. So, permissions are inherited from host.

All solutions boil down to the same concept. Changing user UID to match the owner of the files, or change the file ownership to match the user’s UID. However, setting up a development environment is a bit more complicated than the case of a production system. Do not be tricked! Yes, now we don’t mind running the container as root but remember that many services are running under their own user, which is created during the installation of the service (they may start as root in order to be able to open a socket and then they downgrade to a non-privileged user) . So, files created by the service may not be accessible by the developer and files created by the developer may not be accessible by the service.

There are several ways to handle this issue, but for clarity we will make the following separation:

A. Container services that run as the default user (root)

(a) Create inside the container a user with the same UID as your host user and run the container (or the container service) under this user. You may think that we cannot control the UID that will be assigned to this newly created user (it will probably be a UID around 1000, 1001, 1002,…). Thankfully, we are allowed to set it:

e.g RUN groupadd -r apache -g 1000 && useradd -u 1000 -r -g apache -m -d /opt/apache -s /sbin/nologin -c "Apache user" apache

The problem with this approach is that is not portable. What if I am developing using more than one computers where in each computer my user has different ID ? Or what if many developers use this image for development ? The answer is, probably, that you need to make this change locally (to the local Dockerfile) and keep it locally (don’t commit it). Of course, the image needs to be rebuild whenever the official Dockerfile changes but this should not happen often.

Another problem is that we need to change the owner of the files that have been already created during the installation process of the service and are already owned by root.

Finally, we can’t be sure that the UID of the host user is not already taken by another user already existing in the container. Usually, it shouldn’t but, in any case, this problem can be solved by just chaning the UID of the running container process (no new user is created):

e.g docker run -it --rm -v ~/test-perm:/workdir --workdir /workdir --user $(id -u) alpine:latest touch protected

b) Use ACL for multi-user permissions. When we create the application directory (on host), we can add our user to the list of users that have access to application directory and mark these permissions to be inherited in all files and subdirectories. In that way, the host user will be able to access any files within the application directory without being the owner of them.

(c) Use user namespaces to remap the UIDs/GIDs used inside the container to other UIDs/GIDs outside the container. This is a new feature/technique supported by Docker since 2016. With user remapping, the container user can be considered to be root from container’s viewpoint (so, it can access all files inside the container without a problem) and a non-privileged user outside of the container .

Keep an open eye for entrypoint scripts that need to run with real root privileges (e.g need to modify file permissions and ownership). They may fail in case of user remapping and the solution is usually to maintain an image that is ready to go.

B. Container with services that create their own user

(a) Again, create inside the container a user with the same UID as your host user and run the container (or the container service) under this user. The approach is similar to the first one in section A. If the container service creates a user during installation, you may be able to change the user used by the service through its configuration files.

(b) Change the UID of the service user to the one of the user you are using for development:

RUN usermod -u 1000 www-data

It has the same, more or less, problems with (a).

(c) User namespaces can also be used here but you need to know (or prefix) the UID of the container service in some way.

=====================================================

Here is how you can setup some examples in order to have a first-hand experience on file permissions issues with Docker. I have tested them only in Ubuntu, so keep in mind that there may be a few differences with other linux distributions.

Preparation: Create a directory where we will put the required files to build locally an Alpine Linux image.

$ mkdir alpine
$ cd alpine

Put inside the Dockerfile and the compressed file from Alpine Linux in Docker Hub. The current version at this time is:  https://github.com/gliderlabs/docker-alpine/blob/c14b86580b9f86f42296050ec7564faf6b6db9be/versions/library-3.8/x86_64/Dockerfile

If you dump the Dockerfile contents you will see something like that:

$ cat Dockerfile
FROM scratch
ADD rootfs.tar.xz /
CMD ["/bin/sh"]

Create a subdirectory to be used as a working directory that will be mounted as a volume.

$ mkdir appdir

Now, let’s build an image in order to be able to work locally and uninterrupted from changes to the official image:

$ docker build -t local_alpine .

Case 1: experiencing the problem

Run a container of this image and execute a command that creates an empty file:

$ docker run -it --rm -v ~/alpine/appdir:/workdir --workdir /workdir local_alpine touch alpinefile

You will see that the owner of the created file is root and that you will be unable to edit the file with your user account.

Case 2: Fix by setting the container process user

Since this container process runs under the default user (root) and it does not contain a service that has its own user, we can set the UID of the container process to the UID of the host user. Before running the following command, remember to remove the “alpinefile” that was already created by Case 1.

$ docker run -it --rm -v ~/alpine/appdir:/workdir --workdir /workdir  --user $(id -u) local_alpine touch alpinefile

Now, you see that the created file has our host user as owner and we can edit it.

Case 3: Fix with ACL

Something similar we can achieve through ACL.

$ rm -r appdir
$ setfacl -dm "u:alexandros:rw" ~/alpine

, where alexandros is my host user name (you can replace it with your own). Now we re-create the appdir in order to inherit the new ACL permissions.

$ mkdir appdir
$ docker run -it --rm -v ~/alpine/appdir:/workdir --workdir /workdir local_alpine touch alpinefile

You can see that though the owner of alpinefile is root we are allowed to edit it.

Case 4: Fix with user namespaces

Edit the Docker daemon configuration file and enable namespace remapping by adding the “userns-remap” startup parameter for the Docker engine.

$ sudo nano /etc/docker/daemon.json
{
    "userns-remap": "alexandros"
}

Restart the docker service:

$ sudo service docker restart

Normally, now, you will see the subordinate ID ranges auto-created in the remapping files:

$ sudo cat /etc/subuid
alexandros:100000:65536

$ sudo nano /etc/subguid
alexandros:100000:65536

If these entries (or the files themselves) are not there, create them manually. My host user is “alexandros” and its UID is 1000. So, what I would like is to map the root user of the container to the UID 1000. In that way, any files created by the container process will have as owner (outside of the container) the same UID as my host user and I will be able to access them. To achieve this, edit these 2 files to:

alexandros:1000:65536

What is more, a namespaced storage directory would be there:

$ sudo ls -l /var/lib/docker

Do not forget to restart the docker service everytime you make such a change. Now run again:

$ docker run -it --rm -v ~/alpine/appdir:/workdir --workdir /workdir local_alpine touch alpinefile

You will see that the created alpinefile has owner alexandros.