Mueve el demonio de Docker fuera de root para que una fuga del contenedor caiga en un usuario común
- 1Lock down Docker networks
- 2Run containers as non-root
- 3Identity in front of every port
- 4Membership, not just an account
- 5An identity for the agent, not a key
- 6A tool server that checks who is asking
- 7Where the agent can go, not just what it can call
- 8Move the daemon off root
- 9Run the model's own code without trusting it
- 🏆A scope per tool, and a refusal clients can act on
Esta es la parte que nadie dice en voz alta cuando te recomiendan añadirte al grupo docker
para dejar de escribir sudo.
Ese grupo es root. No "casi root", ni "root para cosas de Docker". Si puedes ejecutar un contenedor, puedes leer, modificar o borrar cualquier archivo de la máquina — incluido el archivo de contraseñas, los directorios personales de otras personas y los archivos que el administrador apartó deliberadamente de ti.
No hace falta ningún exploit. Es un comando, y tarda unos cuatro segundos.
Una WEC Instance: 8 vCPU AMD EPYC 7601, 15 GB de RAM, Ubuntu 22.04, kernel 5.15. El demonio del
sistema es el docker.io de Ubuntu 29.1.3; el demonio rootless instalado aquí es 29.8.1
con rootlesskit 3.1.0. La imagen de prueba es alpine:3.20. El host mantuvo 20 contenedores
en marcha bajo el demonio del sistema durante todo el proceso, y ninguno se detuvo.
Todas las salidas y tiempos de abajo provienen de esa ejecución.
Demostrarlo en tu propia máquina
Ejecuta esto como usuario normal. No hay ningún sudo dentro.
docker run --rm -v /:/host alpine:3.20 \
sh -c 'touch /host/root/rootless-demo && echo WROTE as uid $(id -u)'
sudo ls -l /root/rootless-demo

WROTE as uid 0
-rw-r--r-- 1 root root 0 Sep 16 20:58 /root/rootless-demo
Un usuario que ni siquiera puede listar /root acaba de crear un archivo dentro, propiedad de
root.
Nada falló. Eso es el diseño. El demonio corre como root, le pediste montar el sistema de archivos completo, y lo hizo — porque un proceso root tiene permiso para ello. El "root" del contenedor y el root del host son la misma cosa.
Docker lo documenta con claridad, y conviene leer sus palabras antes que las mías. De Docker daemon attack surface:
only trusted users should be allowed to control your Docker daemon
y, en la misma página:
Docker allows you to share a directory between the Docker host and a guest container; and it allows you to do so without limiting the access rights of the container.
Eso no es una advertencia sobre un fallo. Es la descripción de la función que acabamos de usar.
Límpialo antes de seguir:
sudo rm -f /root/rootless-demo
Por qué esto es peor en un host que ejecuta agentes
Un agente de IA que ejecuta código que él mismo generó es, en términos de seguridad, ejecución remota de código que instalaste a propósito. No es una crítica a los agentes — es para lo que sirven.
Así que la pregunta deja de ser "¿podría un atacante ejecutar código aquí?" — algo ya lo hace, constantemente — y pasa a ser "¿hasta dónde llega ese código?".
Si llega al socket de Docker, la respuesta es "a todo", mediante el comando de arriba. Tus claves de API. Tus claves SSH. El volumen de la base de datos. Todo, por muy cuidadosamente que hayas configurado el contenedor del propio agente.
Qué arregló la Parte 2 y qué no
La Parte 2 encontró dos de seis contenedores
de Langfuse corriendo como root y los arregló con user: "999:999". Aquello valía la pena y
sigue valiéndola.
Pero cambia quién es el proceso dentro del contenedor. El demonio que lo arranca sigue siendo root, y el socket sigue siendo una puerta a toda la máquina. La Parte 2 endureció al pasajero. Este post va a por el conductor.
Qué hace realmente el modo rootless
El demonio deja de correr como root y corre como tú.
Los contenedores siguen creyendo que tienen root — el software de dentro sigue funcionando con
normalidad. Pero ese "root" ahora lo traduce el kernel a una cuenta ordinaria y sin privilegios
que no posee nada importante. Si algo se escapa del contenedor, no es el administrador. Es
ubuntu.
La traducción usa un bloque de IDs de usuario que el sistema reservó para ti. Aquí está la idea completa en una imagen — el mismo contenedor, bajo cada demonio:
La mitad izquierda es por qué funcionó el primer comando. La derecha es lo que vamos a montar, y su aritmética es algo que puedes verificar tú en un minuto.
Esta traducción es una función del kernel de Linux llamada
espacio de nombres de usuario,
no un invento de Docker — Docker solo se lo pide al kernel. El bloque de IDs al que mapea se
declara en /etc/subuid:
grep '^ubuntu:' /etc/subuid /etc/subgid
/etc/subuid:ubuntu:100000:65536
/etc/subgid:ubuntu:100000:65536
65.536 IDs a partir de 100000, solo tuyos. Retén ese número — el mapeo se convierte en aritmética comprobable dentro de un momento.
Instalarlo: cuatro muros
La documentación asume una instalación que esta máquina no tiene. Aquí está cada muro en el orden en que te lo encuentras.
Muro 1 — el paquete no existe
Todas las guías empiezan con la misma línea. Con el Docker de Ubuntu:
sudo apt-get install -y docker-ce-rootless-extras
dpkg -L docker.io | grep -iE 'rootless|rootlesskit'

