How to handle permissions in Docker?

DockerDockerBeginner
Practice Now

Introduction

Handling permissions in Docker is a crucial aspect of managing your containerized applications. This tutorial will guide you through understanding Docker file permissions, setting permissions in Docker containers, and exploring best practices for permissions management. By the end, you will have the knowledge to effectively handle permissions in your Docker-based projects.

In this lab, you will learn how Docker container permissions work, how to create and use non-root users in containers, and how to manage permissions when sharing data between the host and containers.

Understanding Default Docker Permissions

In Docker, understanding how permissions work is fundamental to maintaining secure containers. Let us start by exploring the default permission settings in Docker containers.

Checking Default User in Docker

By default, Docker runs processes inside containers as the root user. This can lead to potential security issues if the container is compromised. To see this behavior, let us create a simple container and check which user runs the processes.

First, make sure you are in your project directory:

cd ~/project

Now, run a basic Ubuntu container and check the current user:

docker run -it --rm ubuntu:22.04 whoami

You should see the following output:

root

This confirms that Docker uses the root user by default. Now, let us check the user ID (UID) and group ID (GID):

docker run -it --rm ubuntu:22.04 id

The output should look similar to:

uid=0(root) gid=0(root) groups=0(root)

The uid=0 and gid=0 indicate that the container is running as the root user and group, which have full access to all resources in the container.

Exploring File Permissions Inside a Container

Let us examine how file permissions work inside a Docker container. Create a simple file inside a container and check its permissions.

First, create a container that will run in the background:

docker run -d --name permissions-demo ubuntu:22.04 sleep 3600

Now, let us execute commands inside this container to create a file and check its permissions:

docker exec permissions-demo touch /test-file
docker exec permissions-demo ls -l /test-file

You should see output similar to:

-rw-r--r-- 1 root root 0 May 15 12:34 /test-file

Notice that the file is owned by the root user and group. This demonstrates that all files created inside a Docker container by default are owned by the root user.

Let us create a directory and check its permissions as well:

docker exec permissions-demo mkdir /test-directory
docker exec permissions-demo ls -ld /test-directory

Output should be similar to:

drwxr-xr-x 2 root root 4096 May 15 12:35 /test-directory

Again, the directory is owned by the root user and group.

Let us clean up the container before moving to the next step:

docker stop permissions-demo
docker rm permissions-demo

This demonstrates the default behavior of Docker with regards to user permissions. In the next step, we will learn how to create and use non-root users in Docker containers to improve security.

Creating and Using Non-Root Users in Docker

Running applications as the root user inside Docker containers presents security risks. If an attacker compromises a container running as root, they could potentially gain escalated privileges on the host system. In this step, we will learn how to create and use non-root users in Docker containers.

Creating a Dockerfile with a Non-Root User

Let us create a Dockerfile that defines a non-root user and sets it as the default user for running commands.

First, create a new directory for our project:

mkdir -p ~/project/non-root-user
cd ~/project/non-root-user

Now, create a Dockerfile using the nano text editor:

nano Dockerfile

Add the following content to the Dockerfile:

FROM ubuntu:22.04

## Create a new user called 'appuser' with user ID 1000
RUN useradd -m -u 1000 appuser

## Create a directory for the application and set ownership
RUN mkdir -p /app && chown -R appuser:appuser /app

## Set the working directory to /app
WORKDIR /app

## Switch to the non-root user
USER appuser

## Create a test file
RUN touch test-file.txt

## Command to run when the container starts
CMD ["bash", "-c", "echo 'Running as user:' && whoami && echo 'File ownership:' && ls -l test-file.txt && tail -f /dev/null"]

Save the file by pressing Ctrl+O, then Enter, and exit the editor with Ctrl+X.

This Dockerfile does the following:

  1. Creates a new user called appuser with UID 1000
  2. Creates a directory for the application and gives ownership to appuser
  3. Sets the working directory to /app
  4. Switches to the non-root user for subsequent commands
  5. Creates a test file which will be owned by appuser
  6. Sets a command that will display user and file ownership information

Building and Running the Non-Root User Container

Now, let us build the Docker image from our Dockerfile:

docker build -t non-root-image .

You should see output indicating that Docker is building the image. Once the build is complete, run a container from this image:

docker run --name non-root-container -d non-root-image

Now, let us check the output from our container:

docker logs non-root-container

You should see output similar to:

Running as user:
appuser
File ownership:
-rw-r--r-- 1 appuser appuser 0 May 15 12:45 test-file.txt

This confirms that the container is running as the appuser user, and the test file is owned by appuser.

Testing the Permissions of the Non-Root User

Let us connect to the running container and explore the permissions of our non-root user:

docker exec -it non-root-container bash

Now you should be inside the container as the appuser. Let us verify this:

whoami

You should see:

appuser

Check the user ID and group ID:

id

Output should be similar to:

uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)

Now, try to create a file in the root directory:

touch /root-test-file

You should see a permission denied error:

touch: cannot touch '/root-test-file': Permission denied

This is because the non-root user does not have write permissions in the root directory. However, our user can write to the /app directory:

