Hiciste el ritual: chmod -R 777, quizá un chown por si acaso, reiniciaste el contenedor,
y aún así dice “permission denied”. La ruta del host es escribible. El UID coincide. El sistema de archivos no está en solo lectura.
Sin embargo, tu contenedor actúa como si estuviera cerrado fuera de su propia casa.
Aquí es donde los ingenieros empiezan a culpar a Docker, luego a Linux, luego a “la seguridad”, y finalmente entre ellos. La verdad es
aburrida: estás siendo bloqueado por un sistema de control de acceso obligatorio (SELinux o AppArmor), y está haciendo exactamente lo que
fue diseñado para hacer. El mensaje de error simplemente no se molesta en decirte.
Por qué “permission denied” sobrevive a chmod y chown
Los permisos Unix (propietario/grupo/modos) son control de acceso discrecional. Son locales y negociables. Si eres el propietario
del archivo, puedes conceder acceso. Si eres root, normalmente puedes forzar el acceso. Los contenedores añaden espacios de nombres y
capacidades, pero al final del día, cuando un proceso intenta abrir un archivo, el kernel decide.
SELinux y AppArmor son control de acceso obligatorio (MAC). No son “permisos de archivo”. Son una capa de política adicional que puede negar acceso incluso cuando los bits de modo lo permiten. Esa es la clave: el kernel puede devolver
EACCES por razones que no tienen nada que ver con ls -l.
Aquí está el patrón:
- Problema clásico de permisos: UID/GID equivocado, modo equivocado, ACL equivocada. Se corrige con chown/chmod/setfacl.
- Problema SELinux: las etiquetas (contextos) no permiten que el dominio del contenedor toque esa ruta. Se corrige con etiquetado, booleanos o política.
- Problema AppArmor: el perfil del contenedor prohíbe una ruta, un montaje, una capacidad o una syscall. Se corrige el perfil o se cambia.
Puedes detectar problemas MAC porque tu contenedor se comporta como un ladrón bien educado: tiene las llaves (UID/mode),
pero el sistema de alarma aún llama a la policía (SELinux/AppArmor).
Broma #1: Si chmod 777 lo solucionara todo, la seguridad sería un simple alias de Bash y todos estaríamos sin trabajo.
SELinux vs AppArmor: religiones diferentes, mismas excomuniones
SELinux en un párrafo orientado a producción
SELinux se basa en etiquetas. Todo tiene un contexto (user:role:type:level). La política decide qué “dominios” (tipos de proceso)
pueden acceder a qué “types” (etiquetas de objetos) con qué permisos. Los contenedores típicamente se ejecutan en un dominio confinado
como container_t, y los archivos del host deben estar etiquetados de forma que ese dominio tenga permiso para tocarlos. Si montas
rutas arbitrarias del host en un contenedor sin las etiquetas adecuadas, SELinux bloqueará el acceso incluso cuando el sistema de archivos
diga “claro”.
AppArmor en un párrafo orientado a producción
AppArmor se basa en rutas. Los perfiles definen qué rutas puede leer/escribir/ejecutar un proceso, además de qué capacidades e
interfaces del kernel puede usar. Docker suele aplicar un perfil por defecto (comúnmente docker-default) a menos que lo sobrescribas.
Si tu contenedor necesita montar algo, acceder a /sys de forma especial, o tocar una ruta del host no prevista por el perfil,
AppArmor puede negarlo.
Por qué te sientes confundido
Fallan de la misma manera a nivel de la aplicación. Tu aplicación ve “permission denied”. Tus logs muestran una traza de pila.
Tu equipo discute sobre la propiedad de archivos. Mientras tanto la verdadera denegación está en el rastro de auditoría del kernel, y tu contenedor
no recibe un error más claro porque la capa VFS no explica las cosas.
Tu trabajo es averiguar qué motor de política está en juego, luego leer los logs correctos, y después hacer el cambio mínimo seguro.
El resto de esta guía es ese camino, con menos metáforas espirituales.
Hechos interesantes y breve historia (para que lo raro tenga sentido)
- SELinux empezó como proyecto de investigación (originalmente de la NSA) y fue diseñado para aplicar política incluso contra root. Por eso “pero soy root” no lo impresiona.
- AppArmor nació como SubDomain y se convirtió en AppArmor tras adquisiciones y rebranding; ganó popularidad porque es más fácil razonar cuando piensas en rutas de archivos.
- Docker no inventó el confinamiento; heredó lo que ya tenía el kernel: namespaces, cgroups, hooks LSM, seccomp. Docker en gran medida los conecta y se lleva la culpa.
- El etiquetado de contenedores en SELinux evolucionó mucho a medida que los contenedores se hicieron mainstream; los patrones tempranos eran más toscos y las distros modernas tienen mejores valores por defecto para
container_ty compañía. - Las banderas de montaje
:zy:Zexisten porque los bind mounts rompen la suposición de “las etiquetas coinciden con la intención”; estas banderas reetiquetan el contenido para que los contenedores puedan accederlo. - Los logs de auditoría no son “logs de depuración”; son eventos de seguridad. Cuando los equipos los centralizan correctamente, los problemas SELinux pasan de misteriosos a rutinarios.
- El modelo de rutas de AppArmor puede ser engañado por juegos de rename/link si los perfiles no son cuidadosos; el modelo de etiquetas de SELinux evita parte de eso pero añade carga operativa de etiquetado.
- Los sistemas de archivos overlay cambiaron la historia del almacenamiento de contenedores; SELinux tuvo que aprender a etiquetar correctamente capas overlay, y un etiquetado incorrecto puede crear fallos que parecen bugs de Docker.
Guía de diagnóstico rápido (qué revisar primero/segundo/tercero)
Primero: identifica qué sistema MAC está activo (y para Docker, cuál importa)
- Si SELinux está en enforcing y Docker lo usa, empieza con denegaciones AVC.
- Si AppArmor está habilitado y el contenedor corre bajo un perfil, empieza con denegaciones de AppArmor.
- Si ninguno está activo, vuelve a los permisos clásicos y a user namespaces.
Segundo: confirma la operación que falla y la ruta/dispositivo exacto del host implicado
- ¿Es una ruta de bind mount? ¿Un volumen nombrado? ¿Un socket (como el socket de Docker)? ¿Un nodo de dispositivo?
- ¿La denegación es en lectura, escritura, ejecución, creación, relabel, montaje u otra cosa?
- ¿Falla solo en un host? Si es así, sospecha diferencias de política, no tu YAML.
Tercero: obtén la evidencia del kernel/auditoría, no conjeturas
- SELinux: busca
type=AVCen los logs de auditoría, luego interpreta contextos y permisos solicitados. - AppArmor: busca
apparmor="DENIED"y el nombre del perfil, luego mapea a rutas de archivo/capacidades.
Cuarto: elige la solución mínima que preserve el confinamiento
- SELinux: usa etiquetas adecuadas (
:z/:Z,chconosemanage fcontext), evita deshabilitar SELinux. - AppArmor: ajusta o crea un perfil, o cámbialo por uno no confinado solo cuando entiendas el radio de impacto.
Quinto: valida con una prueba repetible y deja migas de pan
- Reproduce con un contenedor mínimo y un único montaje.
- Documenta la expectativa de etiquetas/perfil en Compose/Kubernetes y en la provisión del host.
- Asegura que sobreviva a reinicios y relabels.
Tareas prácticas: comandos, salidas y decisiones (12+)
Estos son los comandos a los que realmente recurro cuando un contenedor no puede leer/escribir un montaje o es bloqueado haciendo
algo “obviamente permitido”. Cada tarea incluye: comando, salida de ejemplo, qué significa y la decisión que tomas.
Task 1: ¿Está SELinux habilitado y en enforcing?
cr0x@server:~$ getenforce
Enforcing
Significado: La política SELinux está activamente negando acciones no permitidas (no solo registrando).
Decisión: Trata “permission denied” como potencialmente SELinux hasta que se demuestre lo contrario. Ve a buscar AVCs.
Task 2: Comprobación rápida del estado SELinux
cr0x@server:~$ sestatus
SELinux status: enabled
SELinuxfs mount: /sys/fs/selinux
SELinux root directory: /etc/selinux
Loaded policy name: targeted
Current mode: enforcing
Mode from config file: enforcing
Policy MLS status: enabled
Policy deny_unknown status: allowed
Max kernel policy version: 33
Significado: Tienes SELinux ejecutándose en modo targeted (común en Fedora/RHEL y derivados).
Decisión: Confirma que existen paquetes de política para Docker/contenedores y que el etiquetado del host es correcto.
Task 3: ¿Está AppArmor habilitado?
cr0x@server:~$ aa-status
apparmor module is loaded.
24 profiles are loaded.
22 profiles are in enforce mode.
2 profiles are in complain mode.
0 processes are unconfined but have a profile defined.
Significado: AppArmor está aplicando perfiles activamente.
Decisión: Si el host es tipo Ubuntu/Debian, AppArmor es un sospechoso principal. Siguiente paso: comprobar el perfil del contenedor.
Task 4: Ver qué perfil AppArmor aplicó Docker a un contenedor
cr0x@server:~$ docker inspect --format '{{.Name}} -> AppArmor={{.AppArmorProfile}}' web-1
/web-1 -> AppArmor=docker-default
Significado: El contenedor está confinado bajo docker-default.
Decisión: Si ves denegaciones de AppArmor, tendrás que editar/reemplazar ese perfil o sobrescribir por contenedor.
Task 5: Confirma el montaje exacto y dónde Docker cree que está
cr0x@server:~$ docker inspect --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}}{{"\n"}}{{end}}' web-1
bind /srv/web/data -> /var/lib/app/data
volume web-cache -> /var/cache/app
Significado: Tienes tanto un bind mount como un volumen nombrado. El comportamiento de SELinux/AppArmor difiere entre ellos.
Decisión: Concéntrate primero en el bind mount; suele ser donde el etiquetado/política pegan.
Task 6: Reproduce con una prueba mínima de escritura dentro del contenedor
cr0x@server:~$ docker exec -it web-1 sh -lc 'id; touch /var/lib/app/data/.probe && echo ok'
uid=1000(app) gid=1000(app) groups=1000(app)
touch: cannot touch '/var/lib/app/data/.probe': Permission denied
Significado: El usuario de la aplicación no puede crear un archivo en el directorio montado.
Decisión: Si los permisos del host parecen correctos, procede inmediatamente a buscar evidencia MAC (AVC/AppArmor).
Task 7: Revisa los permisos clásicos del host de todos modos (porque somos humanos)
cr0x@server:~$ ls -ldn /srv/web/data
drwxrwx--- 5 1000 1000 4096 Jan 3 10:12 /srv/web/data
Significado: El modo y la propiedad coinciden con el usuario del contenedor (UID/GID 1000).
Decisión: Deja de discutir sobre chown. Esto casi con seguridad es SELinux/AppArmor (o userns/shiftfs, pero empieza por MAC).
Task 8: En hosts con SELinux, inspecciona el contexto del directorio del host
cr0x@server:~$ ls -ldZ /srv/web/data
drwxrwx---. 5 1000 1000 unconfined_u:object_r:default_t:s0 /srv/web/data
Significado: El directorio está etiquetado default_t, la etiqueta de “no sé qué es esto”. Normalmente los contenedores no pueden tocarlo.
Decisión: Reetiqueta el directorio para acceso por contenedores (preferiblemente de forma persistente). No desactives SELinux.
Task 9: Encuentra la denegación SELinux en los logs de auditoría (la evidencia clave)
cr0x@server:~$ sudo ausearch -m avc -ts recent | tail -n 5
type=AVC msg=audit(1704286512.911:412): avc: denied { create } for pid=23841 comm="touch" name=".probe" scontext=system_u:system_r:container_t:s0:c123,c456 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0
Significado: El dominio del proceso es container_t; el objetivo es default_t; el permiso denegado es create.
Decisión: Corrige el etiquetado de /srv/web/data (contexto objetivo), no los IDs de usuario del contenedor.
Task 10: Reetiquetado temporal rápido usando las banderas de montaje de Docker (:z vs :Z)
cr0x@server:~$ docker run --rm -v /srv/web/data:/data:Z alpine sh -lc 'touch /data/ok && ls -l /data/ok'
-rw-r--r-- 1 root root 0 Jan 3 10:21 /data/ok
Significado: El reetiquetado funcionó; el contenedor puede escribir ahora.
Decisión: Decide entre :Z (etiqueta privada para un solo contenedor) y :z (etiqueta compartida) en función de si múltiples contenedores necesitan la misma ruta.
Task 11: Hacer persistente el etiquetado SELinux con semanage fcontext
cr0x@server:~$ sudo semanage fcontext -a -t container_file_t "/srv/web/data(/.*)?"
cr0x@server:~$ sudo restorecon -Rv /srv/web/data
restorecon reset /srv/web/data context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:container_file_t:s0
restorecon reset /srv/web/data/ok context unconfined_u:object_r:default_t:s0->unconfined_u:object_r:container_file_t:s0
Significado: Has declarado el mapeo de etiqueta esperado y lo aplicaste. Sobrevivirá a operaciones de relabel.
Decisión: Prefiere esto en servidores gestionados. Usa chcon solo para pruebas rápidas.
Task 12: Confirma la nueva etiqueta SELinux
cr0x@server:~$ ls -ldZ /srv/web/data
drwxrwx---. 5 1000 1000 unconfined_u:object_r:container_file_t:s0 /srv/web/data
Significado: El directorio ahora está etiquetado para ser accesible por contenedores.
Decisión: Vuelve a probar la escritura desde el contenedor. Si aún falla, inspecciona otras denegaciones: quizá un subcamino, un socket o una clase distinta.
Task 13: En hosts AppArmor, encuentra la denegación en los logs del kernel
cr0x@server:~$ sudo journalctl -k -g 'apparmor="DENIED"' -n 5
Jan 03 10:24:11 server kernel: audit: type=1400 audit(1704287051.112:96): apparmor="DENIED" operation="open" profile="docker-default" name="/srv/web/data/.probe" pid=24910 comm="touch" requested_mask="wc" denied_mask="wc" fsuid=1000 ouid=1000
Significado: El perfil AppArmor docker-default negó escritura/creación (wc) a una ruta del host.
Decisión: O bien cambia el perfil para permitir esa ruta, o elige un enfoque distinto (volumen nombrado, otro montaje, o sobrescritura de perfil).
Task 14: Identifica la etiqueta del proceso init del contenedor (SELinux) o el perfil (AppArmor) desde dentro
cr0x@server:~$ docker exec -it web-1 sh -lc 'cat /proc/1/attr/current 2>/dev/null || true; cat /proc/1/attr/apparmor/current 2>/dev/null || true'
system_u:system_r:container_t:s0:c123,c456
docker-default (enforce)
Significado: Estás viendo metadatos de confinamiento desde el kernel. (En algunos hosts verás uno u otro.)
Decisión: Si SELinux muestra container_t, enfócate en etiquetas. Si AppArmor muestra un perfil, enfócate en reglas de perfil.
Task 15: Comprueba las opciones de seguridad del daemon de Docker (SELinux/AppArmor/seccomp)
cr0x@server:~$ docker info --format '{{json .SecurityOptions}}'
["name=seccomp,profile=builtin","name=selinux","name=apparmor"]
Significado: Docker conoce SELinux y AppArmor; ambos están en juego en este host.
Decisión: No asumas “somos un shop AppArmor” o “somos un shop SELinux”. Tu flota puede estar mixta.
Task 16: Comprueba si un volumen nombrado evita el problema del bind mount
cr0x@server:~$ docker run --rm -v web-cache:/cache alpine sh -lc 'touch /cache/ok && ls -l /cache/ok'
-rw-r--r-- 1 root root 0 Jan 3 10:28 /cache/ok
Significado: El volumen nombrado funciona porque Docker lo aprovisiona bajo una ubicación ya etiquetada/permitida para contenedores.
Decisión: Prefiere volúmenes nombrados a menos que realmente necesites la semántica de ruta del host (backups, auditorías, herramientas compartidas del host).
Task 17: Verifica opciones de montaje y tipo de sistema de archivos (NFS y amigos se ponen picantes)
cr0x@server:~$ findmnt -T /srv/web/data
TARGET SOURCE FSTYPE OPTIONS
/ /dev/mapper/rootvg ext4 rw,relatime,seclabel
Significado: El sistema de archivos soporta etiquetas SELinux (seclabel). Bien.
Decisión: Si no ves seclabel (o estás en NFS/CIFS), planifica un manejo especial y prueba con cuidado.
Task 18: Para rarezas de overlay2, comprueba la etiqueta del directorio merged del contenedor (hosts SELinux)
cr0x@server:~$ cid=$(docker inspect -f '{{.Id}}' web-1)
cr0x@server:~$ sudo ls -ldZ /var/lib/docker/overlay2/*/merged 2>/dev/null | head -n 2
drwxr-xr-x. 1 root root system_u:object_r:container_file_t:s0:c87,c912 4096 Jan 3 10:30 /var/lib/docker/overlay2/3a3d.../merged
drwxr-xr-x. 1 root root system_u:object_r:container_file_t:s0:c21,c333 4096 Jan 3 10:30 /var/lib/docker/overlay2/9b71.../merged
Significado: Los directorios merged de overlay tienen etiquetas de contenedor con categorías MCS. Eso es esperado en hosts SELinux.
Decisión: Si esas etiquetas están mal o faltan, estás en territorio de “desajuste entre daemon/driver de almacenamiento/política”, no de “chmod”.
Montajes bind, volúmenes y etiquetado: la mecánica real
Por qué los bind mounts son donde las buenas intenciones vienen a morir
Un volumen nombrado de Docker se crea bajo los directorios gestionados por Docker, con el contexto de seguridad por defecto correcto (SELinux)
o con rutas ya permitidas (AppArmor), según los valores de la distro. Un bind mount es una ruta cruda del host que tú elegiste.
Al kernel no le importa que lo hayas elegido “para el contenedor”. Es simplemente una ruta del host con la etiqueta/implicaciones de perfil que ya tiene.
Esta es la distinción operacional central: los volúmenes tienden a “funcionar por sí solos”, los bind mounts frecuentemente fallan de maneras confusas
porque cruzan dominios de seguridad.
SELinux: la etiqueta es el permiso, no el modo
Con SELinux, tu proceso de contenedor se ejecuta en un dominio como container_t más un conjunto de categorías MCS.
Los archivos bajo /var/lib/docker (o la raíz de almacenamiento de contenedores) están etiquetados para coincidir con ese dominio y a menudo
incluyen categorías coincidentes. Tu ruta aleatoria del host bajo /srv puede ser default_t o var_t o algún tipo de aplicación.
La política puede prohibir el acceso del contenedor por completo.
La solución no es “hazlo escribible para todo el mundo”. La solución es “aplica un tipo SELinux que el dominio del contenedor pueda usar”.
Enfoques comunes:
- Usa las etiquetas de montaje de Docker:
:zpara contenido compartido,:Zpara contenido privado. - Establece etiquetas persistentes:
semanage fcontext+restorecon, usandocontainer_file_t. - Usa herramientas container-selinux (varía por distro) para mantener la política alineada con el comportamiento de Docker.
Qué hacen realmente :z y :Z (y por qué sorprenden a la gente)
Las opciones :z/:Z le dicen a Docker que reetiquete la ruta origen para que el contenedor pueda accederla.
:Z típicamente da al contenido una etiqueta privada (incluyendo categorías) para un solo contenedor. :z
lo hace compartible entre contenedores. Si pones :Z en una ruta que usan múltiples contenedores, uno de ellos
puede de repente perder acceso. No es que Docker sea voluble; es que pediste etiquetas privadas en contenido compartido.
Otro borde afilado: reetiquetar un directorio del host puede tener efectos secundarios para procesos no contenedorizados. Las etiquetas SELinux son
una verdad del sistema, no un deseo por contenedor. Si ese directorio lo usa otro servicio con expectativas estrictas,
puedes romperlo.
AppArmor: rutas y capacidades son el campo de batalla
Las denegaciones de AppArmor suelen verse así:
- No se puede escribir en una ruta montada (el perfil no lo permite).
- No se puede
mountdentro del contenedor (capacidad denegada o regla de montaje denegada). - No se puede acceder a
/proco/syscon las características que la app espera. - No se pueden usar operaciones privilegiadas incluso cuando se corre como root en el contenedor (capacidades filtradas).
La solución generalmente es ajustar un perfil (permitir la ruta específica) o ejecutar con un perfil distinto. La solución perezosa
es “sin confinar” (unconfined), que a veces es aceptable para una ventana de troubleshooting controlada y casi nunca aceptable como
estado estable en producción.
Una nota sobre NFS, CIFS, FUSE y otras diversiones en red
Los sistemas de archivos en red complican esto:
- Algunos montajes no soportan etiquetas SELinux como los sistemas locales, o necesitan opciones de montaje explícitas.
- Root-squash en NFS puede convertir “root del contenedor” en “nobody”, y entonces perseguirás al culpable equivocado.
- AppArmor aún puede denegar rutas independientemente del sistema de archivos subyacente.
Cuando los contenedores no pueden escribir en NFS, revisa tanto MAC como las reglas de exportación NFS. Rara vez es solo una cosa.
Perfiles AppArmor: qué hace Docker, qué hace tu distro
docker-default es un compromiso, no una promesa
El perfil AppArmor por defecto de Docker intenta bloquear algunas cosas obviamente peligrosas mientras permite que la mayoría
de contenedores funcionen. No está hecho a la medida para tu app, tus montajes o tu régimen de cumplimiento. Es un cinturón de seguridad genérico.
Útil, pero no a medida.
Cuando te topas con una denegación de AppArmor, tienes opciones:
- Cambiar la carga de trabajo: prefiere volúmenes nombrados; evita montajes extraños; reduce comportamientos privilegiados.
- Cambiar el perfil: permite las rutas y operaciones exactas necesarias.
- Cambiar la configuración del contenedor: sobrescribe el perfil para ese contenedor.
Diagnosticar AppArmor: qué te dice la denegación
La línea de denegación normalmente incluye:
- profile= qué perfil lo bloqueó
- operation= open/mount/ptrace/etc.
- name= la ruta
- requested_mask / denied_mask qué tipo de acceso se intentó
Trátalo como un rompecabezas dirigido. Si niega escritura a una ruta específica del host, o no montes esa ruta, o permítela explícitamente.
Si niega mount/capacidades, considera si realmente las necesitas. La mayoría de las apps no las requieren.
Sobrescribir AppArmor: úsalo como un bisturí
Docker soporta sobrescribir AppArmor por contenedor mediante security opts. Esto puede salvarte la vida para una imagen vendor puntual que necesita
una capacidad específica, pero también es cómo la gente termina ejecutando producción efectivamente “sin MAC”.
La regla operacional: si sobrescribes perfiles, incluye esa decisión en el código de infraestructura y evalúa su modelo de amenazas. No lo dejes
como un parche tribal en una wiki que nadie lee.
Broma #2: AppArmor es como una política de viajes corporativa—tu viaje está “aprobado” hasta que intentas facturar el taxi.
Tres mini-historias corporativas desde las trincheras
Incidente 1: la suposición equivocada (“los permisos son permisos”)
Una empresa mediana migró varios servicios de VMs a contenedores. Fueron cuidadosos con el mapeo UID/GID e incluso estandarizaron en ejecutar apps como non-root. Todo parecía bien. Luego un servicio comenzó a fallar solo en los hosts nuevos basados en RHEL, mientras que funcionaba en la flota Ubuntu anterior.
El on-call hizo lo habitual: comprobó la propiedad, ejecutó chmod, redeployó. El servicio aún no podía escribir en su directorio de datos. Tras dos horas, alguien sugirió “quizá SELinux”, lo que fue recibido con el tipo de silencio que obtienes cuando dices “quizá sea DNS”.
Encontraron denegaciones AVC: el directorio bind-mounted estaba etiquetado default_t. El dominio del contenedor era
container_t. SELinux estaba haciendo su trabajo. La suposición equivocada fue creer que los permisos de archivo eran toda la historia, así que siguieron ajustando el control equivocado.
La solución fue simple: definir reglas fcontext persistentes para las rutas del host de la aplicación y aplicarlas con
restorecon. La mejora real fue procedimental: el equipo añadió un paso de validación del host en la provisión que afirma la corrección de etiquetas para los bind mounts conocidos.
La acción post-incidente fue directa: “Dejad de usar chmod como estrategia de depuración.” La imprimieron en una pegatina y la pegaron en un portátil. Fue solo moderadamente efectiva, pero la moral mejoró.
Incidente 2: una optimización que salió mal (bind mounts compartidos + :Z)
Otra organización ejecutaba un clúster de contenedores que compartían un directorio del host para activos generados. Piensa en miniaturas,
bundles compilados de frontend, ese tipo de cosas. Querían despliegues más rápidos y menos artefactos duplicados, así que bind-mountaron la misma ruta del host en múltiples contenedores en un nodo y lo llamaron “eficiente”.
Un ingeniero con mentalidad de seguridad notó denegaciones SELinux ocasionales y decidió “arreglarlo correctamente” añadiendo
:Z al montaje en el Compose. Funcionó en desarrollo: un contenedor, un directorio, sin problemas. Lo desplegaron.
Producción se volvió rara. La mitad de los contenedores podía escribir, la otra mitad no, y qué mitad cambiaba tras reinicios.
A veces un despliegue “se arreglaba solo” cuando un contenedor aterrizaba en otro nodo. El equipo pasó un día sospechando del backend de almacenamiento, luego de Docker, luego de la aplicación.
La causa raíz: :Z aplica una etiqueta privada pensada para las categorías MCS de un solo contenedor. Cuando múltiples contenedores comparten la ruta, la etiqueta privada coincide con las categorías de uno y no con las de los otros. No es determinista entre reinicios porque las categorías pueden diferir.
Cambiaron a :z para contenido verdaderamente compartido, y para algunos casos se movieron a volúmenes nombrados para evitar el acoplamiento a rutas del host. La “optimización” no solo salió mal; creó un modo de fallo que parecía I/O inestable y consumió mucho tiempo de ingenieros seniors. La lección fue simple: en el mundo SELinux, compartir es una decisión de política, no una conveniencia de montaje.
Historia 3: práctica aburrida pero correcta que salvó el día (logs de auditoría + runbooks)
Una empresa más grande tenía una flota mixta: algunos hosts con SELinux enforcing, otros con AppArmor, unos pocos hardenizados con ambos, y un flujo constante de contenedores de proveedores que asumían que podían hacer lo que quisieran.
Invirtieron en una práctica profundamente poco sexy: centralizar logs de auditoría y enseñar a los ingenieros a leerlos. No como un checkbox de cumplimiento—una capacidad operacional. Tenían un runbook: “permission denied en contenedor” mapea a “revisar montajes Docker, luego denegaciones SELinux/AppArmor, luego UID/GID.” Todo el mundo tenía los mismos pasos.
Una noche, un servicio relacionado con almacenamiento falló tras una ola de parches rutinaria. Los logs de la app no mostraban nada útil.
Los logs del contenedor mostraban “permission denied” en una ruta de datos. El on-call siguió el runbook, encontró denegaciones AVC,
y vio que la ruta objetivo había quedado etiquetada incorrectamente después de una migración de sistema de archivos.
Como tenían reglas fcontext persistentes metidas en la configuración del host, arreglarlo fue un restorecon y revertir un montaje mal aplicado. Servicio restaurado rápidamente. Sin heroísmos. Sin “poner SELinux en permissive temporalmente.” Simplemente un sistema que se comportó de forma predecible.
Lo que salvó la situación no fue la brillantez. Fue la repetibilidad. El equipo no “recordaba” SELinux; sus herramientas y runbooks lo recordaban por ellos.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: el contenedor no puede escribir en un bind mount, pero los permisos del host son correctos
Causa raíz: El contexto SELinux del directorio del host no es accesible por contenedores (a menudo default_t).
Solución: Usa :z/:Z en el montaje, o establece etiquetas persistentes con
semanage fcontext -a -t container_file_t y restorecon.
2) Síntoma: funciona en host Ubuntu, falla en host RHEL/Fedora
Causa raíz: SELinux en enforcing en una clase de host, AppArmor (o ninguno) en la otra. Tus manifiestos asumen la línea base equivocada.
Solución: Detecta y codifica la configuración de seguridad del host; añade etiquetado SELinux a bind mounts en hosts SELinux.
3) Síntoma: solo uno de varios contenedores puede escribir en un directorio compartido
Causa raíz: Uso de :Z (etiqueta privada) para contenido compartido; las categorías MCS no coinciden entre contenedores.
Solución: Usa :z para montajes compartidos, o deja de compartir y usa rutas/volúmenes por contenedor.
4) Síntoma: “permission denied” al acceder a un socket UNIX (p. ej., /var/run/…)
Causa raíz: El tipo SELinux en el archivo de socket prohíbe el dominio del contenedor, o el perfil AppArmor bloquea la ruta.
Solución: Evita montar sockets privilegiados del host cuando sea posible; de lo contrario etiqueta/permite la ruta del socket específicamente. Considera un servicio proxy en lugar de montajes directos de sockets.
5) Síntoma: el contenedor falla al intentar montar o usar FUSE
Causa raíz: AppArmor niega operaciones de montaje o capacidades necesarias; SELinux puede denegar acceso a dispositivos.
Solución: Revalúa la necesidad. Si es imprescindible, crea un perfil AppArmor a medida y permite las operaciones necesarias; evita un blanket unconfined.
6) Síntoma: tras reiniciar o relabel, el problema vuelve
Causa raíz: Usaste chcon o reetiquetado de Docker como arreglo ad-hoc sin reglas fcontext persistentes; o la provisión recrea directorios con etiquetas por defecto.
Solución: Usa semanage fcontext + restorecon, y asegura la creación de directorios vía gestión de configuración.
7) Síntoma: pusiste SELinux en permissive y todo “funciona”
Causa raíz: Comprobaste que era SELinux y te quedaste en la solución más cara.
Solución: Vuelve a poner SELinux en enforcing y corrige etiquetas/política. Permissive es para ventanas de diagnóstico, no para comodidad en producción.
8) Síntoma: volumen nombrado funciona, bind mount falla, misma ruta dentro del contenedor
Causa raíz: La ruta gestionada por Docker ya está etiquetada/permitida; el bind mount arbitrario no lo está.
Solución: Prefiere volúmenes nombrados. Usa bind mounts solo cuando la integración con el host sea necesaria, y entonces etiqueta/permite correctamente.
Listas de verificación / plan paso a paso
Checklist A: cuando un contenedor no puede escribir en un montaje
- Identifica el tipo de montaje y la ruta del host:
docker inspectmounts. - Reproduce con un simple
touchen el contenedor para aislar la lógica de la app de permisos. - Revisa rápidamente modo/propietario del host (
ls -ldn) para descartar desajustes de UID obvios. - Comprueba el estado SELinux (
getenforce) y la etiqueta (ls -ldZ) si está en enforcing. - Busca denegaciones AVC (
ausearch -m avc) que coincidan con la ruta y la operación. - Si AppArmor está activo, busca en los logs del kernel
apparmor="DENIED"e identifica el perfil. - Aplica la solución más pequeña: ajuste de etiqueta SELinux o cambio del perfil AppArmor.
- Vuelve a probar con la sonda mínima de escritura y luego con la app real.
- Hazlo persistente:
semanage fcontext/restorecono despliegue de perfil vía gestión de configuración. - Escribe la expectativa en el manifiesto del servicio (“este bind mount requiere container_file_t”).
Checklist B: endurecimiento sin romper todo
- Prefiere volúmenes nombrados para estado de la app a menos que necesites una ruta del host.
- Estandariza un pequeño conjunto de raíces para bind mounts (
/srv/containers/<app>) y etiquétalas consistentemente. - Centraliza logs de auditoría y kernel; alerta sobre ráfagas de denegaciones AVC/AppArmor.
- Mantén SELinux en enforcing y AppArmor en enforcing; trata las excepciones como controladas por cambio.
- No abuses de
--privileged. Si necesitas una capacidad, añade solo esa capacidad. - En CI, ejecuta una “sonda de permisos” con un contenedor contra los montajes esperados para detectar deriva de etiquetas/perfiles temprano.
Checklist C: día de migración (cuando mueves hosts o almacenamiento)
- Antes de mover directorios de datos, registra contextos:
ls -lZen la ruta antigua. - Tras la migración, aplica reglas fcontext y
restoreconen la nueva ruta. - Verifica con un contenedor mínimo que haga operaciones de crear/leer/borrar.
- Sólo entonces mueve tráfico de producción. Esto no es superstición; es evitar una sorpresa a las 2 a.m.
Una cita que envejeció bien en operaciones: “La esperanza no es una estrategia.” — General Gordon R. Sullivan
Preguntas frecuentes
1) ¿Por qué Docker muestra “permission denied” en vez de “SELinux denied”?
Porque el kernel devuelve un error de acceso genérico al syscall. La app y Docker ven EACCES. Los detalles reales
viven en los logs de SELinux/AppArmor, no en el mensaje de la aplicación.
2) ¿Debería deshabilitar SELinux o AppArmor para que los contenedores funcionen?
No, no como solución permanente. Usa permissive/complain brevemente para confirmar la sospecha, luego corrige etiquetas/reglas de perfil.
Desactivar MAC convierte escapes sutiles de contenedores en incidentes estruendosos más adelante.
3) ¿Cuál es la diferencia entre :z y :Z en montajes de Docker?
En hosts SELinux, provocan reetiquetado. :z está pensado para contenido compartido (múltiples contenedores).
:Z está pensado para contenido privado (un contenedor). Usar :Z en directorios compartidos provoca fallos entre contenedores.
4) Usé chcon y funcionó. ¿Por qué se rompió después?
chcon cambia etiquetas pero no las hace persistentes frente a operaciones de relabel o algunos flujos de provisión.
Usa semanage fcontext para definir una regla y luego aplícala con restorecon.
5) ¿Por qué un volumen nombrado Docker funciona cuando un bind mount falla?
Los volúmenes nombrados viven bajo directorios de almacenamiento gestionados por Docker e heredan etiquetas/rutas que las políticas ya esperan para uso de contenedores. Los bind mounts heredan la etiqueta/política que la ruta del host ya tiene.
6) ¿Puedo ejecutar contenedores sin confinar bajo AppArmor?
Puedes, pero deberías tratarlo como ejecutar sin barandillas. A veces es un movimiento diagnóstico temporal o una excepción deliberada para un host muy controlado. No debe ser el valor por defecto.
7) ¿Por qué esto aparece solo después de un parche del host o una actualización del SO?
Los paquetes de política, los perfiles por defecto y el comportamiento de etiquetado pueden cambiar con actualizaciones. Además, migraciones de almacenamiento y recreación de directorios pueden restablecer contextos a valores por defecto. La solución es codificar etiquetas/perfiles, no depender de “lo que haga el SO”.
8) ¿Es esto el mismo problema que el remapeo de user namespace o Docker rootless?
Capa distinta. userns/rootless afecta al mapeo UID/GID y a límites de capacidades. SELinux/AppArmor son capas de política LSM. Puedes tener ambos problemas a la vez, por eso debes revisar evidencia (logs de auditoría) en lugar de adivinar.
9) ¿Cómo decido entre cambiar la política y cambiar el diseño del contenedor?
Si continuamente abres agujeros en la política MAC por conveniencia, rediseña. Prefiere volúmenes nombrados, evita sockets de host, reduce privilegios. Si tienes una necesidad legítima de integración (p. ej., directorio compartido del host para backups), entonces haz un cambio de política preciso y documentado.
Conclusión: pasos prácticos siguientes
Los errores de permisos que nadie explica rara vez son misteriosos. Simplemente se informan mal. SELinux y AppArmor se sitúan por debajo de tu runtime de contenedores y no negocian con tu chmod. Hacen cumplir política, y los detalles
están en los rastros de auditoría y los logs del kernel.
Pasos siguientes que puedes hacer esta semana, sin empezar una guerra santa:
- Enseña a tu runbook de on-call a verificar SELinux/AppArmor antes de tocar los modos de archivo.
- Estandariza raíces de bind mount y aplica reglas fcontext SELinux persistentes donde sea relevante.
- Prefiere volúmenes nombrados a menos que la semántica de ruta del host sea realmente necesaria.
- Centraliza logs de auditoría y denegaciones del kernel para que “permission denied” sea un diagnóstico de dos minutos, no una discusión de dos horas.
- Cuando debas sobrescribir el confinamiento, hazlo intencionalmente, mínimamente y en código—nunca como un fósil de emergencia que viva para siempre.
Los contenedores ya son complicados. No los hagas espeluznantes. Hazlos observables, etiquetados correctamente y aburridos.
Lo aburrido es lo que se mantiene en funcionamiento.