E: Unable to locate package docker-ce-rootless-extras
Ese paquete pertenece al repositorio propio de Docker. Este host usa el docker.io de Ubuntu, y
el segundo comando no devuelve absolutamente nada: el paquete de la distribución no incluye
ninguna herramienta rootless.
Eso importa porque te obliga a elegir. Puedes sustituir docker.io por Docker CE, lo que
significa cambiar el demonio que ahora mismo ejecuta tus contenedores. O puedes instalar un
segundo demonio, propiedad de tu usuario, junto al primero. Con 20 servicios corriendo, la
segunda opción es la única sensata, y es la que sigue el resto de este post.
Lo que sí necesitas de apt son los ayudantes de mapeo de IDs:
sudo apt-get install -y uidmap
En Ubuntu 22.04 esto puede lanzar un diálogo "Daemons using outdated libraries" con
containerd.service y tu base de datos ya marcados. uidmap no necesita que se reinicie
ninguno. Desmárcalos, o cancela el diálogo — la instalación se completa igual.
Muro 2 — el instalador se niega a ejecutarse
curl -fsSL https://get.docker.com/rootless | sh
# Installing stable version 29.8.1
Aborting because rootful Docker is running and accessible.
Set FORCE_ROOTLESS_INSTALL=1 to ignore.
La protección asume que estás migrando — que un demonio root en marcha significa que estás a punto de liarla. Ejecutar ambos a la vez a propósito no se menciona en ningún sitio, y la forma de continuar aparece solo dentro del texto del error.
Fíjate en la versión que nombra: 29.8.1, mientras que el demonio del sistema es 29.1.3. Vas a acabar con dos versiones distintas de Docker en un mismo host. Aquí es lo esperado, y conviene saberlo.
curl -fsSL https://get.docker.com/rootless | FORCE_ROOTLESS_INSTALL=1 sh
Muro 3 — te mueve la CLI sin avisarte
Lee las últimas líneas de la salida del instalador:
[INFO] Creating CLI context "rootless"
[INFO] Using CLI context "rootless"
Current context is now "rootless"
Ahora pregúntale a Docker qué está corriendo:
docker ps
docker context ls
docker -c default ps -q | wc -l

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
NAME DESCRIPTION DOCKER ENDPOINT ERROR
default Current DOCKER_HOST based configuration unix:///var/run/docker.sock
rootless * Rootless mode unix:///run/user/1000/docker.sock
20
Una lista vacía, mientras veinte contenedores funcionan perfectamente. Nada se rompió y nada te avisó: la CLI está respondiendo a una pregunta distinta de la que crees haber hecho, porque el instalador la reapuntó al terminar.
Devuélvela a su sitio y decide por comando:
docker context use default # trabajo diario, sin cambios
docker -c rootless ps # explícito cuando quieras el nuevo
Arranca el demonio y confirma lo que tienes:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
systemctl --user start docker.service
docker -c rootless info --format '{{.ServerVersion}} {{.SecurityOptions}}'
29.8.1 [name=seccomp,profile=builtin name=rootless name=cgroupns]
name=rootless es el demonio confirmando lo que es.
El instalador te dice que añadas ~/bin a tu PATH. Para el uso diario no hace falta: el
cliente docker del sistema habla perfectamente con el demonio rootless a través del contexto,
y todos los comandos de este post se ejecutaron así. El PATH solo lo necesitan los scripts de
instalación y desinstalación — y sin él fallan con
dockerd-rootless.sh needs to be present under $PATH, que no te dice nada sobre el problema
real.
Comprobar que el mapeo es real
Pregúntale a un contenedor quién es, y luego mira ese mismo proceso desde fuera:
docker -c rootless run --rm alpine:3.20 id
docker -c rootless run -d --name u0 alpine:3.20 sleep 60
ps -o user=,uid=,cmd= -C sleep | grep 'sleep 60'
docker -c rootless run -d --name u1000 --user 1000:1000 alpine:3.20 sleep 120
ps -o uid=,cmd= -C sleep | grep 'sleep 120'