touch /app/user-test-file
ls -l /app/user-test-file

You should see that the new file is owned by appuser:

-rw-r--r-- 1 appuser appuser 0 May 15 12:50 /app/user-test-file

Exit the container shell:

exit

Let us clean up the container before moving to the next step:

docker stop non-root-container
docker rm non-root-container

This step demonstrated how to create and use a non-root user in a Docker container. Using a non-root user helps improve the security of your containerized applications by restricting the permissions available to the application.

Managing Volume Mount Permissions

Docker volumes allow you to share data between the host and containers. However, managing permissions correctly when using volumes is important to avoid issues. In this step, we will learn how to handle permissions when using volume mounts.

Understanding Volume Mount Permission Issues

The main challenge with Docker volume mounts is that the user IDs inside the container may not match the user IDs on the host system. This can lead to permission issues when accessing files in mounted volumes.

Let us demonstrate this issue with a simple example.

First, create a new directory on the host that we will mount into a container:

mkdir -p ~/project/host-data
cd ~/project/host-data

Create a test file in this directory:

echo "This is a test file created on the host" > host-file.txt

Check the ownership of this file:

ls -l host-file.txt

You should see that the file is owned by the labex user (your current user on the host):

-rw-r--r-- 1 labex labex 39 May 15 13:00 host-file.txt

Now, let us run a container that mounts this directory and try to modify the file:

docker run -it --rm -v ~/project/host-data:/container-data ubuntu:22.04 bash

You are now inside the container. Let us check the ownership of the mounted file:

ls -l /container-data/host-file.txt

You may see output similar to:

-rw-r--r-- 1 1000 1000 39 May 15 13:00 /container-data/host-file.txt

Notice that the file shows numeric IDs instead of user names because the container does not know about the labex user from the host.

Try to create a new file in the mounted directory:

echo "This is a test file created in the container" > /container-data/container-file.txt

Now, check the ownership of this new file:

ls -l /container-data/container-file.txt

You should see:

-rw-r--r-- 1 root root 47 May 15 13:05 /container-data/container-file.txt

The file is owned by the root user inside the container (since we are running as root by default).

Exit the container:

exit

Now, check the ownership of both files on the host:

ls -l ~/project/host-data/

You will see that the file created from inside the container is owned by root on the host:

-rw-r--r-- 1 root  root  47 May 15 13:05 container-file.txt
-rw-r--r-- 1 labex labex 39 May 15 13:00 host-file.txt

This can lead to permission issues if a non-root user on the host needs to access or modify these files.

Solving Volume Permission Issues

There are several ways to solve volume permission issues. Let us explore a few common approaches.

Approach 1: Set User in Dockerfile to Match Host User

Create a new directory for this example:

mkdir -p ~/project/volume-permissions
cd ~/project/volume-permissions

Create a new Dockerfile:

nano Dockerfile

Add the following content:

FROM ubuntu:22.04

## Create a user with the same UID as the host user
RUN useradd -m -u 1000 appuser

## Create app directory and set ownership
RUN mkdir -p /app/data && chown -R appuser:appuser /app

## Set working directory
WORKDIR /app

## Switch to appuser
USER appuser

## Command to run
CMD ["bash", "-c", "echo 'I can write to the mounted volume' > /app/data/test.txt && tail -f /dev/null"]

Save and exit the editor.

Build the image:

docker build -t volume-permissions-image .

Create a host directory for testing:

mkdir -p ~/project/volume-permissions/host-data

Run a container with the volume mounted:

docker run -d --name volume-test -v ~/project/volume-permissions/host-data:/app/data volume-permissions-image

After a moment, check the ownership of the created file on the host:

ls -l ~/project/volume-permissions/host-data/

You should see that the file is owned by a user with UID 1000, which should match your host user's UID:

-rw-r--r-- 1 labex labex 35 May 15 13:15 test.txt

This approach works because we created a user in the container with the same UID as the host user.

Approach 2: Using the --user Flag

Another approach is to use the --user flag to specify which user ID to use when running the container.

First, clean up the previous container:

docker stop volume-test
docker rm volume-test

Now run a container with the --user flag:

docker run -d --name user-flag-test --user "$(id -u):$(id -g)" -v ~/project/volume-permissions/host-data:/data ubuntu:22.04 bash -c "echo 'Created with --user flag' > /data/user-flag-test.txt && sleep 3600"

This command runs the container with your current user ID and group ID.

Check the ownership of the new file:

ls -l ~/project/volume-permissions/host-data/user-flag-test.txt

You should see that the file is owned by your host user:

-rw-r--r-- 1 labex labex 23 May 15 13:20 user-flag-test.txt

Let us clean up before moving to the next step:

docker stop user-flag-test
docker rm user-flag-test

These approaches demonstrate how to manage permissions when using Docker volumes. By matching the user IDs between the container and the host, you can avoid permission issues when sharing data between them.

Implementing Permission Best Practices

Let us put everything we have learned into practice by creating a more realistic example of a Docker container that follows permission best practices. We will create a simple web application container that follows security best practices for permissions.

