docker-compose user mapping [duplicate] - docker

I would like to volume mount a directory from a Docker container to my work station, so when I edit the content in the volume mount from my work station it updated in the container as well. It would be very useful for testing and develop web applications in general.
However I get a permission denied in the container, because the UID's in the container and host isn't the same. Isn't the original purpose of Docker that it should make development faster and easier?
This answer works around the issue I am facing when volume mounting a Docker container to my work station. But by doing this, I make changes to the container that I won't want in production, and that defeats the purpose of using Docker during development.
The container is Alpine Linux, work station Fedora 29, and editor Atom.
Question
Is there another way, so both my work station and container can read/write the same files?

There are multiple ways to do this, but the central issue is that bind mounts do not include any UID mapping capability, the UID on the host is what appears inside the container and vice versa. If those two UID's do not match, you will read/write files with different UID's and likely experience permission issues.
Option 1: get a Mac or deploy docker inside of VirtualBox. Both of these environments have a filesystem integration that dynamically updates the UID's. For Mac, that is implemented with OSXFS. Be aware that this convenience comes with a performance penalty.
Option 2: Change your host. If the UID on the host matches the UID inside the container, you won't experience any issues. You'd just run a usermod on your user on the host to change your UID there, and things will happen to work, at least until you run a different image with a different UID inside the container.
Option 3: Change your image. Some will modify the image to a static UID that matches their environment, often to match a UID in production. Others will pass a build arg with something like --build-arg UID=$(id -u) as part of the build command, and then the Dockerfile with something like:
FROM alpine
ARG UID=1000
RUN adduser -u ${UID} app
The downside of this is each developer may need a different image, so they are either building locally on each workstation, or you centrally build multiple images, one for each UID that exists among your developers. Neither of these are ideal.
Option 4: Change the container UID. This can be done in the compose file, or on a one off container with something like docker run -u $(id -u) your_image. The container will now be running with the new UID, and files in the volume will be accessible. However, the username inside the container will not necessarily map to your UID which may look strange to any commands you run inside the container. More importantly, any files own by the user inside the container that you have not hidden with your volume will have the original UID and may not be accessible.
Option 5: Give up, run everything as root, or change permissions to 777 allowing everyone to access the directory with no restrictions. This won't map to how you should run things in production, and the container may still write new files with limited permissions making them inaccessible to you outside the container. This also creates security risks of running code as root or leaving filesystems open to both read and write from any user on the host.
Option 6: Setup an entrypoint that dynamically updates your container. Despite not wanting to change your image, this is my preferred solution for completeness. Your container does need to start as root, but only in development, and the app will still be run as the user, matching the production environment. However, the first step of that entrypoint will be to change the user's UID/GID inside the container to match your volume's UID/GID. This is similar to option 4, but now files inside the image that were not replaced by the volume have the right UID's, and the user inside the container will now show with the changed UID so commands like ls show the username inside the container, not a UID to may map to another user or no one at all. While this is a change to your image, the code only runs in development, and only as a brief entrypoint to setup the container for that developer, after which the process inside the container will look identical to that in a production environment.
To implement this I make the following changes. First the Dockerfile now includes a fix-perms script and gosu from a base image I've pushed to the hub (this is a Java example, but the changes are portable to other environments):
FROM openjdk:jdk as build
# add this copy to include fix-perms and gosu or install them directly
COPY --from=sudobmitch/base:scratch / /
RUN apt-get update \
&& apt-get install -y maven \
&& useradd -m app
COPY code /code
RUN mvn build
# add an entrypoint to call fix-perms
COPY entrypoint.sh /usr/bin/
ENTRYPOINT ["/usr/bin/entrypoint.sh"]
CMD ["java", "-jar", "/code/app.jar"]
USER app
The entrypoint.sh script calls fix-perms and then exec and gosu to drop from root to the app user:
#!/bin/sh
if [ "$(id -u)" = "0" ]; then
# running on a developer laptop as root
fix-perms -r -u app -g app /code
exec gosu app "$#"
else
# running in production as a user
exec "$#"
fi
The developer compose file mounts the volume and starts as root:
version: '3.7'
volumes:
m2:
services:
app:
build:
context: .
target: build
image: registry:5000/app/app:dev
command: "/bin/sh -c 'mvn build && java -jar /code/app.jar'"
user: "0:0"
volumes:
- m2:/home/app/.m2
- ./code:/code
This example is taken from my presentation available here: https://sudo-bmitch.github.io/presentations/dc2019/tips-and-tricks-of-the-captains.html#fix-perms
Code for fix-perms and other examples are available in my base image repo: https://github.com/sudo-bmitch/docker-base