uid=0(root) gid=0(root) groups=0(root),...
ubuntu 1000 sleep 60
100999 sleep 120
Lee esas tres líneas juntas, porque son el mecanismo entero:
- El contenedor cree que es root.
- El host dice que ese proceso pertenece a
ubuntu. - Un contenedor que corre como ID 1000 aparece en el host como 100999.
Ese último número es 100000 + 1000 - 1 — el inicio de tu bloque reservado, desplazado por el
ID del propio contenedor, porque el root del contenedor ya ocupó la primera posición al mapearse
a ti.
Lo que te da una prueba en lugar de una promesa. Calcúlalo desde tu propia línea de
/etc/subuid y comprueba si el número coincide. Si en cambio ves un 1000 pelado, la
traducción no está ocurriendo y algo va mal en la instalación.
Limpieza:
docker -c rootless rm -f u0 u1000
El mismo comando, el otro demonio
Volvamos al comando con el que abrió este post. Misma imagen, mismo montaje del sistema de archivos completo, mismos flags. Solo cambia el demonio:
docker -c rootless run --rm -v /:/host alpine:3.20 \
sh -c 'touch /host/root/rootless-demo || echo DENIED'

touch: /host/root/rootless-demo: Permission denied
DENIED
El contenedor sigue creyendo que es root. El kernel no está de acuerdo, porque el proceso que
hay detrás pertenece a ubuntu, y ubuntu no puede escribir en /root.
Ese es el post, en dos salidas.
Lo que te cuesta
Cuatro cosas, todas medidas aquí en lugar de citadas.
Los contenedores no arrancan notablemente más lento. Tres ejecuciones seguidas:
for i in 1 2 3; do /usr/bin/time -f "run+exit: %es" \
docker -c rootless run --rm alpine:3.20 true; done
docker -c rootless run --rm -p 80:80 alpine:3.20 true

run+exit: 1.19s
run+exit: 0.96s
run+exit: 0.97s
Los puertos por debajo de 1024 se rechazan, y, cosa poco habitual, el error te da todas las salidas:
cannot expose privileged port 80, you can add
'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024),
or set CAP_NET_BIND_SERVICE on rootlesskit binary,
or choose a larger port number (>= 1024)
-p 18080:80 se enlaza sin queja. Detrás de un proxy inverso esto rara vez importa.
Los límites de rendimiento de disco dejan de funcionar. El demonio lo dice al arrancar, y es fácil pasarlo por alto:
WARNING: No io.max (rbps) support
WARNING: No io.max (wbps) support
Así que --device-read-bps y --device-write-bps no hacen nada, en silencio. Si contabas con
ellos para evitar que un contenedor ahogue a los demás, rootless te lo quita.
Descartar capacidades sigue funcionando, lo que importa porque a menudo se tratan como alternativas:
docker -c rootless run --rm --cap-drop=ALL alpine:3.20 sh -c 'echo cap-drop ok'
cap-drop ok
Se acumulan. Sigue usando user: y --cap-drop=ALL — cada capa asume que la anterior falló.
Las carpetas compartidas se comportan distinto
Esta es la parte que pilla a quien mueve un servicio real. Los permisos del host se aplican como tú, no como root:
D=/tmp/rl-vol; mkdir -p $D; echo hello > $D/mine.txt
sudo sh -c "echo secret > $D/theirs.txt; chmod 600 $D/theirs.txt"
docker -c rootless run --rm -v $D:/data alpine:3.20 sh -c \
'cat /data/mine.txt; cat /data/theirs.txt 2>&1 || echo DENIED'
docker -c rootless run --rm -v $D:/data alpine:3.20 stat -c '%u:%g %n' /data/mine.txt

hello
cat: can't open '/data/theirs.txt': Permission denied
DENIED
0:0 /data/mine.txt
Tu propio archivo se lee sin problema. El de root se rechaza — aunque el contenedor se crea
root, porque el kernel sabe más. Y tu archivo aparece dentro del contenedor como propiedad de
0:0, la misma traducción funcionando al revés.
Conviene interiorizarlo antes de mover una base de datos y pasarte una hora preguntándote por qué no puede abrir su propio directorio de datos.
Una cosa más: el demonio rootless mantiene un almacén de imágenes separado en
~/.local/share/docker. Las imágenes que ya tiene tu demonio del sistema son invisibles para
él, y todo se descarga otra vez. Con el disco justo, tenlo en cuenta.
Muro 4 — quitarlo también se niega
Por simetría, la desinstalación tiene la misma protección:
PATH=/home/ubuntu/bin:$PATH ~/bin/dockerd-rootless-setuptool.sh uninstall

[ERROR] Aborting because rootful Docker (/var/run/docker.sock) is running and accessible.
Set --force to ignore.
Añade --force cuando de verdad quieras quitarlo.
Qué te da esto y qué no
Cambia el peor escenario de "un atacante es dueño de la máquina" a "un atacante es dueño de
esta cuenta de usuario". En un host donde todos los servicios corren como ubuntu, eso es un
avance grande y no uno completo — tus claves SSH, tus archivos .env y tu código siguen dentro
de esa cuenta.
Así que el resumen honesto: rootless sube el suelo. No vuelve segura la habitación. Estrechar lo que la propia cuenta puede alcanzar es el siguiente peldaño, y ahí es donde entran el filtrado de llamadas al sistema y la cadena de suministro de imágenes.
Pero lo concreto que era cierto al principio de este post — que cualquiera capaz de ejecutar un contenedor podía quedarse con la máquina entera — ya no lo es, y puedes demostrarlo con un comando.