Creating a Secure Web Application Container

First, create a new directory for our example:

mkdir -p ~/project/secure-app
cd ~/project/secure-app

Let us create a simple web application. First, create an app.py file:

nano app.py

Add the following Python code to create a simple Flask web server:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello from a secure container!'

@app.route('/whoami')
def whoami():
    return os.popen('id').read()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8080)

Save and exit the editor.

Now, create a requirements.txt file for the Python dependencies:

nano requirements.txt

Add the following content:

flask==2.0.1

Save and exit the editor.

Now, create a Dockerfile that follows permission best practices:

nano Dockerfile

Add the following content:

FROM python:3.10-slim

## Create a non-root user to run the application
RUN groupadd -g 1000 appgroup \
  && useradd -u 1000 -g appgroup -s /bin/bash -m appuser

## Set working directory and create necessary directories
WORKDIR /app

## Copy requirements first to leverage Docker cache
COPY requirements.txt .

## Install dependencies as root
RUN pip install --no-cache-dir -r requirements.txt

## Copy application code
COPY app.py .

## Create a data directory that the application can write to
RUN mkdir -p /app/data \
  && chown -R appuser:appgroup /app

## Set proper permissions
RUN chmod -R 755 /app

## Switch to non-root user
USER appuser

## Expose the port the app will run on
EXPOSE 8080

## Command to run the application
CMD ["python", "app.py"]

Save and exit the editor.

Let us build the Docker image:

docker build -t secure-web-app .

Now, run the container:

docker run -d --name secure-app -p 8080:8080 secure-web-app

Let us test the application. First, check if the container is running:

docker ps

You should see your secure-app container in the list. Now, use curl to test the application:

curl http://localhost:8080/

You should see:

Hello from a secure container!

Let us check which user the application is running as:

curl http://localhost:8080/whoami

You should see output similar to:

uid=1000(appuser) gid=1000(appgroup) groups=1000(appgroup)

This confirms that our application is running as the non-root appuser.

Security Analysis

Let us analyze the security improvements in our Dockerfile:

  1. Non-root user: We created a dedicated user (appuser) to run the application, reducing the risk if the container is compromised.

  2. Minimal permissions: We set only the necessary permissions required for the application to function.

  3. Clear ownership: All files and directories in the container have clearly defined ownership.

  4. Proper directory structure: We created a dedicated directory structure for the application and its data.

These practices help ensure that even if an attacker manages to exploit a vulnerability in the application, they will have limited access to the container and the host system.

Implementing Volume Permissions with the Secure App

Let us add a volume to our secure application to demonstrate how to handle permissions properly with volumes.

First, stop and remove the existing container:

docker stop secure-app
docker rm secure-app

Create a data directory on the host:

mkdir -p ~/project/secure-app/host-data

Set the correct permissions on the host directory:

sudo chown 1000:1000 ~/project/secure-app/host-data

Now, run the container with the volume mounted:

docker run -d --name secure-app-with-volume \
  -p 8080:8080 \
  -v ~/project/secure-app/host-data:/app/data \
  secure-web-app

Let us connect to the container and create a file in the mounted volume:

docker exec -it secure-app-with-volume bash

Now, inside the container, create a test file in the mounted volume:

echo "Test file created from inside the container" > /app/data/container-file.txt

Check the file ownership:

ls -l /app/data/container-file.txt

You should see that the file is owned by appuser:

-rw-r--r-- 1 appuser appgroup 43 May 15 13:30 /app/data/container-file.txt

Exit the container:

exit

Now, check the file ownership on the host:

ls -l ~/project/secure-app/host-data/container-file.txt

You should see that the file is owned by UID 1000 (which corresponds to your host user):

-rw-r--r-- 1 labex labex 43 May 15 13:30 container-file.txt

This demonstrates that with proper permission configuration, files created inside the container in a mounted volume will have the correct ownership on the host.

Let us clean up before concluding:

docker stop secure-app-with-volume
docker rm secure-app-with-volume

By following these best practices for Docker permissions, you can create secure, reliable containerized applications that properly manage file permissions both inside the container and when sharing data with the host system.

Summary

In this lab, you have learned essential techniques for managing permissions in Docker containers:

  1. Understanding Default Docker Permissions: You explored how Docker containers run as root by default and how this can lead to potential security risks.

  2. Creating and Using Non-Root Users: You learned how to create custom non-root users in Docker containers, which is a critical security best practice.

  3. Managing Volume Mount Permissions: You discovered how to handle permissions when sharing data between the host and containers using volumes, addressing common permission issues.

  4. Implementing Permission Best Practices: You applied all these concepts to create a secure web application container that follows Docker permission best practices.

By applying these techniques in your Docker projects, you can significantly improve the security and reliability of your containerized applications. Remember, running containers with the least privileges needed is a fundamental security principle that should always be followed.

Some key takeaways:

  • Always use non-root users in your containers
  • Match user IDs between the host and container when using volumes
  • Set proper file and directory permissions
  • Follow the principle of least privilege

These practices will help you build more secure Docker containers and avoid common permission-related issues.