~/home/study/understanding-docker-daemon

Understanding Docker Daemon Architecture & API - An Introductory Guide

Learn how the Docker daemon works, explore its REST API, socket listeners, authentication methods, and common security pitfalls. Ideal for security professionals familiar with Docker basics.

Introduction

Docker has become the de-facto platform for container orchestration in modern DevOps pipelines. At the heart of every Docker host lies the Docker daemon (commonly dockerd), a privileged background service that manages container lifecycles, images, networks, and volumes. The daemon exposes a RESTful API that the docker CLI, third-party tools, and CI/CD systems use to drive container operations.

From a security standpoint, the daemon is a high-value target: it runs as root, holds the host’s namespace, and can launch arbitrary processes. Understanding its architecture, how it communicates, and the default security posture is essential for hardening Docker hosts and detecting abuse.

Real-world relevance includes:

  • Detecting mis-configured remote APIs that allow unauthenticated command execution.
  • Assessing privilege-escalation pathways when an attacker gains docker group membership.
  • Designing network segmentation and TLS policies for container-native workloads.

Prerequisites

  • Basic Docker usage: docker run, docker ps, image management.
  • Familiarity with Linux command line, process management, and networking fundamentals (ports, sockets, TLS).
  • Understanding of JSON data structures and HTTP basics.

Core Concepts

The Docker daemon is a multi-component system built on top of several open-source projects. Its primary responsibilities include:

  1. Container lifecycle management: create, start, stop, and delete containers.
  2. Image handling: pull, push, store, and unpack OCI images.
  3. Network orchestration: bridge, overlay, and host networking.
  4. Volume management: mount host directories or volume plugins.

All of these are exposed via a single HTTP/HTTPS endpoint. Internally, dockerd delegates low-level container operations to containerd, which in turn relies on runc (the OCI runtime) to create Linux namespaces and cgroups.

A simplified diagram (described in words) would be:

CLI/SDK → Docker Daemon (dockerd) → containerd → runc → kernel namespaces/cgroups.

Because the daemon runs with elevated privileges, any compromise of the API or the socket gives an attacker near-root control of the host. This is why the API surface, authentication mechanisms, and default hardening are critical security concerns.

Docker daemon components (dockerd, containerd)

dockerd is the primary entry point. It parses the /etc/docker/daemon.json configuration, starts the HTTP listener (Unix socket or TCP), and launches the containerd daemon.

containerd is a lightweight, purpose-built runtime that manages the full container lifecycle (image transfer, snapshotting, execution). It exposes its own gRPC API on /run/containerd/containerd.sock, used internally by Docker and other platforms such as Kubernetes.

Key interactions:

  • dockerd receives a request to start a container → validates the request → calls containerd via gRPC.
  • containerd pulls the image (if needed), creates a snapshot (filesystem layer), and invokes runc to spawn the container process.

From a security perspective, compromising containerd alone can also lead to container breakout, but the attack path is typically via the higher-level dockerd API because it is more exposed.

RESTful API endpoints and JSON schema

The Docker API follows a conventional REST pattern: resources are identified by URLs, actions are performed with standard HTTP verbs, and payloads are JSON-encoded. The API version is part of the URL (e.g., /v1.44/containers/create).

Common endpoints:

  • GET /containers/json - List containers.
  • POST /containers/create - Create a container (JSON body describes image, command, mounts, etc.).
  • POST /containers/{id}/start - Start a stopped container.
  • DELETE /containers/{id} - Remove a container (optional force flag).
  • GET /images/json - List images.
  • POST /images/create - Pull an image from a registry.

Example JSON payload for creating a container:

{ "Image": "alpine:latest", "Cmd": ["sh", "-c", "echo Hello from Docker API > /tmp/hello.txt"], "HostConfig": { "Binds": ["/host/data:/container/data"] }
}

This JSON is validated against an internal schema; missing required fields result in a 400 Bad Request response with a descriptive error message.

Security note: The API accepts arbitrary command strings. If an attacker can send a crafted payload, they can execute any command inside the container, and if the container runs with --privileged or mounts host paths, they can escape to the host.

Unix socket vs TCP listener (default ports 2375/2376)

By default, dockerd binds to a Unix domain socket at /var/run/docker.sock. This socket inherits the file system permissions of the host. Typical permissions are srw-rw---- 1 root docker, meaning any user in the docker group can control the daemon.

When the daemon is started with the -H tcp://0.0.0.0:2375 flag (or via daemon.json), it also listens on a TCP port. Two ports are commonly referenced:

  • 2375 - Plain-text, unauthenticated HTTP. **Never expose this to untrusted networks**.
  • 2376 - HTTPS with TLS client authentication. This is the recommended remote-API exposure method.

