¿Te has dado cuenta de algo? Desde el primer capítulo de este tutorial, cada vez que ejecutabas podman run, lo hacías como tu usuario normal, sin sudo. Y funcionaba. Los contenedores se ejecutaban, las redes se creaban, los volúmenes se montaban. Todo sin ser root. Eso no es magia. Eso es rootless Podman, y es una de las razones principales por las que Podman ha conquistado el corazón de los administradores de sistemas y desarrolladores de todo el mundo.
Docker necesita un daemon corriendo como root. Un fallo de seguridad en ese daemon y tu sistema entero está comprometido. Podman, en cambio, ejecuta cada contenedor como un proceso más de tu usuario, aislado mediante user namespaces, con un mapeo inteligente de UIDs y sin un punto único de fallo.
En este capítulo vas a entender exactamente cómo funciona esa magia. Vas a ver que es eso de user namespaces, el mapeo de UIDs con /etc/subuid y /etc/subgid, cómo funciona la red en modo rootless con slirp4netns y pasta, cuáles son las limitaciones que todavía existen, cómo endurecer tus contenedores con seccomp y capabilities, y cómo desplegar Quadlets rootless con linger y dbus activación.
Pero sobre todo, vas a entender por qué deberías evitar ejecutar contenedores como root siempre que sea posible, y cómo hacerlo correctamente.
¿Qué significa «rootless» exactamente?
Cuando ejecutas un contenedor en modo rootless, el proceso del contenedor —y todos los procesos dentro de él— se ejecutan bajo la identidad de tu usuario en el host. No hay un proceso root fuera del contenedor gestionando nada.
Pero dentro del contenedor, los procesos se creen root. Pueden instalar paquetes, escuchar en puertos, crear usuarios. Esa es la magia del user namespace: lo que el proceso ve dentro no es lo que realmente es fuera.
Veamos un ejemplo práctico. Abre dos terminales. En una, ejecuta un contenedor rootless:
podman run -d --name rootless-test alpine sleep 3600
Y en la otra, mira quién es el dueño del proceso:
ps aux | grep sleep
Verás que el proceso sleep pertenece a tu usuario, no a root. Sin embargo, dentro del contenedor:
podman exec rootless-test whoami
# root
podman exec rootless-test id
# uid=0(root) gid=0(root)
El proceso dentro del contenedor se cree root, pero fuera es solo tu usuario. Si alguien escapara del contenedor, se encontraría con los permisos de un usuario normal, no con los del administrador del sistema.
Eso es rootless. Y es mucho más seguro que ejecutar contenedores con privilegios de root.
User namespaces: el alma del rootless
Un user namespace es una característica del kernel de Linux que permite aislar los identificadores de usuario y grupo. Un proceso puede tener UID 0 (root) dentro de su namespace, pero UID 1000 (un usuario normal) fuera de él.
Podman utiliza los user namespaces de dos formas:
1. Mapeo automático (el que usarás el 99% del tiempo): Podman le pide al kernel que cree un user namespace nuevo para cada contenedor, y mapea el UID 0 del contenedor al UID de tu usuario en el host. Los UIDs adicionales (del 1 al 65535 dentro del contenedor) se mapean a los UIDs del rango configurado en /etc/subuid.
2. Mapeo manual (para casos avanzados): Tú mismo especificas el mapeo con --uidmap y --gidmap. Útil cuando necesitas que los archivos creados en el contenedor pertenezcan a un usuario específico en el host.
¿Cómo se crea un user namespace?
Cuando ejecutas podman run sin --privileged y sin ser root, Podman hace esto:
- Crea un user namespace vacío.
- Mapea tu UID del host (ej: 1000) al UID 0 dentro del namespace.
- Mapea los siguientes 65536 UIDs de tu rango
subuida los UIDs 1-65535 dentro del contenedor. - Crea el proceso init del contenedor dentro de ese namespace.
- El proceso init «cree» que es root porque dentro del namespace su UID es 0.
Y todo esto sin que tú tengas que hacer nada. Podman lo gestiona automáticamente.
Ver el user namespace de un contenedor
Puedes ver qué user namespace está usando un contenedor:
# Ver el namespace del contenedor
podman inspect rootless-test --format '{{.HostConfig.UsernsMode}}'
# (vacío = modo por defecto = nuevo namespace por contenedor)
# Ver los UIDs mapeados
podman inspect rootless-test --format '{{.HostConfig.IDMappings}}'
/etc/subuid y /etc/subgid: el registro de UIDs secundarios
Aquí llegamos al corazón técnico del rootless. Para que un usuario pueda ejecutar contenedores, necesita tener asignado un rango de UIDs y GIDs secundarios. Estos rangos se definen en dos archivos:
/etc/subuid— Rangos de UIDs secundarios por usuario./etc/subgid— Rangos de GIDs secundarios por usuario.
Formato de los archivos
nombre_usuario:UID_inicial:cantidad
Por ejemplo:
lorenzo:100000:65536
Esto significa que el usuario lorenzo tiene derecho a usar los UIDs del 100000 al 165535 (100000 + 65536 – 1) dentro de sus contenedores.
¿Por qué 65536? ¿De dónde sale ese número?
65536 = 16 bits. Es el número de UIDs que caben en un mapeo completo de user namespace. Los UIDs dentro del contenedor van del 0 al 65535. El UID 0 se mapea al UID real del usuario, y los UIDs 1-65535 se mapean a los UIDs secundarios.
Con 65536 UIDs secundarios, puedes mapear hasta 65535 IDs únicos dentro del contenedor (el UID 0 no cuenta porque se mapea al usuario real).
Ver tu configuración actual
# Ver los rangos asignados a tu usuario
grep "^$USER:" /etc/subuid
grep "^$USER:" /etc/subgid
# Ver todos los rangos del sistema
cat /etc/subuid
cat /etc/subgid
¿Qué pasa si no hay rangos configurados?
Si tu usuario no aparece en /etc/subuid ni en /etc/subgid, no podrás ejecutar contenedores rootless. El error típico es:
ERRO[0000] cannot find UID/GID for user usuario: no subuid ranges found for user "usuario" in /etc/subuid
Normalmente, cuando instalas Podman, el instalador configura automáticamente estos archivos. Pero si estás en un sistema donde instalaste Podman manualmente, puede que tengas que hacerlo tú:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
¿Cuántos rangos necesito?
Cada contenedor rootless consume un rango completo de subUIDs. Si tienes 65536 UIDs asignados, puedes ejecutar un contenedor rootless a la vez con mapeo completo.
Para ejecutar varios contenedores rootless simultáneamente, necesitas más rangos. Por ejemplo, 4 rangos = 4 contenedores simultáneos:
lorenzo:100000:65536
lorenzo:165536:65536
lorenzo:231072:65536
lorenzo:296608:65536
O, más práctico, asigna un rango grande de una sola vez, y Podman lo divide automáticamente:
lorenzo:100000:262144
Con 262144 UIDs, Podman puede ejecutar hasta 4 contenedores rootless simultáneamente.
Mapeo avanzado con –uidmap y –gidmap
Para casos donde necesitas control granular del mapeo, puedes especificarlo manualmente:
podman run --uidmap 0:100000:65536 --gidmap 0:100000:65536 alpine id
Esto mapea el rango de UIDs 100000-165535 del host al rango 0-65535 dentro del contenedor.
Un caso de uso real: cuando montas un volumen del host y quieres que los archivos creados en el contenedor pertenezcan a un UID específico en el host:
# El UID 1000 del host se mapea al UID 33 (www-data) dentro del contenedor
podman run --uidmap 0:1000:1 --uidmap 1:100000:65535 \
--gidmap 0:1000:1 --gidmap 1:100000:65535 \
-v /home/usuario/web:/var/www:Z nginx
Esto es útil cuando compartes volúmenes entre el host y el contenedor y necesitas que los permisos coincidan.
Red rootless: slirp4netns y pasta
Uno de los desafíos técnicos más interesantes del modo rootless es la red. Cuando eres root, el contenedor puede tener una interfaz eth0 real conectada a un bridge. Cuando eres rootless, no puedes crear interfaces de red reales. Necesitas algo que traduzca el tráfico de red del contenedor al mundo real.
Ahí entran slirp4netns y pasta.
slirp4netns
slirp4netns es la solución tradicional y la que usó Podman durante años. Funciona así:
- Crea un namespace de red para el contenedor.
- Dentro de ese namespace, crea una interfaz
eth0virtual. - Un proceso en espacio de usuario (
slirp4netns) traduce el tráfico de esa interfaz al stack de red del host usando SLIRP (un emulador de red en espacio de usuario). - El contenedor puede hacer conexiones salientes (TCP, UDP, ICMP), pero no puede recibir conexiones entrantes a menos que uses reenvío de puertos.
Ventajas:
- Funciona en cualquier sistema Linux sin configuración adicional.
- Ligero y estable.
Desventajas:
- Rendimiento limitado (todo pasa por espacio de usuario).
- ICMP limitado (no todos los tipos funcionan).
- No soporta multicast ni broadcast.
- Los puertos reenviados tienen latencia adicional.
pasta (passt)
pasta (originalmente «passt», luego renombrado a «pasta» para el modo de contenedor) es el sustituto moderno. Está diseñado específicamente para contenedores rootless y ofrece:
- Mejor rendimiento (menos copias entre espacios de usuario y kernel).
- Mejor soporte para ICMP y UDP.
- Soporte para tráfico multicast.
- Menor latencia en reenvío de puertos.
A partir de Podman 5.0, pasta es el backend de red por defecto en muchas distribuciones.
¿Cuál deberías usar?
| Aspecto | slirp4netns | pasta (passt) |
|---|---|---|
| Rendimiento | Aceptable | Mejor |
| ICMP | Limitado | Completo |
| UDP | Básico | Completo |
| Multicast | No | Sí |
| Latencia de puertos | Mayor | Menor |
| Madurez | Probado durante años | Más reciente |
| Disponibilidad | En todas las distros | Requiere versión reciente |
Configurar el backend de red
Puedes elegir el backend de red en ~/.config/containers/containers.conf:
[network]
default_rootless_network_cmd = "slirp4netns"
# o
default_rootless_network_cmd = "pasta"
O por contenedor:
podman run --network slirp4netns nginx
podman run --network pasta nginx
Reenvío de puertos en rootless
Cuando publicas un puerto en modo rootless, Podman usa reenvío de puertos en espacio de usuario. No hay reglas de iptables ni nftables. El proceso rootlessport o pasta escucha en el puerto del host y reenvía el tráfico al contenedor.
# Esto funciona en rootless
podman run -d -p 8080:80 nginx
# Ver el proceso que está haciendo el reenvío
ps aux | grep rootlessport
El proceso rootlessport se ejecuta como tu usuario y escucha en el puerto 8080 del host.
Ejemplo práctico: comparar slirp4netns vs pasta
Si quieres ver la diferencia entre ambos backends, puedes hacer una prueba sencilla con iperf3. Primero arranca un contenedor con slirp4netns:
# Terminal 1: servidor iperf con slirp4netns
podman run --rm -it --network slirp4netns \
-p 5201:5201 docker.io/networkstatic/iperf3 -s
# Terminal 2: cliente iperf
iperf3 -c localhost -p 5201
Anota el resultado. Después, prueba con pasta:
# Terminal 1: servidor iperf con pasta
podman run --rm -it --network pasta \
-p 5201:5201 docker.io/networkstatic/iperf3 -s
# Terminal 2: cliente iperf
iperf3 -c localhost -p 5201
En mi caso, la diferencia ronda el 15-20 % más de rendimiento con pasta. No es un cambio abismal, pero en aplicaciones con mucho tráfico de red, se nota.
Si quieres usar pasta por defecto en tus Quadlets, edita ~/.config/containers/containers.conf:
[network]
default_rootless_network_cmd = "pasta"
Y si prefieres cambiarlo solo para un Quadlet concreto, puedes hacerlo al arrancar:
podman run --network pasta --name mi-servicio nginx
Limitaciones del modo rootless
El modo rootless es potentísimo, pero no es perfecto. Tiene ciertas limitaciones que debes conocer antes de desplegar en producción.
1. Puertos privilegiados (<1024)
Los puertos por debajo de 1024 son privilegiados. Solo root puede escuchar en ellos. En modo rootless, no puedes publicar un contenedor en el puerto 80 o 443 directamente.
Solución 1: Usar un proxy inverso con authbind
authbind permite a usuarios no root escuchar en puertos privilegiados:
sudo apt install authbind
sudo touch /etc/authbind/byport/80
sudo chown $USER /etc/authbind/byport/80
sudo chmod 755 /etc/authbind/byport/80
Luego configuras Podman para que use authbind (no es trivial y no está soportado oficialmente).
Solución 2: Usar redirección con iptables (requiere un script con sudo)
sudo iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-port 8080
Solución 3 (recomendada): Usar un puerto alto y redirigir con un proxy del sistema
La solución más limpia es usar un puerto alto (>1024) en el host y redirigir con un proxy del sistema como socat o un servicio systemd:
# Con socat (como servicio systemd)
sudo systemd-run --unit=redirect-80 \
socat TCP-LISTEN:80,fork,reuseaddr TCP:localhost:8080
O mejor aún, usa un Quadlet de sistema (root) que haga de proxy, o configura tu firewall frontal (hardware o cloud) para redirigir el puerto 80 al 8080.
2. CGroups v2
Los CGroups (Control Groups) permiten limitar recursos como CPU, memoria y E/S de disco. En modo rootless, el acceso a CGroups está muy restringido:
- Puedes ver los límites de tus contenedores.
- Puedes establecer límites dentro del user namespace.
- No puedes usar CGroups v1 (solo v2).
- Algunos flags como
--cgroup-parentno funcionan. - El soporte para
--memory-swappuede ser limitado.
CGroups v2 incluyó mejoras específicas para rootless (como el delegado de CGroups via systemd), pero sigue siendo menos flexible que con root.
# Esto funciona en rootless
podman run -d --memory=256m --cpus=0.5 nginx
# Esto puede fallar
podman run -d --cgroup-parent=/system.slice/ nginx
Para que los límites de memoria y CPU funcionen correctamente en rootless, necesitas:
# Habilitar el delegado de CGroups para tu usuario en systemd
sudo mkdir -p /etc/systemd/system/user@.service.d/
cat << EOF | sudo tee /etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=yes
EOF
sudo systemctl daemon-reload
3. fuse-overlayfs
Para crear las capas de las imágenes de contenedor, Podman necesita un sistema de archivos de superposición. Cuando eres root, usa overlayfs del kernel directamente. Cuando eres rootless, necesita fuse-overlayfs (a menos que el kernel lo soporte de forma nativa para usuarios no root, lo cual es raro).
fuse-overlayfs es una implementación en espacio de usuario de overlayfs usando FUSE. Funciona, pero es más lento que overlayfs nativo:
| Operación | overlayfs (root) | fuse-overlayfs (rootless) |
|---|---|---|
| Lectura de capas | 1x | ~1.5x |
| Escritura en capa superior | 1x | ~2-3x |
| Creación de contenedor | 1x | ~1.2x |
podman build | 1x | ~1.5-2x |
Para la mayoría de los casos de uso, la diferencia es imperceptible. Pero si estás haciendo builds masivos con muchas capas, notarás la diferencia.
Puedes ver qué driver de almacenamiento estás usando:
podman info | grep -A5 storage
Si ves driver: overlay y Backing Filesystem: xfs, probablemente estés usando overlayfs nativo (si tu kernel lo soporta para rootless). Si ves mount_program: /usr/bin/fuse-overlayfs, estás usando fuse-overlayfs.
4. Capacidades de Linux reducidas
En modo rootless, el contenedor comienza con un conjunto reducido de capacidades Linux. Por ejemplo, CAP_NET_ADMIN, CAP_SYS_ADMIN, CAP_NET_RAW no están disponibles sin más configuración.
# Ver las capacidades de un contenedor rootless
podman run alpine cat /proc/self/status | grep Cap
5. No SYS_ADMIN, no dispositivos
No puedes crear dispositivos dentro del contenedor, ni montar sistemas de archivos del kernel (como /proc o /sys de forma privilegiada). Tampoco puedes ejecutar contenedores con --privileged:
# Esto NO funciona en rootless
podman run --privileged alpine ip link add dummy0 type dummy
# Error: permission denied
6. ping no funciona por defecto
El comando ping usa ICMP, que requiere CAP_NET_RAW o CAP_NET_ADMIN. En rootless, estas capacidades no están disponibles por defecto.
podman run alpine ping -c 1 8.8.8.8
# ping: permission denied (creando socket)
Solución: Añadir la capacidad manualmente:
podman run --cap-add=NET_RAW alpine ping -c 1 8.8.8.8
O mejor, añadir net.ipv4.ping_group_range al kernel:
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
7. SCTP no soportado
El protocolo SCTP no funciona en modo rootless porque requiere crear sockets raw, lo cual no está permitido.
Linger: para que los contenedores vivan sin sesión
Cuando ejecutas contenedores rootless con systemd (por ejemplo, con Quadlets), hay un problema: los servicios de usuario de systemd solo se ejecutan mientras el usuario tiene una sesión iniciada. Si cierras sesión, systemd detiene todos tus servicios de usuario… y tus contenedores con ellos.
La solución es linger (antes llamado «user lingering»). Cuando activas linger para un usuario, systemd mantiene su servicio de usuario en ejecución incluso cuando no hay ninguna sesión activa.
Activar linger
sudo loginctl enable-linger $USER
Verificar linger
loginctl show-user $USER | grep Linger
# Linger=yes
# O más directo:
ls -la /var/lib/systemd/linger/
Si el comando anterior muestra un archivo con el nombre de tu usuario, linger está activo.
¿Qué hace exactamente linger?
Cuando activas linger, systemd:
- Crea el archivo
/var/lib/systemd/linger/${USER}. - Al arrancar el sistema, systemd inicia el servicio
user@${UID}.servicepara tu usuario. - Este servicio mantiene todos tus servicios de usuario en ejecución.
systemctl --userempieza a funcionar incluso sin sesión SSH o TTY.
Desactivar linger
sudo loginctl disable-linger $USER
Linger + Quadlets = contenedores que sobreviven a reinicios
Esta es la combinación ganadora para producción rootless:
# 1. Activar linger
sudo loginctl enable-linger $USER
# 2. Crear Quadlets
mkdir -p ~/.config/containers/systemd/
# 3. Habilitar los servicios
systemctl --user enable mi-servicio.container
# 4. Recargar y arrancar
systemctl --user daemon-reload
systemctl --user start mi-servicio.container
# 5. Verificar que sobrevive a reinicios
sudo reboot
# ... después del reinicio ...
systemctl --user status mi-servicio
D-Bus activación: el arranque perezoso
Para rematar, una mención breve a la activación por D-Bus. Cuando tienes linger activado, los servicios de usuario pueden arrancar bajo demanda: no hace falta que estén siempre encendidos, sino que arrancan cuando otro proceso necesita comunicarse con ellos. En el contexto de Podman, puedes habilitar el socket de Podman para que cualquier comando podman active el servicio automáticamente:
systemctl --user enable podman.socket
systemctl --user start podman.socket
Con esto, systemd arranca Podman solo cuando lo usas. Es un detalle menor, pero útil si quieres ahorrar recursos.
Hardening: seccomp y Capabilities
Ahora que entiendes cómo funciona rootless, veamos cómo endurecer aún más tus contenedores. Porque rootless ya es seguro, pero siempre puedes hacerlo más seguro.
Seccomp: filtrar llamadas al sistema
seccomp (secure computing mode) es una característica del kernel de Linux que permite restringir las llamadas al sistema (syscalls) que un proceso puede hacer. Podman aplica un perfil seccomp por defecto que bloquea más de 40 syscalls peligrosas.
Te voy a contar una anécdota. Cuando empecé a usar perfiles seccomp personalizados, me pasé varios días afinando un perfil para un contenedor web. Lo había dejado tan restrictivo que el servidor Nginx ni siquiera podía escribir en sus propios logs. El healthcheck fallaba, el contenedor se reiniciaba en bucle, y yo sin entender qué pasaba. Al final, después de horas revisando logs y comparando syscalls, descubrí que había bloqueado
writecon ciertos flags. Desde entonces, mi enfoque es menos radical: empiezo con el perfil por defecto y voy eliminando syscalls de una en una, probando cada cambio. Es más lento, pero mucho más seguro que lanzarse a lo loco.
Ver el perfil seccomp por defecto:
podman info --format '{{.Host.Security.Rootless}}'
cat /usr/share/containers/seccomp.json | head -50
El perfil por defecto bloquea syscalls como:
clonecon flags peligrosos.mountyumount(a menos que tengasCAP_SYS_ADMIN).ptracesobre procesos de otros namespaces.keyctl(manejo de claves del kernel).bpf(llamadas BPF).perf_event_open.
Aplicar un perfil seccomp personalizado:
Puedes crear tu propio perfil seccomp y aplicarlo al contenedor:
podman run --security-opt seccomp=mi-perfil.json alpine
O desactivar seccomp por completo (no recomendado):
podman run --security-opt seccomp=unconfined alpine
Perfil seccomp mínimo para un contenedor web:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["accept", "bind", "listen", "read", "write",
"open", "close", "stat", "fstat", "mmap",
"brk", "sched_yield", "exit", "exit_group",
"nanosleep", "clock_gettime", "getpid", "getdents64"],
"action": "SCMP_ACT_ALLOW"
}
]
}
Este perfil solo permite las syscalls mínimas para un servidor web. Cualquier otra syscall genera un error y mata el proceso. Es extremadamente restrictivo, pero muy seguro.
Capabilities: el mínimo privilegio posible
Las capabilities de Linux son unidades atómicas de privilegio. En lugar de darle a un proceso todo el poder de root o ninguno, le das capabilities específicas: CAP_NET_BIND_SERVICE para bindear puertos, CAP_DAC_OVERRIDE para saltar permisos de archivos, etc.
Podman aplica una lista blanca de capabilities por defecto. En modo rootless, la lista es aún más restrictiva.
Ver las capabilities por defecto de un contenedor rootless:
podman run alpine cat /proc/self/status | grep Cap
# CapInh: 0000000000000000
# CapPrm: 00000000a80425fb
# CapEff: 00000000a80425fb
# CapBnd: 00000000a80425fb
# CapAmb: 0000000000000000
Puedes decodificar ese valor hexadecimal con capsh:
capsh --decode=00000000a80425fb
Capabilities por defecto en rootless:
Podman rootless concede estas capabilities por defecto:
| Capability | Propósito |
|---|---|
| CAP_CHOWN | Cambiar dueño de archivos |
| CAP_DAC_OVERRIDE | Saltar permisos de lectura/escritura |
| CAP_FOWNER | Operaciones como dueño del archivo |
| CAP_FSETID | Mantener setuid/setgid en archivos |
| CAP_KILL | Enviar señales a procesos |
| CAP_NET_BIND_SERVICE | Bindear a puertos (<1024 no funciona igual) |
| CAP_NET_RAW | Crear sockets RAW |
| CAP_SETGID | Cambiar GID |
| CAP_SETUID | Cambiar UID |
| CAP_SYS_CHROOT | Usar chroot() |
| CAP_SYS_PTRACE | Trazar procesos |
Eliminar capabilities innecesarias (principio de mínimo privilegio):
# Un servidor web no necesita CAP_SYS_PTRACE ni CAP_KILL
podman run -d --cap-drop=SYS_PTRACE --cap-drop=KILL nginx
# Eliminar todas y añadir solo las necesarias
podman run -d --cap-drop=ALL --cap-add=NET_BIND_SERVICE nginx
Ver qué capabilities necesita un contenedor específico:
Puedes averiguarlo probando:
# Intenta arrancar con todas las capabilities eliminadas
podman run --rm --cap-drop=ALL nginx
# Si falla, añade una a una hasta que funcione
Combinar seccomp y capabilities para máximo hardening
podman run -d \
--name web-hardened \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--cap-add=NET_RAW \
--security-opt seccomp=web-profile.json \
--security-opt no-new-privileges:true \
--read-only \
--tmpfs /tmp:noexec,nosuid,size=64M \
nginx:alpine
Este contenedor:
- Se ejecuta rootless (sin necesidad de flags especiales).
- Solo tiene las capabilities mínimas para funcionar como servidor web.
- Usa un perfil seccomp restrictivo que solo permite syscalls de red y E/S básica.
- No puede escalar privilegios (
no-new-privileges). - Su sistema de archivos es de solo lectura.
- Solo puede escribir en
/tmp, que es un tmpfs con restricciones.
Rootless con Quadlets: servicios que no necesitan root
Los Quadlets, como viste en el capítulo anterior, funcionan de maravilla en modo rootless. De hecho, el caso de uso principal de Quadlets es rootless. Veamos cómo aplicar todo lo que has aprendido sobre rootless a Quadlets.
Quadlet rootless básico
# ~/.config/containers/systemd/nginx-hardened.container
[Unit]
Description=Servidor Nginx endurecido (rootless)
Documentation=https://atareao.es/tutorial/podman/
[Container]
Image=docker.io/library/nginx:alpine
ContainerName=nginx-rootless
PublishPort=8080:80
Volume=./html:/usr/share/nginx/html:Z
DropCapability=ALL
AddCapability=NET_BIND_SERVICE
AddCapability=NET_RAW
ReadOnly=true
TmpFs=/tmp:noexec,nosuid,size=64M
LogDriver=journald
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=default.target
Quadlet rootless con seccomp personalizado
Para usar un perfil seccomp personalizado en un Quadlet, necesitas referenciar el archivo:
# ~/.config/containers/systemd/web-hardened.container
[Unit]
Description=Web endurecido con seccomp personalizado
[Container]
Image=docker.io/library/nginx:alpine
ContainerName=web-hardened
PublishPort=8080:80
DropCapability=ALL
AddCapability=NET_BIND_SERVICE
SecurityLabel=seccomp=/home/usuario/perfiles/web-seccomp.json
ReadOnly=true
[Service]
Restart=always
[Install]
WantedBy=default.target
Quadlet rootless con límites de recursos
# ~/.config/containers/systemd/app-limitada.container
[Unit]
Description=Aplicación con límites de recursos
[Container]
Image=localhost/miapp:latest
ContainerName=app-limitada
PublishPort=3000:3000
Memory=256m
MemorySwap=512m
CPUs=0.5
Environment=NODE_ENV=production
[Service]
Restart=on-failure
RestartSec=10s
[Install]
WantedBy=default.target
Activar todo el stack rootless con Quadlets
# 1. Asegurarte de que tienes linger activado
sudo loginctl enable-linger $USER
loginctl show-user $USER | grep Linger
# 2. Crear el directorio de Quadlets
mkdir -p ~/.config/containers/systemd/
# 3. Copiar tus Quadlets
cp *.container ~/.config/containers/systemd/
# 4. Recargar y habilitar
systemctl --user daemon-reload
systemctl --user enable --now nginx-hardened.container
# 5. Verificar
systemctl --user status nginx-hardened
journalctl --user -u nginx-hardened -f
Script generador de contenedores rootless endurecidos
Para facilitar el despliegue de contenedores rootless con hardening, aquí tienes un script en Bash que genera configuraciones completas:
#!/bin/bash
# Script: generar-rootless.sh
# Descripción: Genera contenedores rootless endurecidos con Quadlets
# Autor: Generado para el tutorial de Podman
# Uso: ./generar-rootless.sh [nombre_servicio] [imagen] [puerto_host:puerto_contenedor]
set -euo pipefail
# Colores para output
VERDE='\033[0;32m'
AMARILLO='\033[1;33m'
ROJO='\033[0;31m'
NC='\033[0m' # No Color
# Función para mostrar ayuda
usage() {
echo "Uso: $0 <nombre> <imagen> <puerto_host:puerto_contenedor> [opciones]"
echo ""
echo "Opciones:"
echo " -e VAR=valor Variable de entorno (se puede repetir)"
echo " -v host:ct:op Volumen (se puede repetir)"
echo " -m LIMITE Límite de memoria (ej: 256m)"
echo " -c LIMITE Límite de CPU (ej: 0.5)"
echo " -r Sistema de archivos de solo lectura"
echo " -s ARCHIVO Perfil seccomp personalizado"
echo " -h Mostrar esta ayuda"
echo ""
echo "Ejemplo:"
echo " $0 mi-web docker.io/library/nginx:alpine 8080:80 -e NGINX_HOST=localhost -v ./html:/usr/share/nginx/html:Z -r"
exit 0
}
# Validar argumentos mínimos
if [ $# -lt 3 ]; then
usage
fi
NOMBRE="$1"
IMAGEN="$2"
PUERTO="$3"
shift 3
# Variables por defecto
VARS=()
VOLS=()
MEMORIA=""
CPU=""
READONLY="false"
SECCOMP=""
# Procesar opciones
while getopts "e:v:m:c:rs:h" opt; do
case $opt in
e) VARS+=("$OPTARG") ;;
v) VOLS+=("$OPTARG") ;;
m) MEMORIA="$OPTARG" ;;
c) CPU="$OPTARG" ;;
r) READONLY="true" ;;
s) SECCOMP="$OPTARG" ;;
h) usage ;;
*) usage ;;
esac
done
QUADLET_DIR="${HOME}/.config/containers/systemd"
mkdir -p "$QUADLET_DIR"
# Generar Quadlet
QUADLET_FILE="${QUADLET_DIR}/${NOMBRE}.container"
# Cabecera
cat > "$QUADLET_FILE" << EOF
[Unit]
Description=Contenedor rootless endurecido: ${NOMBRE}
Documentation=https://atareao.es/tutorial/podman/
[Container]
Image=${IMAGEN}
ContainerName=${NOMBRE}
PublishPort=${PUERTO}
EOF
# Añadir variables de entorno
for var in "${VARS[@]}"; do
echo "Environment=${var}" >> "$QUADLET_FILE"
done
# Añadir volúmenes
for vol in "${VOLS[@]}"; do
echo "Volume=${vol}" >> "$QUADLET_FILE"
done
# Añadir límites de recursos
if [ -n "$MEMORIA" ]; then
echo "Memory=${MEMORIA}" >> "$QUADLET_FILE"
fi
if [ -n "$CPU" ]; then
echo "CPUs=${CPU}" >> "$QUADLET_FILE"
fi
# Hardening por defecto (rootless)
echo "DropCapability=ALL" >> "$QUADLET_FILE"
echo "AddCapability=NET_BIND_SERVICE" >> "$QUADLET_FILE"
if [ "$READONLY" = "true" ]; then
echo "ReadOnly=true" >> "$QUADLET_FILE"
echo "TmpFs=/tmp:noexec,nosuid,size=64M" >> "$QUADLET_FILE"
fi
if [ -n "$SECCOMP" ]; then
if [ -f "$SECCOMP" ]; then
echo "SecurityLabel=seccomp=${SECCOMP}" >> "$QUADLET_FILE"
else
echo -e "${AMARILLO}⚠ Archivo seccomp no encontrado: ${SECCOMP}${NC}"
fi
fi
# Secciones de servicio
cat >> "$QUADLET_FILE" << EOF
LogDriver=journald
[Service]
Restart=always
TimeoutStartSec=120
[Install]
WantedBy=default.target
EOF
# Verificar que el Quadlet es válido
echo -e "${VERDE}✓ Quadlet generado: ${QUADLET_FILE}${NC}"
# Mostrar resumen
echo ""
echo "=== Resumen del contenedor rootless ==="
echo "Nombre: ${NOMBRE}"
echo "Imagen: ${IMAGEN}"
echo "Puerto: ${PUERTO}"
echo "Memoria: ${MEMORIA:-sin límite}"
echo "CPU: ${CPU:-sin límite}"
echo "Solo lectura: ${READONLY}"
echo "Seccomp: ${SECCOMP:-perfil por defecto}"
echo ""
echo "Para activarlo:"
echo " systemctl --user daemon-reload"
echo " systemctl --user enable --now ${NOMBRE}.container"
echo " journalctl --user -u ${NOMBRE} -f"
echo ""
echo "Para verificar linger (necesario para auto-arranque):"
echo " sudo loginctl enable-linger \$USER"
Errores comunes y cómo solucionarlos
Error 1: «no subuid ranges found for user»
ERRO[0000] cannot find UID/GID for user lorenzo: no subuid ranges found for user "lorenzo" in /etc/subuid
Causa: El usuario no tiene rangos de subUIDs configurados.
Solución:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
Si tu distribución usa shadow-utils versión antigua, puede que necesites editar los archivos manualmente:
echo "$USER:100000:65536" | sudo tee -a /etc/subuid
echo "$USER:100000:65536" | sudo tee -a /etc/subgid
Error 2: «cannot find any available loopback device»
Error: cannot find any available loopback device
Causa: Se han agotado los dispositivos loopback disponibles, o fuse-overlayfs no está instalado.
Solución:
# Instalar fuse-overlayfs
sudo apt install fuse-overlayfs # Debian/Ubuntu
sudo dnf install fuse-overlayfs # Fedora/RHEL
# O aumentar el número de dispositivos loopback
sudo modprobe loop max_loop=256
Error 3: «Error: rootless requires at least one cgroup controller»
Error: rootless requires at least one cgroup controller
Causa: El sistema no tiene CGroups v2 habilitados, o el usuario no tiene permisos para acceder a ellos.
Solución:
# Verificar que CGroups v2 están activos
grep cgroup /proc/filesystems
# Si no, arrancar con systemd.unified_cgroup_hierarchy=1
# O en la línea de kernel:
# systemd.unified_cgroup_hierarchy=1
# Habilitar el delegado de CGroups para el usuario
sudo mkdir -p /etc/systemd/system/user@.service.d/
cat << EOF | sudo tee /etc/systemd/system/user@.service.d/delegate.conf
[Service]
Delegate=yes
EOF
sudo systemctl daemon-reload
Error 4: «Error: cannot re-exec process»
Error: cannot re-exec process
Causa: Podman necesita re-ejecutarse dentro del user namespace, y algo falla en el proceso.
Solución: Esto suele ser un problema de permisos de newuidmap y newgidmap:
# Verificar que los binarios newuidmap y newgidmap existen y son setuid
ls -la /usr/bin/newuidmap /usr/bin/newgidmap
# Deberían tener permisos -rwsr-xr-x (setuid)
# Si no, reinstalar shadow
sudo apt install --reinstall shadow # Debian/Ubuntu
Error 5: «Error: port is already bound»
Error: rootlessport: failed to create the rootlessport process: port is already bound
Causa: El puerto del host ya está en uso por otro contenedor o proceso.
Solución:
# Encontrar qué proceso usa el puerto
sudo lsof -i :8080
# O usar un puerto diferente
podman run -d -p 8081:80 nginx
Error 6: El contenedor rootless no puede hacer ping
$ podman run alpine ping -c 1 8.8.8.8
ping: permission denied (creating socket)
Causa: El contenedor no tiene CAP_NET_RAW, necesaria para crear sockets ICMP.
Solución:
# Opción 1: Añadir la capacidad
podman run --cap-add=NET_RAW alpine ping -c 1 8.8.8.8
# Opción 2: Configurar ping_group_range a nivel de sistema
sudo sysctl -w net.ipv4.ping_group_range="0 2147483647"
# Para hacerlo permanente
echo "net.ipv4.ping_group_range = 0 2147483647" | sudo tee /etc/sysctl.d/99-ping.conf
Error 7: «Error: OCI runtime error: container_linux.go: …»
Este error puede tener muchas caras. Una común en rootless es:
Error: OCI runtime error: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: set parent death signal: permission denied
Causa: Problema con systemd o con el runtime crun en modo rootless.
Solución:
# Asegurarse de que crun está instalado (mejor que runc para rootless)
sudo apt install crun # Debian/Ubuntu
# Configurar Podman para usar crun
echo "runtime = \"crun\"" >> ~/.config/containers/containers.conf
# O verificar que el runtime por defecto es crun
podman info | grep -A2 runtime
Error 8: Los Quadlets rootless no arrancan después de un reinicio
Causa: No tienes linger activado para tu usuario.
Solución:
sudo loginctl enable-linger $USER
# Verificar
loginctl show-user $USER | grep Linger
# Debe mostrar: Linger=yes
# Verificar el archivo de linger
ls -la /var/lib/systemd/linger/
Error 9: «Error: mount /var/lib/containers/storage/overlay: permission denied»
Error: mount /var/lib/containers/storage/overlay: permission denied
Causa: El directorio de almacenamiento de Podman tiene permisos incorrectos, o fuse-overlayfs no está disponible.
Solución:
# Verificar permisos del almacenamiento
ls -la ~/.local/share/containers/storage/
# Si es necesario, resetear los permisos
podman system reset
# Verificar que fuse-overlayfs está instalado
which fuse-overlayfs
# O cambiar el driver de almacenamiento a vfs (más lento, pero no requiere overlay)
echo -e "[storage]\ndriver = \"vfs\"" >> ~/.config/containers/storage.conf
Error 10: El rendimiento de build es muy lento en rootless
Causa: fuse-overlayfs es más lento que overlayfs nativo, especialmente en builds con muchas capas.
Solución:
# 1. Usar --layers=false para no cachear capas intermedias
podman build --layers=false -t miapp .
# 2. Usar Buildah directamente (a veces mejor rendimiento)
buildah bud -t miapp .
# 3. Si el kernel lo soporta, usar overlayfs nativo
# Verificar en /proc/filesystems si overlay aparece sin restrictions
grep overlay /proc/filesystems
# Si ves "nodev overlay", puedes probar:
echo -e "[storage]\ndriver = \"overlay\"\n[storage.options.overlay]\nmount_program = \"\"" >> ~/.config/containers/storage.conf
Error 11: «Error: lchown … : operation not permitted»
Error: lchown /var/lib/containers/storage/overlay/...: operation not permitted
Causa: El user namespace no puede cambiar el dueño de archivos porque el UID del host no tiene permiso sobre ellos.
Solución:
# Asegurarse de que los archivos pertenecen al usuario correcto
sudo chown -R $USER:$USER ~/.local/share/containers/
# O resetear el almacenamiento de Podman
podman system reset
Error 12: No se puede usar --privileged en rootless
Error: cannot run container with --privileged in rootless mode
Causa: El modo privilegiado requiere capacidades que no están disponibles en rootless por diseño.
Solución: Rootless y --privileged son mutuamente excluyentes por razones de seguridad. Alternativas:
# 1. Añadir capacidades específicas en lugar de --privileged
podman run --cap-add=SYS_ADMIN --cap-add=NET_ADMIN alpine ...
# 2. Usar --device para dispositivos específicos
podman run --device=/dev/fuse alpine ...
# 3. Si realmente necesitas privilegios completos, ejecuta como root
sudo podman run --privileged alpine ...
Conclusión
En este capítulo has visto una de las características más revolucionarias de Podman, la ejecución de contenedores sin privilegios de root,
- Qué es rootless: Contenedores que se ejecutan como un proceso más del usuario, sin necesidad de ser root ni de un daemon privilegiado.
- User namespaces: El mecanismo del kernel que permite que un proceso tenga UID 0 dentro del contenedor y UID 1000 fuera de él.
- /etc/subuid y /etc/subgid: Los archivos que definen los rangos de UIDs secundarios que cada usuario puede usar en sus contenedores.
- Mapeo de UIDs: Cómo se traducen los IDs de usuario entre el contenedor y el host, tanto automática como manualmente.
- Red rootless: Cómo funcionan slirp4netns y pasta para proporcionar conectividad de red a contenedores sin privilegios.
- Limitaciones: Puertos privilegiados, CGroups v2, fuse-overlayfs, capacidades reducidas, y cómo trabajar con ellas.
- Linger: Cómo hacer que los servicios de usuario de systemd sobrevivan al cierre de sesión.
- D-Bus activación: Cómo los servicios pueden arrancar bajo demanda mediante peticiones D-Bus.
- Hardening con seccomp y Capabilities: Cómo restringir aún más los contenedores rootless con perfiles seccomp personalizados y el principio de mínimo privilegio.
- Rootless con Quadlets: Cómo desplegar servicios rootless endurecidos con Quadlets, la integración más potente de Podman con systemd.
El modo rootless no es solo una característica de Podman: es un cambio de paradigma en la seguridad de contenedores. Cuando ejecutas contenedores sin privilegios, eliminas una de las mayores superficies de ataque de la virtualización ligera: el daemon con permisos de root.
Cada vez que puedas, ejecuta tus contenedores en modo rootless. Es más seguro, es más limpio, y es la forma en que Podman fue diseñado para funcionar. Y cuando necesites ir más allá, ya sabes cómo endurecerlos con seccomp, capabilities y Quadlets.
En el próximo capítulo veremos cómo los healthchecks pueden convertir tus contenedores en sistemas autocurables, capaces de detectar problemas y recuperarse sin intervención humana. ¡No te lo pierdas!
Más información,