Since the UID in your containers are baked into the container definition, you can safely assume that they are relatively static. In this case, you can create a user in your host system with the machine UID and GID. Change user to the new account, and then make your edits to the files. Your host OS will not complain since it thinks it's just the user accessing its own files, and your container OS will see the same.
Alternatively, you can consider editing these files as root.

Related

How to securely add an entry into a docker container's /etc/passwd for the uid set with docker's --user option

Problem
For a docker image (alpine based) that is supposed to run as non-root I have two requirements:
I have to mount a FUSE filesystem inside the docker container
The users of the docker image are able to set the UID/GID of the docker
user with docker run --user {uid}:{gid}
FUSE's fusermount command requires a valid entry for the user in /etc/passwd, otherwise it won't mount the filesystem. Given that I don't know the the UID/GID of the user at build time I can't call adduser at build time. And I can't do it at runtime either, as the user then doesn't have the appropriate privileges.
Solutions found
So far I have found two solutions that both feel not appropriate/secure
1. Make /etc/passwd writable
When adding chmod 555 /etc/passwd to the Dockerfile I can then do at runtime
echo "someuser:x:${my_uid}:$(id -g)::/tmp:/sbin/nologin" >> /etc/passwd
This does the job for fusermount. Unfortunately I did not find a way to make change the passwd file back to read-only at runtime and without that I have security concerns that someone might be able to misuse this to gain root rights back. While I could not find a simple way to use the open passwd file for some exploit (while I was able to add/modify password & configurations directly in /etc/passwd for all users and then change users via login, alpine did not allow this for user root (neither via login nor via su). But I guess there are folk out there more clever than me, and somehow the whole solution feels like a quite dirty hack. Does anyone have specific ideas how a writeable passwd file inside a container could be used for getting inappropriate rights inside the container?
2. Replace requirement #2 with two additional environment variables
By introducing DUID and DGID as environment variables and set USER to some newly added non-root user inside the Dockerfile I found a solution with the help of sudo & /etc/sudoers: In a launch script that I use as entrypoint I can call sudo adduser/addgroup for the given DUID/DGID and then launch the actual program with the user specified via sudo -u someuser someprog.
Except for the fact that the whole setup became quite ugly, I disliked the fact the user's of my docker image could no longer use the regular docker run --user option, as this would break the sudo configuration.

Dockerfile hide variables (user creation)

I am trying to generate a docker image from Ubuntu 18.04.
To administrate the container I am creating a default user :
# set default user
RUN useradd -m docker && echo "docker:docker" | chpasswd && adduser docker sudo
USER docker
My problem is I would like to set a secured password on it, and my dockerfile is intended to be versioned with Git.
So my question is : is there a way to load variables in dockerfile from a .env file or anything else ?
I have seen an option on the docker run command, but not for the docker build, am I wrong ?
Anything you write in the Dockerfile can be trivially retrieved in plain text with docker history. Any file in the image can be very easily retrieved by anyone who can run any docker command. There is no way around either limitation.
Do NOT try to set user passwords for your Docker images like this. In most cases it shouldn't be necessary to formally "log in" to a container at all. Let the container run the single application process it needs to run, and don't try to set up an ssh daemon, sudo, or other things you'd have in a more complete server environment.
(You shouldn't usually need a shell inside a container; you don't for other kinds of processes like your Nginx server, for example. If you do, you can get one with docker exec, and if your main process runs as a non-root user, you can add a -u root option to be root in that shell. Again, you can't prevent an end user from being able to do this.)
If you are using a standalone container, then you can use a script with the variables and run docker RUN, or ENTRYPOINT to run the script. This would contain your password information, and then you can carry on with the build of your image.
If you are using Docker Swarm, you can use secrets, more information on the following link, and differences if you are using Windows or Linux are explained as well.
https://docs.docker.com/engine/swarm/secrets/

Docker File Non Root User Theory Question

I've been reading today on the theory behind uid 1001 specifically in Docker where it is a best principle not to have your container running as the root user.
What I've been able to tell so far for a unix system is...
root user has the UID of 0 Most unix distributions reserve the first
100 New users are assigned UIDs starting from 500 or 1000
When you
create a new account, it will usually be give the next-highest unused
number of 1001 (not sure what this means in relation to the previous dot point. If someone could clarify please)
I've seen a lot of Dockerfile examples that will just try to use 1001.
USER 1001
Two questions
Is the theory 1001 is safe to use because it is above the uid allocations ranges for new users on the host i.e. dot point 2?
Is it best practice to specify user as 1001 or would it be best adding a new user?
RUN useradd -ms /bin/bash newuser
USER newuser
WORKDIR /home/newuser
thank you
If your container doesn't need to write data in a named volume or a directory bind-mounted from the host, it usually doesn't matter at all what user ID the container runs as. There are a couple of restrictions still (if you're trying to listen on a port number less than 1024 your user ID must be 0 or you must manually add a capability at startup time).
I would use your second form, except I would not switch users until the end of the Dockerfile.
FROM python:3.8 # arbitrary choice
# ... install OS packages ...
# Create the user. This doesn't need to be repeated if the
# application code changes. It is a "system" user without
# a home directory and a default shell. We don't care what
# its numeric user ID is.
RUN useradd -r newuser
# WORKDIR creates the directory. It does not need to be
# under /home. It should be owned by root.
WORKDIR /app
# Copy the application in and do its installation, still as root.
COPY requirements.txt .
RUN pip3 install -r requirements.txt
COPY . .
# Switch USER only at the end of the file.
USER newuser
CMD ["./app.py"]
"Home directory" isn't usually a well-defined concept in Docker; I've created the user without a specific home directory here, and the application directory isn't under the normal Linux /home directory. Similarly, I haven't gone out of my way to specify a shell for this user or to specifically use GNU bash.
One important security point of this setup is that root owns the application files, but newuser is running the code. If there is some sort of compromise, this gives you an additional layer of protection: the compromised application code can't overwrite the application code or its fixed static data.
I started this with the caveat that this works fine if you don't need to persist data in the filesystem. If it does it will need to be somewhat adaptable, which probably means starting up as root but then dropping privileges. I'd support two modes of running:
The container is started up initially with a totally empty directory, possibly owned by root (a named Docker volume; a Kubernetes named volume). In this case, create the data directory you need and make it owned by the user in the Dockerfile.
docker run -v somevolume:/data myimage
The container is started up with a bind-mounted host directory and also a -u option naming the host user ID to use.
docker run -u $(id -u) "$PWD/data:data" myimage
You would need to use an entrypoint wrapper script to detect which case you're in, create the initial storage structure, set its permissions, and switch to a non-root user if required. There are lighter-weight tools like gosu or su-exec that specifically support this case. The Docker Hub consul image's entrypoint script has an example of doing this at startup time.

Making docker container write files that the host machine can delete

I have a docker-based build environment - in order to build my project, I run a docker container with the --volume parameter, so it can access my project directory and build it.
The problem is that the files created by the container cannot be deleted by the host machine. The only workaround I currently have is to start an interactive container with the directory mounted and delete it.
Bottom line question: It is possible to make docker write to the mounted area files with permissions such that the host can later delete them?
This has less to do with Docker and more to do with basic Unix file permissions. Your docker containers are running as root, which means any files created by the container are owned by root on your host. You fix this the way you fix any other file permission problem, by either (a) ensuring that that the files/directories are created with your user id or (b) ensuring that permissions allow you do delete the files even if they're not owned by you or (c) using elevated privileges (e.g., sudo rm ...) to delete the files.
Depending on what you're doing, option (a) may be easy. If you can run the contanier as a non-root user, e.g:
docker run -u $UID -v $HOME/output:/some/container/path ...
...then everything will Just Work, because the files will be created with your userid.
If the container must run as root initially, you may be able to take care of root actions in your ENTRYPOINT or CMD script, and then switch to another uid to run the main application. To do this, you would need to pass your user id into the container (e.g., as an environment variable), and then later use something like runuser to switch to the new userid:
exec runuser -u $TARGE_UID /some/command
If neither of the above is an option, then sudo rm -rf mydirectory should work just as well as spinning up an interactive container.
If you need your build artifacts just to put them to the docker image on the next stage then it is probably worth to use multi-stage build option.

Synchronizing numeric user id's between Dockerfiles and docker-compose.yml?

I have a docker-compose newbie question. We have an existing Jenkins build that creates Docker images and pushes them to an in house Artifactory repository. This is driven by using Maven/Docker and two Dockerfiles, one for the app and one for a volume/data container. The Dockerfiles look something like this:
App:
FROM centos
RUN useradd -u 6666 -ms /bin/bash foouser
COPY src/main/resources/home/foouser/.bashrc /home/foouser/
RUN chown -R foouser:foouser /home/foouser
USER foouser
COPY src/main/resources/opt/myapp/bin/startup.sh /opt/myapp/bin/
WORKDIR /home/foouser
ENTRYPOINT /opt/myapp/bin/startup.sh && /bin/bash
Data container:
FROM centos
# Environment variable for the path to mount/create. Defaults to /opt/data
ENV DATA_VOL_PATH="/opt/data"
# Make sure the user id is the same as the container using the volume, otherwise we may run into permission issues on
# the container mounting the volume.
RUN useradd -u 6666 -ms /bin/bash foouser && \
mkdir -p "$DATA_VOL_PATH" && \
chown -R foouser:foouser "$DATA_VOL_PATH"
VOLUME [ "$DATA_VOL_PATH" ]
I omitted stuff like labels, etc for brevity. So the images produced by the build from these Dockerfiles will end up in the local Artifactory repo. We're using Rancher/Cattle to instantiate these images, and I've added the Artifactory repo to Rancher so it can pull from there. The docker-compose.yml file in Rancher looks something like this:
# The data/volumes container for the data.
data:
image: data-image
# App
myapp:
image: app-image
environment:
DATA_VOL_PATH:
volumes_from:
- data
I know that I can pass environment variables from docker-compose (as in the DATA_VOL_PATH above), but I'm confused as to how things work. My understanding is that the commands in the Dockerfile are executed when I run docker build, and after that, the image is immutable. When I instantiate a container based on the image, it creates a new writable UFS layer on top of it if I've understood things correctly. So in the case of the data container, I can't really change the volume once it's created, right? If that assumption is correct, it boils down to 1) how do I best synchronize user ids across two different system (Maven for creating the Docker images, and Rancher for instantiating container clusters), and 2) is it better to drive the creation of the data volume container entirely from docker-compose.yml? How would I then be able to replicate the data container's Dockerfile content in docker-compose.yml?
I assume this is a fairly common scenario, so there must be a few "best practices" solutions out there. Thanks.
I've run into this issue as well: in my case there was a non-root process inside Docker container, and this process needed to access files mounted from host, so I had to pay attention to UID/GID values both inside Docker image and host.
I'm afraid that there is no good decision for it. Docker images are really immutable, so you'll have to establish some agreements or use third-party software to control both build and deployment process.
I would also strongly encourage you to stop using docker-compose and to give a try to SaltStack.

Resources