Introduction

Since Jenkins is the No1 tool in the DevOps world and many companies are using it for their CI/CD pipeline needs, soon or later they are facing the issue of security. Right out of the box, the image provided by Jenkins does not run as root. These are good news security-wise. The problem though arises when we are attempting to mount the /var/run/docker.sock from the host to the dockerized Jenkins pod and then try to build a docker image or execute certain basic docker commands. In this case, we are getting a “permission denied” warning.

Technical Explanation

First, let’s try to figure out what is happening behind the scenes.

When the Jenkins pod is attempting to run a Docker Based job, it attempts to call the Docker Deamon via the mounted Unix /var/run/docker.sock socket. During this time though, the user that the Jenkins pod is running under (user=Jenkins) comes with the default permissions UID=1000 and GUI=1000. If you will try to examine the socket permissions on the host, you will see that is running under UID=0 and GUI=1001 (Docker) as seen below after executing the stat /var/run/docker.sock command

nikolas@wsl-ubuntu:~$ 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: -

To get GID automaticallyou you can also execute:

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

Because of the fact that we mounted the Unix socket docker from the host to the container file system, docker engine is inheriting the host permissions to the container respectively. That being said, when the user Jenkins (UID=1000 AND GUI=1000) is trying to connect, is getting permission denied warning.

Solution

Since the Jenkins image comes with the pre-created user (Jenkins), the solution is to create a Dockerfile in which we will totally delete the user Jenkins and recreate it with the permissions that we want, while lastly, we add the user Jenkins to the Docker User Group and delete the shadow package to minimize the image size as much as we can. The complete Linx Apline Dockerfile is 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

As you can see the DOCKER_HOST_GID was set as an environment variable with the default setting of 1001 (exactly as GID of the Docker User on the Host Machine. By Using a Docker-Compose file with a .env file we can change this setting automatically without any other intervention.

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 updated as expected, but in order to change the permissions of the mounted socket we need to escalate our permissions to root, and then de-escalate to the Jenkins user again. That means you will have to build the image yourself in order to work as expected with your own $DOCKER_HOST_GID variable.

The same code can be found also on Github.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *