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.
- Docker Security: https://docs.docker.com/engine/security/
- Jenkins Documentation: https://www.jenkins.io/doc/
- Dockerizing Jenkins: https://www.docker.com/blog/dockerizing-jenkins-2-setup-and-using-docker-build-pipeline-and-docker-workflow-plugin/
- Jenkins Security Best Practices: https://www.cloudbees.com/blog/jenkins-security-best-practices
- Dockerfile reference: https://docs.docker.com/engine/reference/builder/