Example of enabling a secure TLS listener in /etc/docker/daemon.json:

{ "hosts": [ "unix:///var/run/docker.sock", "tcp://0.0.0.0:2376" ], "tls": true, "tlsverify": true, "tlscacert": "/etc/docker/certs/ca.pem", "tlscert": "/etc/docker/certs/server-cert.pem", "tlskey": "/etc/docker/certs/server-key.pem"
}

When TLS is enabled, the daemon validates client certificates against the CA. This mutual TLS (mTLS) model ensures that only trusted clients can issue API calls.

From a defensive viewpoint, the Unix socket is often the weakest link because many container-oriented tools (e.g., docker-compose, kubectl exec via docker exec) assume it is locally accessible. Mis-configuring the socket (e.g., making it world-writable) instantly grants root-level container control to any local user.

Authentication mechanisms (TLS client certs, basic auth)

Docker’s native API supports two primary authentication methods:

  1. Mutual TLS (mTLS) - When the daemon runs with --tlsverify, it requires a client certificate signed by the configured CA. The client presents its cert and key via --tlscert and --tlskey flags or environment variables. This is the most secure, as it provides both confidentiality and strong identity verification.
  2. Basic HTTP authentication - Docker does not natively support basic auth for the daemon API. However, many reverse-proxy solutions (e.g., Nginx, Traefik) sit in front of the API and enforce Authorization: Basic headers. The proxy then forwards the request to Docker over the Unix socket or a local TLS endpoint.

Example of configuring Nginx as a basic-auth front-end:

server { listen 2377 ssl; ssl_certificate /etc/nginx/certs/api.crt; ssl_certificate_key /etc/nginx/certs/api.key; auth_basic "Docker API"; auth_basic_user_file /etc/nginx/.htpasswd; location / { proxy_pass http://unix:/var/run/docker.sock:/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
}

In this setup, the API is still reachable only via the Unix socket, but external clients must first pass basic auth at the Nginx layer.

Security tip: Prefer mTLS over basic auth. Basic auth credentials often travel in clear text (unless forced over TLS) and are susceptible to replay attacks. When using a proxy, enforce Strict-Transport-Security and short-lived tokens.

Security defaults and common misconfigurations

Docker ships with a set of defaults that favor usability over security. Below are the most frequent missteps:

  • Exposing the API on 0.0.0.0:2375 - Attackers on the same network can invoke any daemon operation without authentication.
  • World-writable /var/run/docker.sock - Any local user can gain root privileges by creating a privileged container.
  • Running containers as --privileged or with --cap-add=ALL - Expands the attack surface, allowing container breakout.
  • Mounting host directories without read-only flags - Provides attackers a direct path to host files.
  • Disabling SELinux/AppArmor profiles - Removes mandatory access controls that would otherwise confine container processes.
  • Using default rootless mode but forgetting to limit user namespaces - May still allow privilege escalation within the namespace.

Recommended hardening checklist:

  1. Bind the daemon only to the Unix socket; disable TCP listeners unless TLS is enforced.
  2. Set socket permissions to 660 owned by root:docker and restrict docker group membership.
  3. Enable --tlsverify for any remote API exposure; store CA and client certs securely.
  4. Run containers with the least privileges: avoid --privileged, drop unnecessary capabilities, and use --read-only rootfs when possible.
  5. Activate SELinux or AppArmor profiles (Docker’s default docker-default AppArmor profile is a good start).
  6. Audit daemon configuration regularly (e.g., docker info --format '{{json .SecurityOptions}}').

These steps dramatically reduce the attack surface while preserving most operational functionality.

Practical Examples

Example 1 - Querying containers via the Unix socket

Using curl against the socket:

curl --unix-socket /var/run/docker.sock http://localhost/v1.44/containers/json

Expected output (truncated):

[ { "Id": "a1b2c3d4e5f6", "Image": "nginx:latest", "Command": "nginx -g 'daemon off;'", "Created": 1723456789, "State": "running", "Status": "Up 2 hours" }
]

Example 2 - Creating a privileged container via the API (dangerous)

Payload:

{ "Image": "alpine", "Cmd": ["sh", "-c", "apk add --no-cache tcpdump && tcpdump -i any -w /host/tmp/cap.pcap"], "HostConfig": { "Privileged": true, "Binds": ["/tmp:/host/tmp"] }
}

Command:

curl -X POST --unix-socket /var/run/docker.sock -H "Content-Type: application/json" -d @payload.json http://localhost/v1.44/containers/create

Running this container gives the attacker raw packet capture capabilities on the host network - a classic privilege-escalation vector if the API is exposed.

Tools & Commands

  • docker info - Shows daemon configuration, security options, and enabled runtimes.
  • docker system info --format '{{json .SecurityOptions}}' - Dumps security options in JSON for scripting.
  • ss -ltnp | grep dockerd - Verifies listening TCP ports.
  • lsof -U | grep docker.sock - Lists processes using the Unix socket.
  • openssl s_client -connect host:2376 -CAfile ca.pem -cert client-cert.pem -key client-key.pem - Tests TLS handshake.
  • curl --unix-socket /var/run/docker.sock - Quick health check; returns OK if daemon is reachable.

Defense & Mitigation

Securing the Docker daemon involves both configuration hardening and runtime monitoring:

  1. Network segmentation: Place Docker hosts in a dedicated VLAN or subnet. Block inbound traffic to ports 2375/2376 at the perimeter firewall unless explicitly needed.
  2. Use rootless Docker where possible. Rootless mode runs the daemon as an unprivileged user, drastically reducing impact of a compromised API.
  3. Enable audit logging (--log-level=debug or the auditd plugin) to capture each API request, method, and client IP.
  4. Implement host-based intrusion detection (e.g., Falco, Sysdig) with rules that flag privileged container creation, host-path mounts, or changes to /var/run/docker.sock permissions.
  5. Rotate TLS certificates regularly and enforce short-lived client certs.
  6. Apply least-privilege OS security modules: SELinux enforcing mode, AppArmor profiles, and seccomp filters that drop dangerous syscalls.

Common Mistakes

  • Assuming the Unix socket is safe because it is local - Any local user can abuse it; restrict group membership.
  • Leaving default 2375 open on cloud VMs - Attackers can enumerate and exploit the open API.
  • Using --insecure-registry without TLS - Allows MITM attacks that could inject malicious images.
  • Neglecting to update daemon configuration after a security advisory - Docker periodically releases patches that tighten defaults (e.g., disabling legacy --iptables=false).
  • Running containers as root inside the image - Combine with privileged mode, and you have a full host takeover.

Real-World Impact

In 2023, a high-profile ransomware campaign leveraged mis-configured Docker APIs exposed on port 2375 within Kubernetes nodes. The attackers used the API to spin up privileged containers, mount the host filesystem, and encrypt critical data before demanding ransom. Organizations that had limited docker group membership and enforced TLS on the API avoided infection.

Another case involved a CI/CD pipeline that stored the Docker daemon’s TLS client key in a public GitHub repository. Attackers cloned the repo, authenticated to the remote API, and exfiltrated proprietary container images. The breach underlined the importance of secret management around Docker’s TLS assets.

My expert opinion: As container adoption grows, the Docker daemon becomes a “soft” perimeter. Treat it like any other privileged service - lock it down, monitor it, and rotate its credentials. Expect future Docker releases to push towards default rootless operation and stricter API defaults, but until then, manual hardening is essential.

Practice Exercises

  1. Socket Permissions Audit - On a test host, run stat /var/run/docker.sock. Change the group to docker and set mode 660. Verify that a non-docker user cannot access the socket.
  2. Secure API Exposure - Configure daemon.json to enable TLS on port 2376. Generate a self-signed CA, server, and client certs. Verify connectivity with curl --cert client-cert.pem --key client-key.pem https://localhost:2376/v1.44/_ping.
  3. Detect Privileged Container Creation - Deploy Falco with the rule evt.type = execve and container.privileged = true. Trigger a privileged container via the API and observe the alert.
  4. Rootless Migration - Install Docker in rootless mode on a VM, run a simple container, and compare the process tree to a traditional daemon. Note the reduced UID/GID values.
  5. API Misuse Simulation - Using curl against a deliberately exposed 2375 endpoint, create a container that mounts /etc from the host. Observe the security implications, then remediate by shutting down the TCP listener.

Further Reading

  • Docker Engine API Reference - v1.44 spec
  • Docker Security Best Practices - official docs
  • Rootless Docker - guide
  • Container Runtime Interface (CRI) - Kubernetes integration details.
  • MITRE ATT&CK Technique T1609 - “Container Administration Command” - mapping of Docker API abuse.

Summary

The Docker daemon is a powerful, privileged service that powers container orchestration. Understanding its components (dockerd and containerd), the RESTful API, socket vs TCP listeners, and authentication mechanisms is essential for securing any container host. Default configurations favor convenience, often leaving the API exposed on plain-text ports or the Unix socket with lax permissions. By applying strict socket permissions, enforcing TLS with client certificates, disabling unnecessary listeners, and employing OS-level security modules, defenders can dramatically reduce the attack surface. Regular audits, monitoring with tools like Falco, and adopting rootless Docker where feasible further fortify the environment against remote API abuse and local privilege escalation.