The pattern that's everywhere and shouldn't be
If you've set up CI that builds container images, you've probably seen this: a build job that mounts the host's Docker socket so it can run docker build.
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sockIt works, and that's why it's everywhere. It's also a serious security hole. Let's unpack why, and what the alternative — rootless BuildKit — actually does differently.
Why the Docker socket is dangerous
The Docker daemon runs as root on the host. The socket is the control API for that daemon. Anything that can talk to the socket can ask the daemon to do anything the daemon can do — which is to say, anything root can do on the host.
Concretely, a process with socket access can:
# Mount the host's root filesystem into a privileged container
docker run -v /:/host -it alpine chroot /host sh
# You are now root on the node.In a CI context this is catastrophic. Your build runs *untrusted-ish* code — a malicious dependency, a poisoned base image, a compromised build script — and that code can pivot from "building an image" to "root on the build node," then to other tenants' workloads on the same host. Docker socket access is effectively root on the host, and CNCF/Kubernetes security guidance treats it as such.
In multi-tenant platforms this is a non-starter. You cannot let tenant A's build escape to tenant B's data.
What rootless BuildKit changes
[BuildKit](https://github.com/moby/buildkit) is the modern build engine (it's what powers docker buildx). The important property here is that it can run rootless and daemonless — building images without a privileged daemon and without root on the host.
The key differences:
| Property | Docker socket build | Rootless BuildKit |
|---|---|---|
| Privilege | Talks to root daemon | Runs as unprivileged user |
| Host access | Effectively root on node | No host root, no socket |
| Daemon | Shared host daemon | No shared daemon needed |
| Isolation | Weak (shared daemon) | Strong (per-build, namespaced) |
| Multi-tenant safe | No | Yes |
Rootless BuildKit uses Linux user namespaces to map the build's "root" to an unprivileged UID on the host. Inside the build, processes think they're root (so Dockerfiles that apt-get install work fine), but on the host they're a nobody user with no special powers. It pairs with fuse-overlayfs (or native rootless overlay on modern kernels) to handle layered filesystems without needing privileged mounts.
How a build actually flows without a socket
In a well-designed pipeline, each build is an ephemeral, isolated unit:
git push
-> spin up an ephemeral build pod (Kubernetes Job)
-> rootless BuildKit builds the image (no host socket, no root)
-> push image to a registry (Artifact Registry)
-> tear down the pod
-> deploy the imageNothing persists. There's no long-lived privileged daemon for an attacker to compromise, and no socket to escape through. If a build does something malicious, the blast radius is its own short-lived, unprivileged pod.
This is exactly the architecture PandaStack uses: builds run with rootless BuildKit inside ephemeral Kubernetes Job pods, images go to Google Artifact Registry, and deployment happens via Helm — no host Docker socket anywhere in the path. It's the difference between "a build can theoretically own the cluster" and "a build can at worst waste its own CPU quota."
What you give up (the honest part)
Rootless isn't free. The trade-offs:
- Some edge-case features that need real privileges (certain
--security=insecureoperations, some nested-container builds) are restricted or need explicit opt-in. - Performance of
fuse-overlayfscan trail native overlayfs on older kernels, though native rootless overlayfs on recent kernels closes most of the gap. - Networking inside rootless builds uses slirp4netns or similar, which has overhead versus host networking.
For the vast majority of Dockerfiles — install packages, copy source, build, set an entrypoint — none of this matters. The features that break rootless are usually the features you shouldn't be using in a build anyway.
Mitigations if you're stuck on the socket
If you genuinely can't move off socket-mounting yet, reduce the risk:
- Use a separate, dedicated build cluster so an escape doesn't reach production workloads.
- Never mount the socket into pods that run untrusted code.
- Consider Kaniko or buildah as drop-in rootless-ish alternatives that also avoid the daemon.
- Apply strict NetworkPolicies and node isolation around build nodes.
But these are mitigations, not fixes. The architectural answer is to stop handing build jobs root-on-host in the first place.
The takeaway
The Docker socket is the path of least resistance and the path of greatest risk. Rootless BuildKit removes the privileged daemon and the host socket entirely, contains each build in an unprivileged, ephemeral, namespaced unit, and makes multi-tenant builds actually safe. For anything beyond a single-trusted-team CI, it's the right default — and increasingly the expected one.
References
- [BuildKit — rootless mode docs](https://github.com/moby/buildkit/blob/master/docs/rootless.md)
- [Docker — Daemon socket security risk](https://docs.docker.com/engine/security/#docker-daemon-attack-surface)
- [CNCF — Kubernetes Security guidance on hostPath/socket](https://kubernetes.io/docs/concepts/security/pod-security-standards/)
- [Kaniko — build images without Docker daemon](https://github.com/GoogleContainerTools/kaniko)
Want builds that never touch a Docker socket? PandaStack builds every image with rootless BuildKit in ephemeral pods — see it on the free tier: [dashboard.pandastack.io](https://dashboard.pandastack.io)