My website/blog hosted on Codeberg pages. https://maze88.dev
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

239 lines
16 KiB

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-us" xml:lang="en-us">
<head>
<title>The Docker Socket and using Docker from within containers</title>
<meta name="owner" content="Michael Zeevi">
<meta name="designer" content="Michael Zeevi">
<meta name="author" content="Michael Zeevi">
<meta name="copyright" content="Michael Zeevi">
<meta name="date" content="2022-05-01">
<meta name="revised" content="2022-05-01">
<meta name="keywords" content="docker, containers, dockerfile, advanced, socket, permissions, linux, groups, jenkins">
<meta name="generator" content="pandoc"> <!-- template by Michael Zeevi -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes">
<meta name="language" content=”en-us”>
<meta http-equiv="content-language" content=”en-us”>
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta charset="utf-8">
<link rel="stylesheet" href="res/styles.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.5.0/css/all.css" integrity="sha384-B4dIYHKNBt8Bc12p+WXckhzcICo0wtJAoU8YZTY5qE0Id1GSseTk6S+L3BlXeVIU" crossorigin="anonymous">
</head>
<body>
<header class="wrapper">
<h1 id="site-title"><a href="index.html">maze88.dev</a></h1>
<nav>
<ul id="site-menu">
<li><a href="index.html">Home</a></li>
<li><a href="blog.html">Tech Blog</a></li>
<li><a href="links.html">Links</a></li>
<li><a href="photography.html">Photography</a></li>
</ul>
</nav>
</header>
<div id="colorscheme">
<a id="colorscheme-toggle">
<i class="fas fa-fw fa-moon"></i><i class="fas fa-fw fa-toggle-on" id="colorscheme-toggle-switch"></i><i class="fas fa-fw fa-sun"></i>
</a>
<script src="res/colorscheme.js"></script>
</div>
<main>
<hr class="hidden-on-normal-displays">
<header>
<h1 id="content-title">The Docker Socket and using Docker from within containers</h1>
<p class="content-header author">Michael Zeevi</p>
<p class="content-header date"><time datetime="2022-05-01">2022-05-01</time></p>
</header>
<h2 id="intro-and-use-case">Intro and use case</h2>
<p>Sometimes one may want to be able to use Docker from <strong>within</strong> another Docker container. This could be useful in various cases such as:</p>
<ul>
<li>For test running a containerized application as part of a continuous integration (CI) pipeline - where the CI server (such as <em>Jenkins</em>) itself is containerized (this is what we’ll setup in the final demonstration).</li>
<li>To be weaponized as an escape/escalation/pivot method in a security/penetration testing scenario.</li>
<li>When developing a Docker related utility.</li>
<li>Or simply when just learning and hacking around. (:</li>
</ul>
<p>By the end of this article we’ll understand how this can be achieved through an example setup that deploys Jenkins in a container and grants it the appropriate permissions to properly run Docker commands from inside!</p>
<blockquote>
<p><em>Note: This could be considered a slightly advanced topic, so I assume basic familiarity with Linux permissions, Docker and Docker-compose.</em></p>
</blockquote>
<h2 id="docker-architecture-review">Docker architecture review</h2>
<p>Before getting started, let’s refresh ourselves on some of Docker’s architecture (you can find <a target="_blank" href="https://docs.docker.com/get-started/overview/#docker-architecture">a nice diagram in the official documentation</a>) and some terminology...</p>
<ul>
<li>The <em>Docker client</em> (commonly our <code>docker</code> CLI program) is the standard way we interact with Docker’s engine. Docker itself runs as a <em>daemon</em> (server) on the <em>Docker host</em> (this host is often the same machine the client runs on, but in theory can be a remote machine too).</li>
<li>The <em>Docker host</em> exposes the <em>Docker daemon</em>’s <a href="https://docs.docker.com/engine/api/latest/">REST API</a>.</li>
<li>The way the <em>Docker client</em> communicates with the <em>Docker host</em> is via the <em>Docker socket</em>.</li>
</ul>
<h2 id="the-docker-socket">The Docker socket</h2>
<p>A <a target="_blank" href="https://en.wikipedia.org/wiki/Unix_domain_socket"><em>Socket</em></a>, on a Unix system, acts as an endpoint allowing communication between two processes on a host.</p>
<p>The Docker socket is located in <code>/var/run/docker.sock</code>. It enables a Docker client to communicate with the Docker daemon on the Docker host via its API.</p>
<h3 id="communicating-with-the-socket">Communicating with the socket</h3>
<p>Let’s briefly dive one layer deeper and try reach the API server directly. Instead of using the standard CLI and running <code>docker container ls</code>, let’s use <code>curl</code>:</p>
<pre><code>curl --unix-socket /var/run/docker.sock http://api/containers/json | jq</code></pre>
<blockquote>
<p><em>Note: I piped <code>curl</code>’s output into <code>jq</code> to prettify the output. You can <a target="_blank" href="https://stedolan.github.io/jq/">get jq here</a>.</em></p>
</blockquote>
<p>The Docker daemon should return some JSON similar to this (output truncated):</p>
<pre><code>[
{
&quot;Id&quot;: &quot;3c70064d5b8b85688fef7b0eb4d8573967faa5a349b8c9e94d9a175aaf85a59f&quot;,
&quot;Names&quot;: [
&quot;/pensive_lewin&quot;
],
&quot;Image&quot;: &quot;nginx:alpine&quot;,
&quot;ImageID&quot;: &quot;sha256:51696c87e77e4ff7a53af9be837f35d4eacdb47b4ca83ba5fd5e4b5101d98502&quot;,
&quot;Command&quot;: &quot;/docker-entrypoint.sh nginx -g &#39;daemon off;&#39;&quot;,
&quot;Created&quot;: 1650493607,
&quot;Ports&quot;: [
{
&quot;PrivatePort&quot;: 80,
&quot;Type&quot;: &quot;tcp&quot;
}
],
&quot;Labels&quot;: {
...
</code></pre>
<p>Here we can see I have an <em>Nginx Alpine</em> container (named <em>pensive_lewin</em>) running. Cool!</p>
<h3 id="the-sockets-permissions">The socket’s permissions</h3>
<p>Running <code>ls -l /var/run/docker.sock</code> will allow us to see its permissions, owner and group:</p>
<pre><code>srw-rw---- 1 root docker 0 Apr 18 17:17 /var/run/docker.sock</code></pre>
<p>We can see that:</p>
<ul>
<li>The file type is <code>s</code> - it’s a Unix socket.</li>
<li>Its permissions are <code>rw-</code> (<em>read &amp; write</em>) for the <strong>owner</strong> (<em>root</em>)</li>
<li>Its permissions are <code>rw-</code> (<em>read &amp; write</em>) for the <strong>group</strong> (<em>docker</em>)</li>
</ul>
<p>A common practice when setting up Docker is to grant our local user permissions to run <code>docker</code> without <code>sudo</code>, this is achieved by adding our local user to the group <em>docker</em> (<code>sudo usermod -a -G docker $USER</code>)... Now that we’re familiar with the Docker socket and the group it belongs to, the <code>usermod</code> command above should make more sense to you. ;)</p>
<p>We’ll revisit these permissions when we discuss hardening of the Docker image we’ll create.</p>
<h2 id="using-docker-from-within-a-container">Using Docker from within a container</h2>
<p>To use Docker from <em>within</em> a container we need two things:</p>
<ul>
<li>A <em>Docker client</em> - such as the standard <code>docker</code> CLI - which can be installed normally.</li>
<li>A way to reach the Docker host... Normally this is done via the <em>Docker socket</em> (as described above). However, since the container is already running <em>in</em> our Docker host we perform a little “hack” in which we mount the Docker socket (via a Docker volume) into our container.</li>
</ul>
<h3 id="proof-of-concept">Proof of concept</h3>
<p>Let’s put this all together and give it a quick test:</p>
<ol type="1">
<li><p>Run a container locally, mounted with the Docker socket:</p>
<pre><code>docker run --rm -itv /var/run/docker.sock:/var/run/docker.sock --name docker-sock-test debian</code></pre></li>
<li><p>Install the Docker client (inside the container):</p>
<pre><code>apt update &amp;&amp; apt install -y curl
curl -fsSL https://get.docker.com | sh</code></pre></li>
<li><p>Try listing containers using the Docker client (from inside the container):</p>
<pre><code>docker ps</code></pre>
<p>In the output, our container (named <em>docker-sock-test</em>) should be able to see <em>itself</em>:</p>
<pre><code>CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
09094c778449 debian &quot;bash&quot; About a minute ago Up About a minute docker-sock-test</code></pre></li>
</ol>
<h3 id="visual-explanation">Visual explanation</h3>
<p>This configuration and example described above can be visualized with the following diagram:</p>
<p><img src="res/docker-socket/diagram.png" /></p>
<p>Legend:</p>
<ul>
<li>The <em>Docker client</em> (<span style="color:#d7f">pink</span>) is the <code>docker</code> CLI which you should be familiar with. It was installed separately both on the <em>Localhost</em> (<span style="color:#888">grey</span>) and in the <em>Container</em> (<span style="color:#38f">blue</span>).</li>
<li>The Localhost’s <em>Docker socket</em> (<span style="color:#1ab">teal</span>) is mounted into the <em>container</em>. This links the container to the <em>Docker host</em> and exposes its <em>daemon</em>.</li>
<li>The <em>Container</em> was created using the <em>Localhost</em>’s <em>Docker client</em>, via the <code>docker run...</code> command (<span style="color:#e55">red</span>).</li>
<li>The <em>Container</em> can see itself using the <em>Container</em>’s <em>Docker client</em> via the <code>docker ps</code> command (<span style="color:#d71">orange</span>).</li>
</ul>
<h3 id="building-the-docker-image">Building the Docker image</h3>
<p>A classic example (from the DevOps field) would be to deploy a Jenkins container which has Docker capabilities. Let’s start by creating its <code>Dockerfile</code>...</p>
<ol type="1">
<li><p>We’ll base our image on the <a target="_blank" href="https://hub.docker.com/r/jenkins/jenkins">official Jenkins Docker image</a>.</p></li>
<li><p>This image runs by default with user <em>jenkins</em> - a <strong>non</strong>-root user (and <strong>not</strong> in the <em>sudo</em> group), so in order to install the Docker client we’ll escalate to user <em>root</em>.</p></li>
<li><p>We append the user <em>jenkins</em> to the group <em>docker</em>, in order for them to have permissions to access the Docker socket (as discussed earlier).</p></li>
<li><p>This step has a subtle concept to do with Linux permissions, which is worth emphasizing...</p>
<p>The Docker socket on/from our host is associated with the <em>docker</em> group, however there is no guarantee that this is the same <em>docker</em> group in the image (the group that gets created by <em>step 2</em>’s script, and used in <em>step 3</em>, above). Linux groups are defined by IDs - so in order to align the group we set in the image with the group that exist on the host, they must both have the <strong>same</strong> group ID!</p>
<p>The group ID on the host can be looked up with the command <code>getent group docker</code> (mine was <code>998</code>, yours could differ). We’ll pass it to the Docker build via <a target="_blank" href="https://docs.docker.com/engine/reference/commandline/build/#set-build-time-variables---build-arg">an argument</a> and then <a target="_blank" href="https://docs.docker.com/engine/reference/builder/#arg">use that</a> to modify the <em>docker</em> group ID in the image.</p></li>
<li><p>Finally, to re-harden the image, we’ll switch back to the user <em>jenkins</em>.</p></li>
</ol>
<p>Here is the actual <code>Dockerfile</code> (with each of the above steps indicated by a comment):</p>
<pre><code># step 1:
FROM jenkins/jenkins:lts
# step 2:
USER root
RUN curl -fsSL https://get.docker.com | sh
# step 3:
RUN usermod -aG docker jenkins
# step 4:
ARG HOST_DOCKER_GID
RUN groupmod -g $HOST_DOCKER_GID docker
# step 5:
USER jenkins</code></pre>
<h3 id="running-the-container">Running the container</h3>
<p>Due to the nature of the configuration and the dynamic nature of group IDs (differing per each device) I find it simplest to deploy (and build) using Docker-compose.</p>
<p>Here is the <code>docker-compose.yaml</code> file:</p>
<pre><code>version: &quot;3.6&quot;
services:
jenkins:
hostname: jenkins
build:
context: .
args:
HOST_DOCKER_GID: 998 # check *your* docker group id with: `getent group docker`
ports:
- 8080:8080
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jenkins_home:/var/jenkins_home
volumes:
jenkins_home:</code></pre>
<blockquote>
<p><em>Notes:</em></p>
<ul>
<li><p><em>Make sure to check (and set) <strong>your</strong> host’s docker group ID</em>.</p></li>
<li><p><em>This Docker-compose file additionally:</em></p>
<ul>
<li><em>Maps Jenkins’ port to host port <code>8080</code>.</em></li>
<li><em>Creates/uses the named volume <code>jenkins_home</code> to persist any Jenkins data.</em></li>
</ul></li>
</ul>
</blockquote>
<h3 id="testing">Testing</h3>
<p>In order to test the setup, one can:</p>
<ol type="1">
<li><p>Spin up the Jenkins service with:</p>
<pre><code>docker-compose up -d --build</code></pre></li>
<li><p>Login to a shell in the Jenkins container:</p>
<pre><code>docker-compose exec jenkins bash</code></pre></li>
<li><p>In the container, let’s make sure we:</p>
<ul>
<li>Are <strong>not</strong> running as <em>root</em> - by looking at the CLI prompt, or by running <code>whoami</code>.</li>
<li>Have access to the Docker socket - by running any Docker command (such as <code>docker container ls</code>) with the Docker client.</li>
</ul></li>
</ol>
<blockquote>
<p><em>Note: In case of any permission issues, troubleshoot with the <code>getent group docker</code> command on both the host and in the container...</em></p>
</blockquote>
<h2 id="conclusion">Conclusion</h2>
<p>Hopefully this article managed to clarify a thing or two about the Docker socket and how it fits into Docker’s architecture, how its permissions are setup and how it is utilized by a Docker client.</p>
<p>Equipped with said knowledge, we created a Dockerfile for a <em>Jenkins-with-Docker</em> image and saw how to deploy it with the appropriate permissions configuration for <em>our</em> host.</p>
<hr class="hidden-on-normal-displays">
</main>
<footer>
<address>
<ul id="social">
<li><a rel="author" target="_blank" href="https://fosstodon.org/@maze" title="Mastodon: maze@fosstodon.org" ><i class="fab fa-fw fa-mastodon" ></i><div class="hidden-on-tiny-displays"> Mastodon</div></a></li>
<li><a rel="author" target="_blank" href="https://pixelfed.social/@maze88" title="Pixelfed: maze88@pixelfed.social"><i class="fas fa-fw fa-camera" ></i><div class="hidden-on-tiny-displays"> Pixelfed</div></a></li>
<li><a rel="author" target="_blank" href="https://codeberg.org/maze" title="Codeberg: maze" ><i class="fas fa-fw fa-code-branch"></i><div class="hidden-on-tiny-displays"> Codeberg</div></a></li>
<li><a rel="author" target="_blank" href="mailto:michaelzeevi@proton.me" title="E-mail: Michael Zeevi" ><i class="fas fa-fw fa-envelope" ></i><div class="hidden-on-tiny-displays"> E-mail</div></a></li>
<li><a rel="author" target="_blank" href="res/pgp.asc" title="PGP: Public key block" ><i class="fas fa-fw fa-fingerprint"></i><div class="hidden-on-tiny-displays"> PGP</div></a></li>
<li><a rel="author" target="_blank" href="https://keyoxide.org/hkp/a04876190823b1cc383e882d2458479e16ef8831" title="Keyoxide"><i class="fas fa-fw fa-key" ></i><div class="hidden-on-tiny-displays"> Keyoxide</div></a></li>
</ul>
</address>
<p id="license" xmlns:cc="http://creativecommons.org/ns#" xmlns:dct="http://purl.org/dc/terms/">
<a href="index.html" property="dct:title" rel="cc:attributionURL">maze88.dev</a> by
<a target="_blank" href="mailto:michaelzeevi@proton.me" property="cc:attributionName" rel="cc:attributionURL dct:creator">Michael Zeevi</a> is licensed under
<a target="_blank" href="http://creativecommons.org/licenses/by-sa/4.0" rel="license noopener noreferrer">CC BY-SA 4.0</a>
</p>
</footer>
</body>
</html>