How To Run Rootless Docker in Dockerized Jenkins

Introduction

Jenkins is an essential tool in the DevOps world, and countless organizations use it for their CI/CD pipeline needs. However, security concerns can arise when running Jenkins, especially when attempting to mount the /var/run/docker.sock from the host to the dockerized Jenkins pod. By default, the image provided by Jenkins does not run as root, which is a positive from a security standpoint. However, this can lead to issues such as receiving a “permission denied” warning when building a Docker image or executing basic Docker commands. This guide will provide you with a step-by-step approach to running rootless Docker in Dockerized Jenkins, ensuring your security and stability concerns are addressed.

Technical Explanation

First, it’s important to understand the technical processes at play.

When the Jenkins pod initiates a job, it communicates with the Docker Daemon using the mounted Unix /var/run/docker.sock socket. However, the Jenkins pod runs under the user “jenkins,” which has default permissions of UID=1000 and GUI=1000. This can result in a “permission denied” warning when attempting to access the socket. By examining the socket permissions on the host, we can see that it runs under UID=0 and GUI=1001 (Docker), as revealed by executing the stat /var/run/docker.sock command:

$ stat /var/run/docker.sock
  File: /var/run/docker.sock
  Size: 0               Blocks: 0          IO Block: 4096   socket
Device: 17h/23d Inode: 22031       Links: 1
Access: (0660/srw-rw----)  Uid: (    0/    root)   Gid: ( 1001/  docker)
Access: 2021-02-04 14:03:26.650000000 +0200
Modify: 2021-02-04 14:03:26.650000000 +0200
Change: 2021-02-04 14:03:26.650000000 +0200
 Birth: -

This technical issue can create security vulnerabilities and instability in the system, so it’s essential to take the necessary steps to address it.

To automatically retrieve the GID, you can run the following command:

$ stat /var/run/docker.sock | awk '{print $9}' | sed -E '/^$/d' | tr -d '/'

As we mount the Unix socket docker from the host to the container file system, the Docker engine inherits the host permissions to the container. However, this creates a permission denied warning when the user Jenkins (UID=1000 and GUI=1000) attempts to connect. This technical issue can pose security risks and impact system stability, which is why it’s crucial to take appropriate steps to resolve it.

Solution

To resolve this issue, we can create a Dockerfile that removes the pre-existing Jenkins user and recreates it with the desired permissions. We can then add the user “jenkins” to the Docker User Group and delete the shadow package to reduce the image size. The complete Linux Alpine Dockerfile is provided below.

FROM jenkins/jenkins:lts-alpine

USER root

ARG DOCKER_HOST_GID=1001 #Replace with your own docker.sock GID
ARG JAVA_OPTS=""

ENV DOCKER_HOST_GID $DOCKER_HOST_GID
ENV JAVA_OPTS $JAVA_OPTS

RUN set -eux \
&& apk --no-cache update \
&& apk --no-cache upgrade --available \
&& apk --no-cache add shadow \
&& apk --no-cache add docker curl --repository http://dl-cdn.alpinelinux.org/alpine/latest-stable/community \
&& deluser --remove-home jenkins \
&& addgroup -S jenkins -g $DOCKER_HOST_GID \
&& adduser -S -G jenkins -u $DOCKER_HOST_GID jenkins \
&& usermod -aG docker jenkins \
&& apk del shadow

USER jenkins
WORKDIR $JENKINS_HOME

In the Dockerfile, we set the DOCKER_HOST_GID as an environment variable with the default setting of 1001, which is the same as the GID of the Docker User on the Host Machine. By using a Docker-Compose file with a .env file, we can modify this setting automatically without any additional intervention. This approach helps to mitigate security risks and ensure system stability when running Dockerized Jenkins.

Docker-Compose File

The relevant Docker-Compose.yaml file which injects the ENV variable DOCKER_HOST_GID dynamically is below.

version: '3.3'

services:
  jenkins:
    image: jenkins_master
    container_name: jenkins_master
    hostname: jenkins_master
    restart: unless-stopped
    env_file:
      - docker.env
    build:
      context: .
    cpus: 2
    mem_limit: 1024m
    mem_reservation: 800M
  
    ports:
      - 8090:8080
      - 50010:50000
      - 2375:2376
    volumes:
      - ./jenkins_data:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
    networks:
      - default

volumes:
  jenkins_data: {}
networks:
  default:
    driver: bridge

And the actual env file docker.env holding our variables.

DOCKER_HOST_GID=1001 #Replace with your own docker.sock GID
JAVA_OPTS=-Dhudson.slaves.NodeProvisioner.MARGIN0=0.85

The only thing that you need to do is to recreate the image with the command:

$ docker build . -t your-image-tag 

Or even easier would be to just use the Docker-Compose file above that already contains the build command (The image will only build if it doesn’t exist).

$ docker-compose up -d

For the time being, the variables are correctly updated. However, to modify the permissions of the mounted socket, we must elevate our permissions to root and then de-escalate back to the Jenkins user. This means that building the image yourself is necessary to ensure that it functions correctly with your own $DOCKER_HOST_GID variable. By doing so, we can enhance the security and stability of the system while running Dockerized Jenkins.

The same code can be found also on Github.

External resources for further reading:

The resources below provide detailed information on Docker and Jenkins security, best practices, and Dockerfile usage. They can be useful in understanding how to configure and secure Jenkins in a Docker environment.

  1. Docker Security: https://docs.docker.com/engine/security/
  2. Jenkins Documentation: https://www.jenkins.io/doc/
  3. Dockerizing Jenkins: https://www.docker.com/blog/dockerizing-jenkins-2-setup-and-using-docker-build-pipeline-and-docker-workflow-plugin/
  4. Jenkins Security Best Practices: https://www.cloudbees.com/blog/jenkins-security-best-practices
  5. Dockerfile reference: https://docs.docker.com/engine/reference/builder/