Initializing Enclave...

Fixing containerd.sock Permission Denied in Rootless Docker: A Complete Debugging Guide

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: Rootless Docker cannot access containerd.sock because the socket is owned by root or lives outside the user's XDG_RUNTIME_DIR, making every docker command fail with permission denied.
  • How to fix it: Ensure dockerd-rootless-setuptool.sh ran correctly for your user, XDG_RUNTIME_DIR is exported, and the systemd user service is active under the correct UID.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor your broken Docker daemon config or systemd unit file.

The Incident (What does the error mean?)

Raw error output from a broken rootless Docker environment:

$ docker ps
Cannot connect to the Docker daemon at unix:///run/user/1001/docker.sock. Is the docker daemon running?

$ docker info
ERROR: Got permission denied while trying to connect to the Docker daemon socket at
unix:///run/containerd/containerd.sock: Get "http://%2Frun%2Fcontainerd%2Fcontainerd.sock/v1.24/info":
dial unix /run/containerd/containerd.sock: connect: permission denied

Immediate consequence: Every Docker command is dead. No containers start. No images pull. If this is a CI runner, your entire pipeline is blocked. The daemon is either not running under your user context, or the DOCKER_HOST environment variable is pointing to the system-wide socket at /run/containerd/containerd.sock instead of the user-scoped socket at /run/user/$UID/docker.sock.


The Attack Vector / Blast Radius

This is not just an annoyance — it is a security boundary failure in both directions:

Scenario A — User escalating to root socket accidentally: If a developer works around this by running sudo chmod 777 /run/containerd/containerd.sock or sudo usermod -aG docker $USER, they have re-introduced root-equivalent Docker access, completely defeating the purpose of rootless mode. Any process running as that user can now mount host filesystems, escape namespaces, and own the node. This is a textbook container escape prerequisite.

Scenario B — CI/CD runner misconfiguration: In a shared CI environment (GitLab Runner, GitHub Actions self-hosted), if DOCKER_HOST is not scoped per-runner UID, one job's containerd socket bleeds into another user's namespace. A malicious pipeline job can attempt to connect to a neighbor's socket, enumerate running containers, and exfiltrate environment variables injected as Docker secrets.

Blast radius: Full host compromise if the workaround is chmod on the system socket. Data exfiltration and cross-tenant container inspection in shared CI environments.


How to Fix It (The Solution)

Step 1: Verify the actual socket path and daemon status

# Check if rootless daemon is running under YOUR user
systemctl --user status docker

# Confirm the correct socket path
echo $XDG_RUNTIME_DIR
ls -la /run/user/$(id -u)/docker.sock

If systemctl --user status docker shows inactive or failed, the rootless daemon never started. If XDG_RUNTIME_DIR is empty, the environment is broken.


Basic Fix: Re-run rootless setup and export environment

- # Missing or wrong DOCKER_HOST pointing to system socket
- export DOCKER_HOST=unix:///run/containerd/containerd.sock

+ # Correct: point to the user-scoped rootless socket
+ export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
+ export XDG_RUNTIME_DIR=/run/user/$(id -u)

Add both exports to your ~/.bashrc or ~/.profile. Then enable and start the user-scoped service:

dockerд-rootless-setuptool.sh install
systemctl --user enable docker
systemctl --user start docker
# Enable lingering so daemon survives logout
sudo loginctl enable-linger $(whoami)

Enterprise Best Practice: Lock the socket path in the systemd user unit

If you are deploying rootless Docker across multiple users on a shared node (CI runners, multi-tenant build servers), do not rely on shell exports. Encode the socket path into the systemd user unit override:

# File: ~/.config/systemd/user/docker.service.d/override.conf

[Service]
- # No explicit socket path — inherits broken system default
- Environment=""

+ Environment="XDG_RUNTIME_DIR=/run/user/%U"
+ Environment="DOCKER_HOST=unix:///run/user/%U/docker.sock"

After writing the override:

systemctl --user daemon-reload
systemctl --user restart docker

This ensures the socket path is deterministic and UID-scoped regardless of how the shell session was initiated (SSH, cron, CI agent fork).


Verify UID namespace mapping is intact

# Must return a valid subordinate UID range
grep $(whoami) /etc/subuid
grep $(whoami) /etc/subgid

# Expected output:
# youruser:100000:65536

If these entries are missing, rootless mode cannot create user namespaces and containerd will refuse to start:

- # /etc/subuid — missing entry for your user

+ youruser:100000:65536
# After editing /etc/subuid and /etc/subgid
dockerд-rootless-setuptool.sh install

💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. Enforce socket path in CI runner configuration (GitLab example):

# .gitlab-ci.yml
variables:
-  DOCKER_HOST: "unix:///var/run/docker.sock"  # system socket — WRONG for rootless
+  DOCKER_HOST: "unix:///run/user/1001/docker.sock"  # UID-scoped rootless socket

2. OPA/Conftest policy to block system socket references in CI configs:

package ciconfig

deny[msg] {
  input.variables.DOCKER_HOST == "unix:///var/run/docker.sock"
  msg := "DOCKER_HOST must not reference the system socket. Use rootless user-scoped socket."
}

3. Checkov custom check for Dockerfile and compose files:

Add a pre-commit hook that greps for /var/run/docker.sock volume mounts in docker-compose.yml files — binding the system socket into a container is an immediate critical finding in any rootless deployment.

# .pre-commit-config.yaml
- repo: local
  hooks:
    - id: block-system-docker-sock
      name: Block system docker.sock bind mounts
      entry: bash -c 'grep -r "/var/run/docker.sock" . && exit 1 || exit 0'
      language: system
      pass_filenames: false

4. Ansible/Terraform provisioning: When bootstrapping nodes, always include loginctl enable-linger for service accounts running rootless Docker. Without linger, the user daemon dies on logout and the socket disappears, causing the exact permission denied error on the next CI job.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →