Host privilege and the Docker socket

The DataMind Installer manages the DataMind OS stack by driving the host's Docker daemon. It does this by mounting the Docker socket into its own container and shelling out to docker compose. That mount is the single most important security fact about the architecture.

Important

Controlling the Docker socket means controlling the host. Anyone who can reach the Installer can, through it, run code as root on the server. Treat every Installer account as a root credential.

Why socket access is host root

The backend container mounts the socket directly:

yaml
# docker-compose.yml (backend service)
volumes:
  - ${DOCKER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock

The Docker API exposed on that socket has no per-action authorization: a caller who can talk to it can create containers, and a container can be created that mounts the host filesystem or joins the host's PID namespace. There is no way to expose "run docker compose up but nothing else". The compose-based actions the Installer performs — restart, stop, kill, pull, compose-up, deploy, apply-config — are the visible surface of a capability that is, at the socket, full root.

The practical consequence: the Installer's admin role and the host's root are the same privilege level. Granting one should be treated as granting the other.

The container runs as root by default

The Installer container does not drop privileges. The image installs the Docker CLI and the compose plugin and these comments are explicit in the Dockerfile:

dockerfile
# Runtime needs the Docker CLI + the compose v2 plugin (the deploy engine shells out to
# `docker compose`). ... Runs as root so it can use the mounted socket with zero config.
RUN apk add --no-cache docker-cli docker-cli-compose util-linux

util-linux provides nsenter, which the host-migration feature uses to run the retirement script in the host's namespaces. There is no non-root user configured for the image.

Besides the socket, the backend container mounts:

MountPurposeAccess
./deployment:/usr/src/app/deploymentThe downloaded docker-compose.yml, the env template and the generated .envRead/write
/var/tmp/delamain:/var/tmp/delamainCompose TMPDIR, shared at the same path on host and containerRead/write
${MIGRATE_HOST_DIR:-/opt/delamain}:/opt/delamainThe host-migration script locationRead/write
${JENKINS_HOME:-/var/lib/jenkins}:/var/lib/jenkins:roRead legacy Jenkins configuration for the import featureRead-only

The privileged helper for host migration

The host-migration action is a deliberate, one-off escalation on top of the socket. When it runs, the Installer spawns an ephemeral container from its own image with --privileged --pid=host and enters the host's namespaces:

bash
docker run --rm --privileged --pid=host <installer-image> \
  nsenter -t 1 -a -- bash /opt/delamain/migrate-host.sh --yes

That container runs the retirement script inside host PID 1's namespaces — stopping, disabling and masking the legacy systemd units. --pid=host plus --privileged plus a real socket is, again, root. The action is behind the admin role and recorded in the deployment audit trail as migrate-host, but it is not otherwise constrained.

DOCKER_SOCK and rootless Docker

The socket path is configurable, precisely so you can point it at a rootless daemon:

bash
# In the deployment .env
DOCKER_SOCK=/var/run/docker.sock              # default — rootful Docker (socket access ≈ host root)
DOCKER_SOCK=/run/user/1000/docker.sock        # rootless Docker — find the UID with: id -u

With rootful Docker (the default), the daemon runs as root and the socket is the root gateway described above. With rootless Docker, the daemon and its containers run as a single unprivileged user, so controlling that socket is bounded by that user's rights rather than by root's. Same dialect, same commands, materially smaller blast radius.

The Installer does not enforce which socket you use — it simply mounts whatever DOCKER_SOCK points at, so a wrong path fails at container start (compose cannot reach the daemon) rather than being silently ignored.

What to do about it

Because the privilege is inherent to the architecture, the controls are around it, not inside it: