Nada arruina una guardia tranquila como un contenedor que arranca bien, registra bien y luego falla estrepitosamente en una sola línea: permission denied. Siempre es el socket que montaste “como siempre lo hacemos”, o el nodo de dispositivo que “definitivamente le di”. El equipo de la aplicación jura que funcionó en su portátil. Tú juras que no puede ser un problema de permisos porque lo ejecutaste como root. Ambos están equivocados de maneras interesantes.
Esta es la forma práctica y apta para producción de depurar fallos de permiso de Docker en sockets UNIX y dispositivos /dev, sin recurrir a --privileged como si fuera un extintor por aburrimiento.
El modelo mental: por qué al “root” aún se le niega
Cuando un contenedor dice “permission denied”, tu cerebro quiere que sea el clásico permiso UNIX: usuario, grupo, bits de modo. A veces lo es. Con frecuencia no. En el mundo de los contenedores, el acceso es una negociación entre varios porteros independientes:
- Permisos de archivo (UID/GID/modo) en el sistema de archivos del host (incluyendo montajes bind).
- Mapeo de namespaces (namespaces de usuario, Docker rootless, remapeo de root) que cambia lo que significa UID/GID dentro del contenedor frente al exterior.
- Capacidades de Linux (poderes granulares de “root” como
CAP_NET_ADMIN). - Seccomp (filtro de llamadas al sistema) que puede bloquear operaciones con un error engañoso.
- LSMs (SELinux/AppArmor) que niegan acciones independientemente de los bits de modo.
- Controlador de dispositivos de cgroups (listas de permitidos/denegados) que pueden bloquear
open()en nodos de dispositivo.
Estas capas fallan de manera diferente, y las correcciones no son intercambiables. “Ejecutarlo como root” solo aborda una capa (UID/GID) y a veces ni siquiera eso si hay namespaces de usuario implicados. “Usar --privileged” aplasta varias capas a la vez y hace que todo funcione hasta que hace que otra cosa prenda fuego.
Aquí está la regla que quiero que interiorices: privileged no es un permiso; es un cambio de entorno. Desactiva o relaja múltiples salvaguardas. Por eso “funciona”. Y también por eso normalmente es la respuesta equivocada.
Una cita que envejece bien en operaciones: La esperanza no es una estrategia.
— James Cameron
Broma #1: Si --privileged es tu plan de depuración, no estás depurando—estás negociando con el kernel usando una trompeta.
Hechos interesantes y algo de historia (útil, no trivia)
- Las capacidades no son nuevas. Las capacidades de Linux dividieron a “root” en poderes discretos a finales de los 90; los contenedores solo las hicieron visibles para la gente normal.
- Docker empezó como un pegamento sobre LXC. Docker temprano dependía de LXC; el ecosistema de contenedores luego se estandarizó en torno a las especificaciones de runtime OCI.
docker.sockes efectivamente root. El acceso a la API de Docker suele otorgar control sobre el host, porque puedes montar el sistema de archivos del host o iniciar contenedores privilegiados.- El filtrado de dispositivos de cgroups es anterior a Docker. El controlador de dispositivos existía mucho antes del bombo de los contenedores; Docker simplemente lo usa para minimizar aventuras con
/dev/mem. - El perfil seccomp por defecto de Docker es conservador. Docker incluye un perfil seccomp por defecto que bloquea muchas llamadas al sistema; muchos reportes de “permission denied” son en realidad denegaciones de syscalls.
- Rootless Docker cambió el modelo de amenazas. El modo rootless evita un daemon root, pero también cambia cómo se comportan dispositivos y operaciones privilegiadas. Muchas cosas simplemente no se pueden hacer.
- SELinux hace literal el “permission denied”. En sistemas con SELinux, DAC (bits de modo) pueden decir “permitir” y SELinux aún puede decir “no”. Ambos son “permisos”, pero de sistemas distintos.
- Montar un socket cruza límites de confianza. Un contenedor que se conecta a un socket del host hereda lo que ese socket puede hacer (systemd, containerd, Docker, demonios administrativos personalizados).
Guía rápida de diagnóstico
Estás contra el reloj. No toques al azar. Haz esto en orden; cada paso reduce rápidamente la clase de fallo.
1) Confirma qué ruta falla y dónde vive
- ¿Es un bind mount desde el host? ¿Un socket creado dentro del contenedor? ¿Un nodo de dispositivo pasado?
- ¿El error ocurre en
open(),connect()o en alguna llamada de biblioteca de más alto nivel?
2) Determina identidad: UID/GID dentro vs fuera
- Revisa el UID/GID del proceso en el contenedor.
- Revisa el propietario/modo del archivo en el host y cualquier requerimiento de grupo (común con sockets).
- Si está involucrado rootless o userns-remap, asume que el mapeo de UID es el problema hasta que se demuestre lo contrario.
3) Verifica las “capas de política”: LSMs y seccomp
- Si SELinux/AppArmor está habilitado, busca denegaciones AVC/AppArmor.
- Si el fallo ocurre en algo “privilegiado” (mount, setns, perf, bpf, raw sockets), sospecha de seccomp/capacidades.
4) Para dispositivos, revisa permisos de dispositivos en cgroups
- Si puedes ver
/dev/...pero no puedes abrirlo, a menudo es filtrado por cgroups device o falta de capacidad.
5) Solo entonces considera añadir capacidades o --device
- Prefiere
--cap-add,--devicey correcciones explícitas de grupos. - Reserva
--privilegedpara casos raros y trátalo como paso diagnóstico temporal, no como solución.
Tareas prácticas: comandos, salidas, decisiones (12+)
Estos son los movimientos que realmente uso. Cada tarea incluye qué significa la salida y la decisión que tomas a partir de ella. Ejecútalos en el host salvo indicación contraria.
Task 1: Reproducir el fallo con máximo contexto
cr0x@server:~$ docker logs --tail=50 myapp
...snip...
Error: connect unix /var/run/docker.sock: permission denied
...snip...
Qué significa: Tienes una ruta que falla concreta: /var/run/docker.sock. Ese es un socket UNIX. La operación es connect(), no solo open().
Decisión: Enfócate en la propiedad del socket, el grupo, las etiquetas SELinux/AppArmor y si el usuario del contenedor está en el grupo correcto. No empieces por capacidades.
Task 2: Identificar el usuario efectivo del contenedor
cr0x@server:~$ docker exec myapp id
uid=10001(app) gid=10001(app) groups=10001(app)
Qué significa: El proceso no es root. No tiene grupos suplementarios. Si el socket requiere un grupo (usualmente lo hace), esto fallará.
Decisión: O ejecutas ese proceso específico con un grupo que coincida con el socket, o rediseñas para que el contenedor no necesite acceso al Docker del host.
Task 3: Inspeccionar permisos del socket en el host
cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan 3 10:12 /var/run/docker.sock
Qué significa: Solo root y miembros del grupo docker pueden conectar. Modo 660. Esto es típico.
Decisión: Si insistes en montar este socket, el usuario del contenedor necesita el grupo docker (coincidencia de GID importa), o necesitas un proxy con una API más reducida.
Task 4: Confirmar el GID del grupo docker (el desajuste de GID es clásico)
cr0x@server:~$ getent group docker
docker:x:998:cr0x
Qué significa: El grupo docker del host tiene GID 998. Dentro del contenedor, los IDs de grupo pueden no alinearse a menos que los hagas coincidir.
Decisión: Pasa el grupo al contenedor (--group-add 998) o construye la imagen para que el contenedor tenga un grupo con GID 998 y el proceso lo use.
Task 5: Ejecutar un contenedor puntual con group-add explícito para validar
cr0x@server:~$ docker run --rm -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--group-add 998 \
alpine:3.20 sh -lc 'id && apk add --no-cache docker-cli >/dev/null && docker ps >/dev/null && echo OK'
uid=0(root) gid=0(root) groups=0(root),998
OK
Qué significa: Con el grupo 998, el contenedor puede hablar con la API de Docker.
Decisión: Si esta “solución” es aceptable (a menudo no lo es), implementa el mapeo de grupo correctamente. De lo contrario, trata cualquier petición de montar docker.sock como una excepción de seguridad que necesita revisión.
Task 6: Diagnosticar denegaciones SELinux (si SELinux está habilitado)
cr0x@server:~$ getenforce
Enforcing
Qué significa: La política SELinux está activa. Un bind mount simple puede ser bloqueado por reglas de etiquetado.
Decisión: Revisa los logs de auditoría por denegaciones AVC antes de tocar permisos o capacidades.
Task 7: Buscar AVC recientes que coincidan con tu contenedor
cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1735902901.123:812): avc: denied { connectto } for pid=21456 comm="myapp" path="/var/run/docker.sock" scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=system_u:object_r:docker_var_run_t:s0 tclass=unix_stream_socket permissive=0
...snip...
Qué significa: SELinux negó explícitamente connectto en el socket. Incluso permisos UNIX correctos no ayudarán.
Decisión: Arregla el etiquetado/opciones de montaje (:Z/:z) o la política. No cambies el modo del socket por frustración.
Task 8: Validar el perfil AppArmor (común en Ubuntu)
cr0x@server:~$ docker inspect --format '{{.AppArmorProfile}}' myapp
docker-default
Qué significa: Se aplica el perfil AppArmor por defecto. Puede negar ciertas operaciones (menos frecuente en sockets, más en montajes, perf, ptrace).
Decisión: Si ves denegaciones de AppArmor en dmesg, ajusta con un perfil personalizado o prueba con --security-opt apparmor=unconfined solo como diagnóstico.
Task 9: Comprobar el modo seccomp
cr0x@server:~$ docker inspect --format '{{.HostConfig.SecurityOpt}}' myapp
[]
Qué significa: No hay security opts personalizadas; está en juego el perfil seccomp por defecto de Docker.
Decisión: Si la falla es una denegación de syscall (a menudo aparece como Operation not permitted), prueba con --security-opt seccomp=unconfined para confirmar, y luego arregla añadiendo solo las syscalls/capacidades necesarias.
Task 10: Para acceso a dispositivos, comprueba si el nodo existe dentro del contenedor
cr0x@server:~$ docker exec myvpn ls -l /dev/net/tun
crw-rw-rw- 1 root root 10, 200 Jan 3 10:12 /dev/net/tun
Qué significa: El nodo de dispositivo está presente. Si la app aún no puede usarlo, el problema no es “archivo faltante”. Es ya sea filtrado por cgroups device o falta de capacidades (comúnmente CAP_NET_ADMIN).
Decisión: Verifica reglas de allow de dispositivos y capacidades requeridas, no cambies permisos con chmod.
Task 11: Confirmar que el contenedor se inició con el dispositivo permitido
cr0x@server:~$ docker inspect --format '{{json .HostConfig.Devices}}' myvpn
[{"PathOnHost":"/dev/net/tun","PathInContainer":"/dev/net/tun","CgroupPermissions":"rwm"}]
Qué significa: Docker configuró el passthrough del dispositivo y los permisos de cgroup para lectura/escritura/mknod.
Decisión: Si los permisos aún fallan, probablemente necesites una capacidad (CAP_NET_ADMIN) o estás bloqueado por LSM/seccomp.
Task 12: Inspeccionar las capacidades del contenedor (conjunto efectivo)
cr0x@server:~$ docker exec myvpn sh -lc 'apk add --no-cache libcap >/dev/null 2>&1; capsh --print | sed -n "1,8p"'
Current: cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap=ep
Bounding set =cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap
...snip...
Qué significa: No hay cap_net_admin. Muchos flujos de trabajo VPN/TUN lo necesitan para configurar interfaces o enrutamiento.
Decisión: Añade --cap-add NET_ADMIN (y posiblemente NET_RAW, según) en lugar de --privileged.
Task 13: Validar ejecutando con la capacidad mínima añadida
cr0x@server:~$ docker run --rm -it \
--device /dev/net/tun \
--cap-add NET_ADMIN \
alpine:3.20 sh -lc 'ip link show >/dev/null 2>&1 || apk add --no-cache iproute2 >/dev/null; ip tuntap add dev tun0 mode tun && echo OK'
OK
Qué significa: Con NET_ADMIN más el dispositivo, la operación tiene éxito.
Decisión: Integra esos requisitos en la configuración de ejecución (Compose/équivalente securityContext en Kubernetes). No escales más a menos que algo más esté bloqueado.
Task 14: Revisar logs del kernel/auditoría por pistas de “denegado” (LSM/seccomp)
cr0x@server:~$ dmesg | tail -n 8
[1735902910.441] audit: type=1400 audit(1735902910.441:813): apparmor="DENIED" operation="mount" info="failed flags match" error=-13 profile="docker-default" name="/sys/fs/cgroup/" pid=21999 comm="runc"
...snip...
Qué significa: AppArmor denegó un mount. El error es -13 (EACCES), que parece un problema de permisos a nivel de usuario.
Decisión: No vas a arreglar esto con chmod. Necesitas un cambio en AppArmor, una estrategia de montaje diferente o dejar de hacer ese montaje desde dentro del contenedor.
Task 15: Confirmar si estás en modo rootless (y dejar de esperar milagros)
cr0x@server:~$ docker info --format '{{.SecurityOptions}}'
[name=seccomp,profile=default name=rootless]
Qué significa: Rootless está habilitado. Muchas operaciones con dispositivos y bajo nivel del kernel no funcionarán igual o no funcionarán.
Decisión: Si necesitas acceso a dispositivos brutos o funciones de red del kernel, rootless puede ser la plataforma equivocada para esa carga. Elige tus batallas.
Capacidades vs privileged: lo que realmente necesitas
--privileged es el martillo: todas las capacidades, todos los dispositivos, y un montón de restricciones de seguridad relajadas. Es genial para demostrar algo y terrible para producción. Las capacidades son el bisturí: añades exactamente lo que el proceso necesita y mantienes el sandbox en su sitio.
Qué cambia realmente --privileged
El comportamiento exacto varía según la versión de Docker y el kernel, pero en la práctica el modo privileged suele hacer todo lo siguiente:
- Añade todas las capacidades al contenedor (o casi todas), ampliando los conjuntos efectivos y bounding.
- Desactiva el filtro de dispositivos de cgroup: el contenedor puede acceder a dispositivos del host de forma amplia.
- Relaja algunas restricciones de LSM según la configuración (no es un bypass universal, pero cambia las reglas del juego).
- Facilita que los procesos hagan mounts, manipulen la pila de red, carguen módulos del kernel (aún suele requerir cooperación del host) y toquen namespaces.
Por eso “arregla” el permission denied. También por eso puede convertir una app comprometida en una comprometedora del host con muy pocos pasos adicionales.
Necesidades comunes de capacidades según el síntoma
- Necesita crear TUN/TAP, establecer rutas, configurar iptables:
CAP_NET_ADMIN(y a vecesCAP_NET_RAW). - Necesita enlazar puertos <1024:
CAP_NET_BIND_SERVICE(Docker otorga esto por defecto en muchos casos). - Necesita ajustar la hora o el reloj:
CAP_SYS_TIME(evítalo si puedes). - Necesita montar sistemas de archivos:
CAP_SYS_ADMIN(esta es la capacidad “todo en uno”; evítala si es posible). - Necesita usar eventos perf: a menudo bloqueado por configuraciones del kernel y necesita
CAP_SYS_ADMINo cambios en sysctl; también suele bloquearse por seccomp. - Necesita cambiar propietario en bind mounts: puede necesitar
CAP_CHOWNpero normalmente deberías arreglar la propiedad en el host o usar el mapeo de UID correcto.
La guía aburrida que funciona
Si te tienta --privileged, primero pregúntate:
- ¿Puedo resolver esto con alineación de UID/GID o membresía de grupo?
- ¿Puedo resolverlo con una sola capacidad y quizá un mapeo de dispositivo?
- ¿Puedo resolverlo moviendo la acción privilegiada a un sidecar/agente con una API restringida?
- ¿El problema real es que intentamos hacer administración del host desde dentro de un contenedor de aplicación?
Sockets UNIX: docker.sock, runtimes y tus propios sockets
Los sockets de dominio UNIX son archivos, pero no son archivos regulares. Los “permisos” se aplican en tiempo de conexión, y el servicio detrás del socket decide qué hacer una vez que te conectas. Eso significa dos planos de control diferentes:
- Permisos del sistema de archivos controlan quién puede conectar.
- Autorización del servicio (si existe) controla lo que puedes hacer después de conectar.
docker.sock: la pistola con campaña de marketing excelente
Montar /var/run/docker.sock en un contenedor es común. También es un colapso del límite de seguridad. Si un atacante obtiene ejecución de código en ese contenedor, a menudo puede crear un nuevo contenedor con el sistema de archivos del host montado y llamarlo “debugging”.
Así que cuando arreglas permiso denegado en docker.sock, no solo solucionas un problema técnico. Estás otorgando control del host. Trátalo como claves SSH de producción: muy acotado, auditado y raramente necesario.
Grupo y GID: el generador de “funciona en mi laptop”
El socket docker suele ser propiedad del grupo docker. Dentro de un contenedor, el nombre “docker” es irrelevante; lo que importa es el GID numérico. Si el socket es root:docker con GID 998, el proceso del contenedor debe estar en el GID 998 para conectar.
Hay tres opciones sensatas:
- Usar
--group-addcon el GID del host. Bueno para arreglos rápidos, pero debes documentarlo. - Crear un grupo en la imagen con el mismo GID numérico y ejecutar el proceso con ese grupo. Mejor para reproducibilidad.
- No montar el socket. Usar un proxy estrecho o rediseñar. Mejor para seguridad.
Tus propios sockets de servicio (Postgres, Redis, demonios del sistema)
Mismo patrón: el archivo socket en el host tiene propietario/grupo/modo. El usuario del contenedor debe coincidir con esos permisos tal como los ve el host. Si montas /run/postgresql/.s.PGSQL.5432 en un contenedor, el proceso cliente dentro debe tener derechos para conectar según los bits de modo de ese socket.
Además: muchos servicios ponen sockets bajo /run, que es tmpfs y se recrea al arrancar. Si “arreglaste” los bits de modo una vez, no arreglaste nada. Dejaste una nota para el tú del futuro que se desilusionará.
Dispositivos: /dev, cgroups y por qué mknod no es tu amigo
Los nodos de dispositivo bajo /dev son archivos especiales que representan interfaces del kernel. El acceso se controla por:
- Permisos UNIX en el nodo de dispositivo (propietario/grupo/modo).
- La lista de permiso del controlador de dispositivos de cgroup (major/minor + r/w/m).
- Capacidades necesarias para realizar la operación (admin de red, I/O raw, etc.).
- LSMs y seccomp para ciertas operaciones sensibles.
Los fallos de permiso de dispositivo más comunes
/dev/net/tun: Pasaste el dispositivo pero olvidasteCAP_NET_ADMIN, o estás ejecutando rootless y esperabas milagros./dev/fuse: Pasaste el dispositivo pero olvidaste que el módulo kernel fuse no está disponible, o el runtime del contenedor lo deshabilita sin configuración extra.- Dispositivos GPU (
/dev/nvidia*,/dev/dri): La membresía de grupo (a menudovideoorender) y hooks del runtime importan tanto como los nodos de dispositivo. - Dispositivos de bloque: Si intentas montar o formatear discos desde dentro de un contenedor, detente y piensa en el radio de daño. Luego vuelve a pensarlo.
--device es preciso; úsalo
Para acceso a dispositivos, prefiere:
--device /dev/net/tun(o el dispositivo específico)--cap-addpara la capacidad que autoriza la operación del kernel asociada- Membresía de grupo explícita (p. ej., añadir a
renderpara dispositivos DRM) cuando sea aplicable
No lances --privileged por un único dispositivo faltante. Si tu app necesita un dispositivo, dale ese dispositivo. Si necesita veinte dispositivos, tu app quizá no sea una app; puede ser un agente, y los agentes merecen otra revisión.
Broma #2: Al kernel no le importa que tu contenedor “solo intenta ayudar”. Es como RR. HH.: primero la política, luego los sentimientos.
SELinux, AppArmor, seccomp: la capa de “parece permisos”
Los bits de modo son solo la corteza exterior. El relleno es la política.
SELinux: cuando chmod es arte performativo
En sistemas con SELinux, una denegación suele registrarse como un AVC. El proceso tiene un contexto de seguridad (como container_t) y el objetivo tiene un tipo (como docker_var_run_t). Si la política dice “no”, es no.
Operativamente, la corrección común para bind mounts es el etiquetado correcto:
:Zpara relabel del contenido para uso exclusivo del contenedor:zpara relabel para uso compartido entre contenedores
Si no relabelas y SELinux está en modo enforcing, puedes ver un socket y aún así estar bloqueado para conectarte a él. Eso no es Docker siendo raro. Es SELinux haciendo su trabajo.
AppArmor: más silencioso, pero todavía afilado
Las denegaciones de AppArmor aparecen en dmesg y pueden bloquear montajes, ptrace y otras operaciones sensibles. Puede manifestarse como permission denied u operation not permitted, dependiendo de la llamada y cómo falle.
La prueba “fácil” es --security-opt apparmor=unconfined. La corrección “correcta” es escribir un perfil AppArmor que permita exactamente lo que necesitas. Si tu contenedor necesita montar cosas arbitrarias, eso es un olor a problema, no un requisito de perfil.
Seccomp: la mano invisible que devuelve “EPERM”
Seccomp filtra llamadas al sistema. Cuando se bloquea, a menudo obtienes EPERM (Operation not permitted) y un informe de error que dice “permisos”. A veces es correcto; a veces es una syscall bloqueada que no tiene nada que ver con permisos de archivo.
Al depurar, ejecuta temporalmente con seccomp sin confinement para confirmar el diagnóstico, y luego ajusta el perfil o cambia el enfoque. Un ejemplo clásico son workloads que usan syscalls recientes que el perfil por defecto no permite.
Docker sin root (rootless): reglas distintas, mismo dolor
Rootless Docker es excelente para reducir el riesgo del host. También es un llamado a la realidad: si tu app necesita realizar acciones que normalmente requieren root en el host (dispositivos, configuración de red a bajo nivel, mounts), el modo rootless hará eso difícil o imposible.
Cómo rootless cambia la historia de permisos
- Namespaces de usuario no son opcionales. UID/GID dentro del contenedor se mapean a IDs no root en el host.
- El acceso a dispositivos está limitado. Incluso si puedes ver nodos de dispositivo, las operaciones pueden bloquearse porque no existen los privilegios subyacentes.
- La red puede ser diferente. Dependiendo de la configuración, podrías usar slirp4netns o similar, cambiando lo que significa “admin de red”.
Rootless no es “Docker pero más seguro”. Es “Docker con restricciones distintas”. Si ejecutas motores de almacenamiento, endpoints VPN, o cualquier cosa que quiera ser parte del kernel, rootless probablemente sea la herramienta equivocada.
Tres micro-historias corporativas desde las trincheras
Incidente: la suposición equivocada (root dentro del contenedor == root en el host)
Un equipo desplegó un recolector de logs containerizado que necesitaba leer logs rotados desde una ruta del host. En staging ejecutaron el contenedor como root y montaron /var/log. Funcionó. El cambio pasó la revisión porque “es solo lectura”.
En producción, el daemon Docker tenía habilitado el remapeo de namespaces de usuario. Dentro del contenedor, el proceso era UID 0. En el host, se mapeaba a un rango de UIDs no privilegiados. De pronto el recolector empezó a fallar con permission denied en algunos archivos rotados y no en otros. Los errores eran intermitentes porque la propiedad de los archivos rotados variaba por servicio y por el job de rotación.
Intentaron las soluciones habituales: chmod en el host, reiniciar el contenedor, ejecutar como root (ya lo hacían), añadir --privileged (que no ayudó consistentemente porque el mapeo userns seguía aplicándose). Mientras tanto, las alertas empezaron a quedarse ciegas porque el recolector descartaba selectivamente logs de los servicios que precisamente querías en un incidente.
La solución fue aburrida: dejar de asumir que UID 0 significa algo a través de una frontera de namespace. Alinearon la propiedad usando ACLs en los directorios de logs del host para el rango de UID remapeado, y cambiaron el recolector para que corriera como un UID específico que coincidiera con la política del host. También añadieron una prueba canaria que lee una ruta de log conocida en el arranque y falla rápido, en lugar de “hacer el intento” y perder logs en silencio.
Conclusión del postmortem: los contenedores no “rompieron permisos”. La suposición del equipo lo hizo. Los namespaces no son una vibra; son matemática.
Optimización que salió mal: “simplemente monta docker.sock para evitar desplegar agents”
Un equipo de plataforma quería jobs de CI más rápidos. La idea: ejecutar contenedores de build que hablen con el daemon Docker del host vía /var/run/docker.sock. Sin virtualización anidada, sin builders separados, menos overhead. Todos aplaudieron, porque era rápido, barato y “estándar de la industria”.
Luego un desarrollador añadió una dependencia que ejecutaba un script post-install de un paquete de terceros. El script no era malicioso a propósito; era descuidado y asumía que podía inspeccionar el entorno. Consultó la API de Docker, notó que tenía control y lanzó un contenedor auxiliar. Ese contenedor auxiliar montó rutas del host para “cachear” cosas. La caché incluía credenciales y archivos de configuración que nunca debieron estar visibles para los builds.
Seguridad lo detectó durante una revisión rutinaria porque los logs de build empezaron a contener detalles ambientales extraños. No se explotó nada más allá de eso, pero fue un susto. El problema subyacente era sencillo: montar docker.sock equivale a dar al contenedor una API administrativa del host.
La remediación fue un servicio de builders controlado con una API estrecha: “compila este repo en este commit con esta configuración”. Sin exposición general de la API de Docker. Los builds se hicieron un poco más lentos. La organización durmió mejor. Y el equipo de plataforma dejó de tratar docker.sock como una función de conveniencia.
Lección: si tu “optimización” acorta una frontera de confianza, no es una optimización. Es un instrumento de deuda con tasa de interés variable.
Práctica aburrida pero correcta que salvó el día: capacidades explícitas y verificaciones preflight
Un grupo de producto de networking ejecutaba contenedores que creaban interfaces TUN y aplicaban reglas de enrutamiento. Los prototipos iniciales eran todos con --privileged porque la meta era entregar una demo, no impresionar al kernel. Cuando el producto pasó a producción, un SRE insistió en documentar el conjunto mínimo: --device /dev/net/tun, --cap-add NET_ADMIN, más un par de sysctls gestionados fuera del contenedor.
Parecía pedante. También obligó al equipo a documentar exactamente qué operaciones realizaba el contenedor y dónde. Añadieron un preflight de arranque: verificar que /dev/net/tun está presente, verificar que CAP_NET_ADMIN existe (mediante una pequeña auto-verificación), y registrar un error claro si no.
Meses después, un cambio rutinario de hardening del host ajustó el perfil seccomp por defecto en un subconjunto de nodos. Algunos contenedores empezaron a fallar al inicio con errores parecidos a permisos. Porque el preflight era explícito, la triage del incidente fue rápida: los logs decían “NET_ADMIN faltante” en los nodos afectados. No faltaba; el perfil estaba siendo recortado por una política mal aplicada. Revirtieron la política y luego arreglaron el proceso de despliegue.
No pasó nada heroico. Nadie hizo SSH a cajas a las 3 a.m. El sistema dijo la verdad pronto, y la solución fue obvia. Ese es el aburrido que quieres en producción.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: “permission denied” al conectar a /var/run/docker.sock
Causa raíz: usuario del contenedor no en el grupo del socket (o desajuste de GID), o SELinux niega la conexión.
Solución: alinea el GID con --group-add o crea el grupo en la imagen; en SELinux usa etiquetado o política adecuada. Y replantea si debes montar docker.sock.
2) Síntoma: el nodo de dispositivo existe, pero open() falla con permiso denegado
Causa raíz: cgroup de dispositivos lo niega, o falta la capacidad para la operación asociada.
Solución: pasa el dispositivo con --device y permisos rwm; añade la capacidad mínima (a menudo NET_ADMIN para TUN). Valida con docker inspect y capsh.
3) Síntoma: chmod 666 aún no funciona (socket o archivo)
Causa raíz: denegación SELinux/AppArmor, o el objeto no es el que crees (p. ej., un socket nuevo recreado bajo /run).
Solución: revisa logs AVC/AppArmor, etiqueta montajes correctamente, y arregla el servicio que crea el socket (unidad systemd) en vez de perseguir archivos transitorios.
4) Síntoma: funciona con –privileged, falla sin él
Causa raíz: necesitas uno de: mapeo de dispositivo, una capacidad, una permisividad de seccomp, o permiso de montaje.
Solución: haz una bisect: añade --device primero (si aplica), luego añade una única capacidad, luego prueba seccomp sin confinement para confirmar. Sustituye privileged por requisitos explícitos.
5) Síntoma: contenedores rootless no pueden acceder a /dev/kmsg, /dev/net/tun o montar sistemas de archivos
Causa raíz: rootless carece de privilegios de root en el host por diseño.
Solución: no ejecutes esas cargas rootless. Usa un pool de nodos con root para cargas privilegiadas, o mueve la operación al host mediante un agente.
6) Síntoma: “Operation not permitted” al hacer mount o setns
Causa raíz: bloqueado por seccomp/AppArmor o falta de CAP_SYS_ADMIN.
Solución: evita hacerlo dentro del contenedor. Si es inevitable, usa un perfil seccomp/AppArmor personalizado y justifica explícitamente la capacidad. Trata CAP_SYS_ADMIN como “privileged-lite”.
7) Síntoma: el contenedor puede ver el socket pero la conexión falla solo en algunos hosts
Causa raíz: diferencias en políticas a nivel de host (SELinux enforcing en algunos, GIDs distintos, permisos de unidad systemd distintos).
Solución: estandariza la configuración del host; añade preflights que registren la propiedad/GID del socket al inicio del contenedor; trata el drift como causa de incidentes.
8) Síntoma: se puede acceder a rutas de archivo pero no cuando están bind-mounted
Causa raíz: opciones/etiquetas de montaje (SELinux), o el mapeo userns cambia la semántica de propiedad.
Solución: etiqueta con :Z/:z en SELinux; asegúrate de que el mapeo UID/GID coincida; considera usar volúmenes nombrados con propiedad gestionada si procede.
Listas de verificación / plan paso a paso
Checklist A: Arreglar un socket UNIX “permission denied” sin volverse privileged
- Identifica la ruta del socket desde logs o debug tipo strace en la app.
- En el host:
ls -ldel socket y captura propietario/grupo/modo. - En el contenedor: comprueba
idpara UID/GIDs. - Alinea el acceso por grupo por GID numérico:
- Prefiere
--group-add <gid>como prueba rápida. - Para producción, crea el grupo en la imagen con el mismo GID y ejecuta el proceso en él.
- Prefiere
- Si SELinux está activo: revisa logs AVC; etiqueta el montaje correctamente.
- Si AppArmor está activo: revisa
dmesgpor denegaciones; ajusta el perfil si hace falta. - Documenta el riesgo si el socket es administrativo (docker.sock, containerd, systemd).
Checklist B: Arreglar /dev “permission denied” de la manera correcta
- Confirma que el nodo de dispositivo existe dentro del contenedor (
ls -l). - Confirma que el dispositivo fue pasado (
docker inspect .HostConfig.Devices). - Determina la capacidad requerida para la operación (TUN y rutas:
NET_ADMIN, sockets raw:NET_RAW). - Añade una capacidad a la vez y vuelve a probar.
- Revisa logs LSM/seccomp si el fallo persiste.
- Rechaza CAP_SYS_ADMIN por defecto. Si alguien lo pide, que explique la syscall y la alternativa.
Checklist C: Uso seguro de docker.sock (si absolutamente debes)
- Modela la amenaza: asume que compromiso del contenedor = compromiso del host.
- Limita quién puede desplegarlo y dónde puede ejecutarse (nodos dedicados, políticas de red estrictas).
- Ejecuta como non-root y añade solo el grupo del socket por GID numérico.
- Prefiere un proxy que exponga solo endpoints necesarios, no la API completa de Docker.
- Añade monitorización para creación inesperada de contenedores y mounts del host iniciados vía ese socket.
Preguntas frecuentes
1) ¿Por qué un contenedor que corre como root aún recibe “permission denied”?
Porque “root” dentro de un contenedor puede no ser root en el host (namespaces de usuario), y porque LSMs, seccomp y reglas de cgroup de dispositivos pueden negar acceso independientemente del UID.
2) ¿Debería alguna vez montar /var/run/docker.sock en un contenedor?
Rara vez. Trátalo como otorgar control administrativo del host. Si lo haces, usa mapeo de GID explícito, controles de despliegue estrictos y documenta la excepción.
3) ¿Cuál es la diferencia entre --cap-add y --privileged?
--cap-add otorga una capacidad kernel específica. --privileged otorga un conjunto amplio de capacidades, relaja restricciones de dispositivos y debilita el aislamiento de múltiples maneras. Uno es un bisturí; el otro es una carretilla.
4) Mi app necesita CAP_SYS_ADMIN. ¿Es aceptable?
Asume “no” hasta que se demuestre lo contrario. CAP_SYS_ADMIN cubre una gran gama de operaciones (incluyendo mounts e interacciones de namespaces). A menudo hay alternativas: hacer el mount en el host, usar un plugin de volumen o rediseñar el flujo.
5) ¿Por qué funciona en mi laptop pero no en prod?
Diferentes políticas del host: SELinux en enforcing en prod, diferencias en perfiles AppArmor, GID distinto para el grupo docker, userns-remap habilitado, o kernel/seccomp distintos. Los contenedores son portables; las políticas del host no lo son.
6) ¿Cómo saber si SELinux es el problema?
Si getenforce está Enforcing y ves denegaciones AVC en los logs de auditoría que referencian el contexto de tu contenedor y el objeto objetivo, SELinux es la causa. Arregla el etiquetado o la política; no salgas con chmod.
7) ¿Cuál es la forma más segura de dar acceso a un contenedor a un socket UNIX del host?
Asegura que el socket atienda una API no administrativa; establece el modo del socket para requerir un grupo dedicado; añade solo ese GID numérico al contenedor; evita ejecutar todo el contenedor como root. Si es un socket administrativo, replantea fuertemente.
8) ¿En qué difieren los permisos de dispositivos de los permisos de archivos normales?
Los nodos de dispositivo además pasan por la lista de permitidos del controlador de dispositivos de cgroup y a menudo requieren capacidades para las operaciones privilegiadas del kernel asociadas. Ver /dev/net/tun no significa que puedas usarlo.
9) ¿Es rootless Docker una solución a estos problemas de permisos?
Reduce el riesgo, pero también elimina la capacidad de hacer muchas cosas privilegiadas. Si tu workload necesita dispositivos o configuración de red del kernel, rootless probablemente lo dificulte más que lo ayude.
Próximos pasos que sí puedes hacer
Si estás frente a un “permission denied” hoy, deja de adivinar y comienza a clasificar:
- ¿Es un socket o un dispositivo? Esa elección determina el camino más rápido.
- Verifica identidad (UID/GID y mapeo) antes de tocar capacidades.
- Revisa logs SELinux/AppArmor/seccomp cuando chmod no cambia nada.
- Reemplaza privileged por requisitos explícitos: un
--device, una--cap-add, un--group-add, más etiquetado si hace falta. - Escribe preflights dentro de los contenedores que dependen de integraciones con el host, para que los fallos sean ruidosos y precisos.
Y si alguien te pide “simplemente monta docker.sock”, pregunta qué problema están resolviendo realmente. La mayoría de las veces, la solución correcta no son permisos. Es arquitectura.