Volúmenes Docker: Permission denied — Estrategias UID/GID que evitan el dolor

¿Te fue útil?

“Permission denied” en un volumen de Docker nunca es solo un problema de permisos. Es un problema de identidades desajustadas, vestido de error de sistema de archivos, y suele aparecer en el peor momento posible: durante un despliegue, una migración o un “pequeño cambio” que alguien juró que era seguro.

Si ejecutas contenedores en producción, no puedes tratar UID/GID como trivialidad. Los contenedores escriben archivos. Los hosts hacen cumplir la propiedad. El almacenamiento en red añade sus propias reglas. El endurecimiento de seguridad añade más reglas. Tu trabajo es alinear esas reglas.

Por qué esto sigue pasando (es identidad, no magia)

En Linux, los permisos de archivos se aplican mediante identificadores numéricos: UID para el propietario, GID para el grupo. Los nombres como www-data son solo una tabla de búsqueda. Cuando un proceso en un contenedor escribe en un directorio montado, el sistema de archivos del host ve un UID y un GID, no “el usuario del contenedor”.

Aquí está el problema central: la idea del contenedor de que UID 1000 es “appuser” puede coincidir con la idea del host de que UID 1000 es “alice”, y tu servidor NFS puede decidir que es “nobody”. Mismo número, distinto significado, o mismo significado, distinto número. En cualquier caso, el host dice: no.

Los volúmenes Docker hacen esto visible porque son literalmente sistemas de archivos (bind mounts) o directorios gestionados por el host (volúmenes nombrados). Tu contenedor no es una pequeña VM con su propio kernel y su propio universo de permisos. Son procesos con una vista restringida del host. El kernel es el portero, y comprueba IDs, no excusas.

Un borde afilado más: muchas imágenes oficiales están diseñadas para ejecutarse como root por defecto. Eso “funciona” hasta que montas una ruta del host propiedad de un usuario normal, o hasta que endureces el contenedor para que no corra como root, o hasta que tu backend de almacenamiento rechaza escrituras con root-squash. Entonces descubres que el camino “fácil” era deuda técnica con temporizador.

Broma corta #1: Los contenedores no “tienen permisos.” Piden prestados los tuyos, arruinan la alfombra y te dejan sosteniendo el informe de seguridad.

Hechos e historia que explican el dolor actual

  • Los permisos Unix son más antiguos que la mayoría de tu infraestructura: la aplicación de UID/GID viene de los primeros Unix; el modelo es simple, duradero e indiferente a los contenedores.
  • Los defaults tempranos de Docker favorecieron la conveniencia: los primeros flujos de trabajo de Docker promovían “ejecutar como root” porque evitaba fricciones; la seguridad en producción después hizo costosa esa elección.
  • Los bind mounts existían décadas antes que los contenedores: al kernel no le importa que un mount provenga de Docker; aplica las mismas reglas de propiedad.
  • Los namespaces de usuario existían mucho antes de popularizarse: las user namespaces en Linux llegaron hace años, pero la adopción tardó porque son potentes y fáciles de malconfigurar.
  • El root squash de NFS es un removedor de trampas intencional: muchas exportaciones NFS mapean root remoto a nobody por diseño, específicamente para evitar que root del contenedor se convierta en root del almacenamiento.
  • SELinux/AppArmor cambiaron las reglas del juego: las distribuciones modernas pueden negar acceso aun cuando UID/GID sean correctos, porque MAC (control de acceso obligatorio) está por encima de los permisos DAC clásicos.
  • Los sistemas de archivos overlay añaden síntomas confusos: overlay2 puede hacer que parezca que los permisos “cambiaron aleatoriamente,” cuando en realidad estás viendo capas fusionadas y directorios opacos.
  • Kubernetes normalizó cargas no-root: una vez que los clusters empezaron a hacer cumplir “runAsNonRoot,” la industria dejó de salirse con suposiciones descuidadas sobre UID/GID.

Hay una razón por la que esto no lo “arregló” un runtime de contenedores mejor: el kernel está haciendo exactamente lo que debe hacer.

Una cita operativa para tener en el escritorio: “La esperanza no es una estrategia.” — General Gordon R. Sullivan

Guion rápido de diagnóstico (primero/segundo/tercero)

Primero: identifica quién cree que es el proceso

Si no conoces el UID/GID efectivo dentro del contenedor, estás adivinando. Obtén los números. Confirma qué usuario ejecuta realmente el proceso de la app, no lo que implica el Dockerfile.

Segundo: identifica cuál es la ruta del host y quién la posee

“Volumen” puede significar bind mount, volumen nombrado, NFS, CIFS o un mount gestionado por un plugin. Cada uno tiene distinto comportamiento de propiedad y ACL. Determina la ruta de respaldo real y su propiedad, bits de modo, ACLs y cualquier etiqueta MAC.

Tercero: comprueba las capas de política por encima de los permisos POSIX

Cuando los bits de modo parecen correctos pero sigue fallando, suele ser SELinux, AppArmor o root squash en almacenamiento en red. No pases una hora haciendo chmod hasta caer en la zanja equivocada.

Árbol de decisión rápido

  • Funciona sin volumen, falla con volumen: desajuste de identidad o política de almacenamiento.
  • Funciona como root, falla como no-root: propiedad/mode bits del directorio incorrectos para el UID/GID previsto.
  • Funciona en disco local, falla en NFS/CIFS: root squash, idmapping o permisos/ACLs del servidor.
  • Solo falla en Fedora/RHEL: sospecha del etiquetado SELinux primero.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estos son los comandos que realmente ejecuto durante incidentes. Cada tarea incluye: comando, qué significa la salida y la decisión que tomas.

Task 1: confirmar el usuario del contenedor (UID/GID efectivo)

cr0x@server:~$ docker exec -it app sh -lc 'id; umask'
uid=10001(app) gid=10001(app) groups=10001(app),10002(shared)
0022

Significado: La app escribe como UID 10001/GID 10001, umask por defecto 0022 (archivos 644, directorios 755 salvo que se indique lo contrario).

Decisión: El directorio montado debe ser escribible por UID 10001 o por un grupo en el que esté el proceso (como GID 10002), o vía ACL.

Task 2: confirmar qué está montado y dónde (dentro del contenedor)

cr0x@server:~$ docker exec -it app sh -lc 'mount | sed -n "1,5p"; mount | grep -E "/data|/var/lib"'
overlay on / type overlay (rw,relatime,lowerdir=...,upperdir=...,workdir=...)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755,inode64)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
/dev/sdb1 on /data type ext4 (rw,relatime)

Significado: /data es un montaje real, ext4, no una ruta overlay dentro del contenedor.

Decisión: La resolución de problemas debe involucrar la propiedad/modes del sistema de archivos en el host para el punto de montaje ext4.

Task 3: inspeccionar la configuración del mount de Docker (vista del host)

cr0x@server:~$ docker inspect app --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/app/data","Destination":"/data","Mode":"rw","RW":true,"Propagation":"rprivate"}]

Significado: Es un bind mount desde /srv/app/data. Los permisos de ese directorio son el problema, no el driver de volúmenes de Docker.

Decisión: Arregla la propiedad/mode/ACL en /srv/app/data, o cambia el UID/GID en tiempo de ejecución del contenedor.

Task 4: inspeccionar propiedad, permisos y ACLs del directorio host

cr0x@server:~$ sudo ls -ldn /srv/app/data
drwxr-x--- 2 0 0 4096 Jan  2 10:11 /srv/app/data
cr0x@server:~$ sudo getfacl -p /srv/app/data
# file: /srv/app/data
# owner: root
# group: root
user::rwx
group::r-x
other::---

Significado: Propiedad root, grupo root y “other” sin acceso. UID 10001 no puede escribir ni siquiera leer.

Decisión: O bien chown/chgrp al identity del contenedor, o bien concede acceso vía grupo/ACL. No uses chmod 777 a menos que disfrutes revisiones de incidentes.

Task 5: reproducir la falla con una prueba de escritura (dentro del contenedor)

cr0x@server:~$ docker exec -it app sh -lc 'touch /data/.permtest && echo OK'
touch: cannot touch '/data/.permtest': Permission denied

Significado: No es tu aplicación. Es un fallo básico de escritura.

Decisión: Arregla el acceso al sistema de archivos primero; no reconfigures la app hasta que un simple touch funcione.

Task 6: comprobar si SELinux está negando (host)

cr0x@server:~$ getenforce
Enforcing
cr0x@server:~$ sudo ls -ldZ /srv/app/data
drwxr-x---. 2 root root unconfined_u:object_r:default_t:s0 4096 Jan  2 10:11 /srv/app/data

Significado: SELinux está en modo enforcing, y el directorio tiene una etiqueta genérica (default_t) que los contenedores pueden no estar autorizados a acceder.

Decisión: Añade opciones de montaje SELinux apropiadas (:Z o :z) para bind mounts, o relabela la ruta a un tipo amigable con contenedores.

Task 7: comprobar uso del perfil AppArmor (host)

cr0x@server:~$ docker inspect app --format '{{.AppArmorProfile}}'
docker-default

Significado: Se aplica el perfil AppArmor por defecto. Normalmente está bien, pero perfiles personalizados pueden bloquear mounts o rutas.

Decisión: Si los permisos parecen correctos y SELinux está desactivado, audita logs de AppArmor y reglas del perfil.

Task 8: identificar ruta de respaldo de volumen nombrado (si no es bind mount)

cr0x@server:~$ docker volume inspect appdata --format '{{.Mountpoint}}'
/var/lib/docker/volumes/appdata/_data
cr0x@server:~$ sudo ls -ldn /var/lib/docker/volumes/appdata/_data
drwxr-xr-x 2 0 0 4096 Jan  2 09:50 /var/lib/docker/volumes/appdata/_data

Significado: Los volúmenes nombrados por defecto son propiedad root en el host a menos que la imagen o un paso de init lo cambie.

Decisión: Crea un paso de inicialización controlado para establecer la propiedad una vez, o ejecuta el servicio con UID/GID coincidente.

Task 9: comprobar si estás en NFS y si el root squash te está afectando

cr0x@server:~$ mount | grep -E ' nfs| nfs4'
10.0.2.10:/exports/app on /srv/app/data type nfs4 (rw,relatime,vers=4.1,proto=tcp,clientaddr=10.0.2.21,local_lock=none,sec=sys)
cr0x@server:~$ sudo touch /srv/app/data/.hosttest
touch: cannot touch '/srv/app/data/.hosttest': Permission denied

Significado: Incluso root del host no puede escribir. Eso es root squash clásico o permisos/ACLs del servidor.

Decisión: Deja de tratar esto como un problema de Docker. Arregla los permisos de exportación NFS y el mapeo de IDs en el servidor, o usa un UID de servicio dedicado que exista de forma consistente en todos los clientes.

Task 10: verificar el mapeo numérico de propiedad entre host y contenedor

cr0x@server:~$ getent passwd 10001
appuser:x:10001:10001::/nonexistent:/usr/sbin/nologin
cr0x@server:~$ docker exec -it app sh -lc 'getent passwd 10001 || true; cat /etc/passwd | tail -n 3'
app:x:10001:10001:app:/home/app:/bin/sh
messagebus:x:100:102::/nonexistent:/usr/sbin/nologin
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin

Significado: UID 10001 existe en host y contenedor; buena señal. Si en el host aparece otro usuario para 10001, tu historia de “UID coincidente” es ficción.

Decisión: Si puedes, estandariza UIDs entre sistemas para servicios con estado. Si no puedes, usa ACLs o mounts con idmapping (donde estén disponibles) en lugar de pensar con deseos.

Task 11: probar acceso basado en grupo (la solución intermedia sensata)

cr0x@server:~$ sudo groupadd -g 10002 shared 2>/dev/null || true
cr0x@server:~$ sudo chgrp -R 10002 /srv/app/data
cr0x@server:~$ sudo chmod -R g+rwX /srv/app/data
cr0x@server:~$ sudo chmod g+s /srv/app/data
cr0x@server:~$ sudo ls -ldn /srv/app/data
drwxrws--- 2 0 10002 4096 Jan  2 10:15 /srv/app/data

Significado: El directorio es propiedad de grupo 10002 y el bit setgid está activado para que los nuevos archivos hereden el grupo 10002.

Decisión: Haz que el proceso del contenedor sea miembro de GID 10002 (vía imagen o tiempo de ejecución). Esto evita chownar todo a un solo UID y funciona mejor con acceso compartido.

Task 12: añadir un ACL para un UID específico (cuando los grupos no bastan)

cr0x@server:~$ sudo setfacl -m u:10001:rwx /srv/app/data
cr0x@server:~$ sudo setfacl -m d:u:10001:rwx /srv/app/data
cr0x@server:~$ sudo getfacl -p /srv/app/data | sed -n '1,12p'
# file: /srv/app/data
# owner: root
# group: shared
user::rwx
user:10001:rwx
group::rwx
mask::rwx
other::---
default:user::rwx
default:user:10001:rwx
default:group::rwx

Significado: UID 10001 tiene acceso explícito, y los nuevos archivos lo heredan vía ACL por defecto.

Decisión: Usa ACLs cuando necesites múltiples escritores con distintos UIDs, especialmente entre hosts, sin recurrir a 777.

Task 13: comprobar atributos inmutables (el momento “por qué chmod no funciona”)

cr0x@server:~$ sudo lsattr -d /srv/app/data
-------------e-- /srv/app/data

Significado: No hay bandera inmutable. Si ves i, los cambios fallarán silenciosa o con EPERM.

Decisión: Si la inmutabilidad está activada, quítala intencionadamente (chattr -i) y documenta por qué estaba ahí.

Task 14: verificar la ruta real de escritura y errores con strace (quirúrgico, no diario)

cr0x@server:~$ docker exec -it app sh -lc 'strace -f -e trace=file -o /tmp/trace.log sh -lc "touch /data/x" || true; tail -n 5 /tmp/trace.log'
openat(AT_FDCWD, "/data/x", O_WRONLY|O_CREAT|O_NOCTTY|O_NONBLOCK, 0666) = -1 EACCES (Permission denied)
write(2, "touch: cannot touch '/data/x': Permission denied\n", 52) = 52
exit_group(1)                           = ?
+++ exited with 1 +++

Significado: El kernel devolvió EACCES al crear. Esto es negación clásica DAC/MAC, no “archivo ya existe” ni “disco lleno”.

Decisión: Vuelve a propiedad/ACL/SELinux. No pierdas tiempo en configuración a nivel de aplicación.

Estrategias UID/GID que realmente funcionan

Strategy 1: Coincide con la propiedad del host (ejecuta el contenedor con el mismo UID/GID)

Este es el enfoque más limpio para bind mounts en un único host: elige un UID/GID de servicio en el host y ejecuta el proceso del contenedor con esa misma identidad numérica.

En Docker Compose, eso suele ser:

cr0x@server:~$ cat docker-compose.yml
services:
  app:
    image: yourorg/app:1.2.3
    user: "10001:10001"
    volumes:
      - /srv/app/data:/data:rw

Cuándo hacerlo: servicios con estado en nodo único, despliegues simples, discos locales, UIDs predecibles.

Evitar cuando: compartes el mismo volumen entre muchos hosts con asignación de UIDs inconsistente, o dependes de grupos suplementarios que difieren entre entornos.

Strategy 2: Acceso basado en grupo + directorios setgid (mejor por defecto para volúmenes compartidos)

Si más de un servicio necesita acceso, o si operadores tocan archivos, el acceso por grupos es tu amigo. Crea un GID compartido, asigna el grupo al directorio, activa setgid en el directorio y asegúrate de que los usuarios en el contenedor se unan al grupo.

En el lado del contenedor puedes añadir grupos suplementarios:

cr0x@server:~$ cat docker-compose.yml
services:
  app:
    image: yourorg/app:1.2.3
    user: "10001:10001"
    group_add:
      - "10002"
    volumes:
      - /srv/app/data:/data:rw

Por qué funciona: escala mejor que la propiedad por UID y el bit setgid evita la deriva de archivos con grupos aleatorios.

Strategy 3: ACLs para escritores mixtos (herramienta de precisión, no un mazo)

Las ACLs POSIX te permiten conceder acceso a múltiples UIDs/GIDs sin cambiar el propietario principal. Son ideales cuando el directorio lo administra un equipo pero lo escriben múltiples servicios con identidades numéricas diferentes.

Regla: Si usas ACLs, usa también ACLs por defecto, o arreglarás el incidente de hoy y romperás la creación de archivos de mañana.

Strategy 4: Inicialización única (chown una vez, no en cada arranque)

Muchas imágenes resuelven esto haciendo un chown -R al inicio. Eso es aceptable solo cuando el directorio de datos es pequeño y local. En producción con datos reales, un chown recursivo en cada arranque es un autoinfligido DoS.

Mejor: un job de init explícito que se ejecuta una vez por volumen nuevo, ajusta la propiedad y nunca vuelve a ejecutarlo a menos que se rote el almacenamiento intencionadamente.

Ejemplo: ejecutar un contenedor temporal para inicializar un volumen nombrado:

cr0x@server:~$ docker run --rm -u 0:0 -v appdata:/data busybox sh -lc 'mkdir -p /data && chown -R 10001:10001 /data && ls -ldn /data'
drwxr-xr-x    2 10001    10001         4096 Jan  2 10:20 /data

Decisión: Usa esto para volúmenes nombrados y provisión de primera ejecución. No incluyas chown recursivo en el arranque principal a menos que te gusten los reinicios lentos y las caídas largas.

Strategy 5: No luches contra la imagen — elige imágenes que soporten non-root correctamente

Algunas imágenes tienen un usuario non-root bien definido y respetan variables env como PUID/PGID, o aceptan un --user en tiempo de ejecución limpiamente. Otras asumen root y luego fallan de formas creativas cuando les niegas root.

Tu decisión: si una imagen para un servicio con estado no puede ejecutarse como non-root sin hacks, considérala un pasivo. O arregla el Dockerfile internamente o cambia la imagen. “Pero funciona como root” no es una postura de seguridad.

Strategy 6: Remapeo de namespaces de usuario (aislamiento fuerte, complejidad extra)

Los user namespaces permiten mapear root del contenedor (UID 0) a un rango de UID no privilegiados en el host. Eso reduce el radio de impacto cuando un contenedor escapa. También hace confusos los permisos de volumen si no planificas.

Cuando está habilitado, un archivo creado por “root en el contenedor” podría aparecer como UID 165536 en el host, porque ese es el UID mapeado. Eso es comportamiento correcto. Solo sorprende si no lo esperabas.

Usar cuando: necesitas más protección del host y puedes estandarizar mapeos entre nodos.

Evitar cuando: dependes mucho de bind mounts a directorios gestionados por humanos y no toleras la complejidad del mapeo.

Strategy 7: Docker rootless (buena base de seguridad, aún requiere planificación)

Docker rootless ejecuta el daemon y los contenedores sin privilegios root. Reduce incidentes donde root del contenedor se convierte en root del host. Pero los volúmenes aún deben tener la propiedad correcta bajo el usuario rootless y los rangos subordinados de UID/GID.

Punto clave: rootless cambia dónde vive el almacenamiento y cómo se mapean los IDs. No es un reemplazo directo para “todo bajo /srv”.

Strategy 8: Kubernetes: runAsUser + fsGroup (si estás en ese mundo)

En Kubernetes, el equivalente de “estrategia UID/GID” suele ser un securityContext con runAsUser, runAsGroup y fsGroup. fsGroup ayuda porque el kubelet puede ajustar la propiedad de grupo/permisos en volúmenes montados para que la escritura por grupo funcione.

Pero no trates fsGroup como magia. En algunos tipos de volumen requiere un cambio recursivo de permisos, que puede ser dolorosamente lento en datasets grandes. Planifícalo.

Broma corta #2: “Solo chmod 777” es el equivalente en almacenamiento a “solo reinícialo”: a veces efectivo, siempre sospechoso.

Modos de fallo según almacenamiento (ext4, NFS, CIFS, ZFS, overlay)

Sistemas de archivos locales (ext4/xfs): predecibles, pero fáciles de sabotear

En disco local, UID/GID y bits de modo suelen explicar todo. Si está mal, es porque humanos (o scripts de init) lo pusieron mal. Los patrones comunes de sabotaje:

  • Ruta del host creada como root durante la provisión y nunca chowned.
  • El directorio es writable por grupo, pero los archivos no heredan el grupo porque setgid no está activado.
  • Umask demasiado restrictiva (p. ej., 0077) y los archivos se vuelven privados por defecto.
  • La máscara de ACL restringe el acceso, aun cuando existen entradas ACL.

NFS: la identidad es política

Los problemas de permisos en NFS son a menudo problemas de mapeo de identidad. Si usas AUTH_SYS (el clásico “sec=sys”), el servidor confía en que el cliente envíe UIDs. Eso significa que la consistencia de IDs numéricos entre máquinas no es opcional; es todo el modelo de seguridad.

Si root squash está habilitado (a menudo por defecto), UID 0 en el cliente se mapea a un usuario no privilegiado en el servidor. En mundo contenedor, eso significa que “ejecutar como root” no es el botón fácil que creías.

Consejo práctico: elige cuentas de servicio con UIDs fijos en todos los nodos, gestionalas centralmente y no esperes que el mapeo por nombre te salve. NFS no se preocupa por cómo llamas al usuario.

CIFS/SMB: permisos que pueden ser simulados en el cliente

Los mounts SMB en Linux pueden presentar una vista de permisos parcialmente sintética. Opciones de montaje como uid=, gid=, file_mode= y dir_mode= pueden hacer que todo parezca escribible, hasta que el servidor diga lo contrario mediante sus propias ACLs.

Modo de fallo: Crees que lo arreglaste cambiando opciones de montaje, pero la ACL del servidor aún deniega escrituras. O lo “arreglas” forzando la propiedad y entonces rompes la auditabilidad porque cada archivo está a nombre del mismo UID local.

ZFS: excelente con los datos, estricto con la propiedad

ZFS no es especial respecto a permisos POSIX; es solo consistente. Esa consistencia puede ser brutal cuando esperas “Docker se encargará.” Si haces snapshot y rollback, los cambios de propiedad también retroceden. Eso es una característica y también una trampa durante la respuesta a incidentes.

overlay2: la ilusión de capas escribibles

El root filesystem de tu contenedor suele ser overlay2: una unión de capas de imagen de solo lectura y una capa superior escribible. Los mounts de volúmenes lo evitan. Así que si tu app escribe bien en /tmp pero falla en /data, eso es esperado: /tmp está dentro de la capa escribible del contenedor, mientras que /data lo aplica el sistema de archivos del host que montaste.

SELinux: los permisos pueden parecer correctos y aún así estar mal

En sistemas con SELinux, la etiqueta importa. Un directorio bind-montado etiquetado default_t puede ser ilegible para contenedores incluso si UID/GID son perfectos. Docker soporta relabeling con flags de montaje:

  • :Z para una etiqueta privada (exclusiva a un contenedor)
  • :z para una etiqueta compartida (múltiples contenedores)

Elige con deliberación. Si compartes la misma ruta del host entre contenedores y usas :Z, la relabelarás hacia una esquina.

Tres mini-historias corporativas desde el frente

Mini-historia 1: El incidente causado por una suposición equivocada (“root puede escribir en cualquier sitio”)

Una empresa mediana ejecutaba un job ETL contenedorizado que escribía archivos parquet en una exportación NFS compartida. Había estado “estable” durante meses, lo que mayormente significaba que nadie lo tocaba. Entonces seguridad les pidió dejar de ejecutar contenedores como root, y el equipo cumplió poniendo user: "10001:10001" en Compose.

La noche siguiente, el job ETL falló inmediatamente: Permission denied en el directorio de salida. El ingeniero on-call intentó la solución clásica: ejecutarlo como root otra vez. Mismo error. Ahí murió la suposición: “root en el contenedor equivale a root en el almacenamiento.” No era así. NFS root squash mapeó root del cliente a una identidad no privilegiada en el servidor.

Luego probaron el siguiente arreglo clásico: chmod 777 al directorio en el punto de montaje del cliente. No cambió nada. Porque las ACL del servidor seguían negando escrituras, y chmod en el cliente no estaba cambiando la política del servidor como imaginaban.

La solución real fue aburrida: crearon una cuenta de servicio dedicada con UID/GID fijo, se aseguraron de que esa cuenta existiera con los mismos números en todos los nodos ETL y ajustaron la propiedad servidor-side acorde. También añadieron una comprobación previa simple: crear un archivo temporal en el directorio objetivo antes de ejecutar el job caro.

El postmortem fue incluso más aburrido: actualizaron sus runbooks para tratar NFS como un dominio de seguridad separado con sus propias reglas. Ese párrafo ahorró a futuros on-call repetir el mismo baile.

Mini-historia 2: La optimización que salió mal (chown recursivo en el arranque)

Otro equipo ejecutaba un servicio con estado en contenedores con un volumen nombrado. Un ingeniero notó errores de permisos ocasionales tras migraciones y decidió “endurecer” el arranque: cada boot del contenedor ejecutaba chown -R app:app /var/lib/app antes de iniciar el daemon.

En dev fue genial. Volumen fresco, datos pequeños, chown rápido, sin más problemas de permisos. En producción el dataset era grande y vivía en discos lentos. Durante un deploy rutinario el servicio arrancó despacio, luego más despacio, luego pareció muerto. El health check falló. El orquestador lo reinició. Lo que disparó otro chown recursivo. Ahora tenían un loop de reinicio haciendo pesadas operaciones de metadata en el mismo árbol de directorios.

Los gráficos de almacenamiento mostraron la verdad: IOPS altos, mayormente escrituras de metadata y picos de latencia que afectaron otros servicios. La caída no la provocó la aplicación. La provocó una “optimización” que convirtió cada reinicio en una prueba de estrés al almacenamiento.

Lo arreglaron eliminando el chown de arranque, reemplazándolo por un job de init que solo corría cuando se creaba un volumen nuevo. También cambiaron su estrategia de despliegue: no rotar todas las instancias a la vez y no entrar en reinicio continuo sin backoff.

La lección: las soluciones de permisos que escalan linealmente con el tamaño de los datos acabarán escalando hasta provocar una alerta.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día (UIDs estándar y un contrato de permisos)

Un equipo de plataforma en una gran empresa tenía una regla: cada contenedor con estado corre con un UID de servicio dedicado, con una asignación centralizada de UID/GID. Los desarrolladores se quejaban que era burocracia. Querían “usar simplemente 1000.” El equipo de plataforma se negó, educada y repetidamente.

Meses después, un nodo tuvo que reconstruirse rápidamente y un servicio fue reubicado en una máquina nueva. El servicio arrancó, adjuntó su volumen persistente y empezó a escribir inmediatamente. Sin errores de permisos, sin chown manual, sin pánico.

¿Por qué? El mapeo UID/GID era consistente en la flota. La propiedad del volumen coincidía con la cuenta de servicio en todas partes. El usuario runtime del contenedor estaba fijado. La estructura de directorios tenía setgid y ACLs por defecto donde se requería compartir. Nada ingenioso, nada emocionante. Solo un contrato.

En la revisión post-incident, alguien preguntó si fue suerte. La respuesta del líder SRE fue esencialmente: la suerte es para la lotería; nosotros tenemos runbooks.

Ese equipo aún tuvo outages. Todos los tenemos. Pero no tuvieron este tipo de fallo, que ya es mucho por unas cuantas decisiones de política tomadas temprano.

Errores comunes: síntoma → causa raíz → solución

1) Síntoma: funciona en el contenedor sin volumen, falla con bind mount

Causa raíz: la propiedad/mode bits de la ruta del host no permiten que el UID del contenedor escriba.

Solución: chown/chgrp el directorio del host para que coincida con UID/GID del contenedor, o usa grupo compartido + setgid + group_add, o usa ACLs.

2) Síntoma: funciona como root, falla como non-root

Causa raíz: la imagen fue diseñada alrededor de root, o el directorio del host es propiedad de root y no escribible por el UID previsto.

Solución: elige un UID/GID conocido y ajusta la propiedad en consecuencia; prefiere imágenes que soporten non-root; evita hacks de “sudo dentro del contenedor”.

3) Síntoma: los permisos parecen correctos, aún así “Permission denied” en Fedora/RHEL

Causa raíz: desajuste de etiqueta SELinux para bind mount.

Solución: monta con :Z/:z o relabela la ruta del host apropiadamente; confirma con ls -Z y logs de audit.

4) Síntoma: no se puede escribir en NFS incluso como root en el host

Causa raíz: root squash de NFS o negación por ACLs en el servidor.

Solución: arregla permisos/ACLs del export en el servidor; usa un UID de servicio dedicado consistente entre clientes; no intentes chmodear la política de NFS.

5) Síntoma: fallos intermitentes después del deploy; a veces arregla con reinicio

Causa raíz: carrera entre scripts de init y arranque de la app, o múltiples contenedores inicializando el mismo volumen de forma distinta.

Solución: aisla la inicialización (job one-time), impone propiedad determinista y evita comportamiento concurrente de “init” en réplicas múltiples.

6) Síntoma: el directorio del volumen es escribible, pero los archivos nuevos tienen grupo incorrecto

Causa raíz: falta setgid en directorios o falta ACL por defecto; umask demasiado restrictiva.

Solución: activa setgid en directorios compartidos; aplica ACLs por defecto; revisa umask y comportamiento de la app.

7) Síntoma: cambiar permisos en el host no cambia lo que ve el contenedor

Causa raíz: no estás editando la ruta de respaldo real (volumen nombrado vs bind mount), o estás en SMB/NFS con política server-side.

Solución: docker inspect la fuente del mount; para volúmenes nombrados usa docker volume inspect; para almacenamiento en red, cambia reglas en el servidor.

8) Síntoma: después de habilitar userns-remap, todo “se volvió” UID 165536

Causa raíz: el mapeo de user namespace está funcionando; tus herramientas y expectativas no.

Solución: planifica rangos de mapeo UID; asegúrate de que rutas del host y automatización entiendan la propiedad mapeada; evita bind mounts a directorios gestionados por humanos en contenedores remapeados.

Listas de verificación / plan paso a paso

Checklist A: Detén la hemorragia durante un incidente (10–15 minutos)

  1. Prueba que sean permisos: touch en el directorio montado desde dentro del contenedor. Si falla, procede; si tiene éxito, es tu app.
  2. Obtén UID/GID efectivo: id dentro del contenedor, no lo del Dockerfile.
  3. Confirma el tipo de mount: docker inspect → ¿es bind o volumen nombrado?
  4. Revisa propiedad/mode/ACL del host: ls -ldn y getfacl en la ruta fuente.
  5. Revisa SELinux: getenforce y ls -Z en la ruta host.
  6. Revisa almacenamiento en red: mount en host; si es NFS/CIFS, sospecha reglas server-side.
  7. Elige la solución menos peligrosa: prefiere escritura basada en grupo o una ACL dirigida; evita 777; evita chown recursivo en árboles grandes.

Checklist B: Hacer que no vuelva a ocurrir (el contrato de producción)

  1. Estandariza UIDs/GIDs de servicio: asigna IDs numéricos fijos para cada servicio con estado entre entornos.
  2. Documenta el contrato del volumen: “Esta ruta debe ser escribible por UID X y/o GID Y, con setgid y ACL por defecto.” Ponlo en el repo.
  3. Usa grupo + setgid para rutas compartidas: es el modelo escalable más sencillo para múltiples escritores.
  4. Gestiona la inicialización explícitamente: job one-time para establecer propiedad/permisos en volúmenes nuevos.
  5. Decide la política SELinux: aplica etiquetado correcto en manifests de Compose/Kubernetes.
  6. Prueba con un preflight: CI o entrypoint que verifique test -w en dirs requeridos y falle rápido con buen mensaje.
  7. Mantén a los humanos fuera del camino de datos: si operadores deben tocar archivos, dales acceso por grupo; no “sudo editar” archivos cambiando propiedad de forma impredecible.

Checklist C: Si debes usar almacenamiento en red

  1. NFS: asegura consistencia UID/GID entre nodos; decide root squash intencionalmente; gestiona permisos server-side.
  2. CIFS: sé explícito con opciones de montaje; entiende si los permisos se aplican server-side; evita fingir que los bits de modo son reales si no lo son.
  3. Latencia y coste de metadata: evita cambios recursivos de permisos en árboles enormes; planifica el comportamiento de arranque en consecuencia.

Preguntas frecuentes

1) ¿Por qué no importa el nombre del usuario del contenedor para los permisos de volumen?

Porque el kernel aplica permisos usando UID/GID numéricos. Los nombres son solo entradas en /etc/passwd (o NSS). El host ve números.

2) ¿Debo ejecutar contenedores como root para evitar problemas de permisos?

No. Desplaza el riesgo de “permission denied” a “compromiso del host” y aun así falla con NFS root squash y políticas SELinux. Arregla el mapeo de identidades en su lugar.

3) ¿Cuál es el valor por defecto mejor para directorios escribibles compartidos?

Crea un GID compartido, chgrp al directorio, aplica g+rwX, activa setgid y asegura que los contenedores se unan a ese GID. Añade ACLs por defecto si es necesario.

4) ¿Es aceptable chmod 777 alguna vez?

Como diagnóstico breve para demostrar que es un problema de permisos, quizá. Como solución, es descuidado y a menudo innecesario. Usa escritura por grupo o ACLs.

5) ¿Por qué falla solo en un host?

Usualmente desajuste UID/GID (asignación de usuarios diferente), distinto modo/etiquetas SELinux, o la “misma ruta” en realidad apunta a distinto almacenamiento (local vs NFS).

6) ¿Cuál es la diferencia entre volúmenes nombrados y bind mounts respecto a permisos?

Los bind mounts usan una ruta existente del host (tu responsabilidad). Los volúmenes nombrados son gestionados bajo el directorio de datos de Docker y suelen arrancar con propiedad root a menos que se inicialicen.

7) ¿Cómo arreglo permisos para un volumen nombrado de forma segura?

Ejecuta un contenedor de init único (o un docker run temporal) como root para establecer la propiedad en el volumen, luego ejecuta el servicio principal como non-root.

8) ¿Por qué veo UID 165536 (o similar) en archivos del host?

Probablemente habilitaste user namespace remapping o modo rootless. Los UIDs del contenedor se mapean a un rango subordinado de UID en el host. Eso es esperado.

9) ¿Por qué los permisos parecen correctos pero las escrituras fallan?

SELinux/AppArmor pueden negar acceso incluso si los permisos POSIX lo permiten. En sistemas con SELinux, el etiquetado es un culpable frecuente para bind mounts.

10) ¿Puedo “arreglar” esto añadiendo usuarios en /etc/passwd dentro del contenedor?

Añadir un nombre ayuda en logs y herramientas, pero no cambia el UID numérico. La solución sigue siendo: coincidir UIDs, usar grupo/ACLs o ajustar mappings.

Conclusión: siguientes pasos que puedes hacer hoy

Los problemas de permisos en volúmenes Docker son predecibles. Esa es la buena noticia. Ocurren cuando identidades numéricas y capas de política no concuerdan. Tu objetivo no es “probar chmod hasta que funcione.” Tu objetivo es definir un contrato de permisos y aplicarlo de forma consistente.

Haz esto a continuación:

  1. Elige una estrategia UID/GID por servicio: coincidir UID para casos simples, grupo compartido + setgid para acceso compartido, ACLs para escritores mixtos.
  2. Elimina chown recursivo del arranque normal: reemplázalo por un step de init one-time ligado a la creación del volumen.
  3. Haz que el diagnóstico sea rápido: incorpora checks de preflight (id, test -w) y documenta el UID/GID esperado en el repo.
  4. Maneja SELinux y almacenamiento en red intencionalmente: etiquetas y políticas server-side no son “casos marginales”. Son producción.

Una vez que trates UID/GID como parte de la especificación de despliegue, “Permission denied” deja de ser un misterio y pasa a ser una prueba unitaria que olvidaste escribir.

← Anterior
Por qué 640×480 se sintió eterno: estándares que no sueltan
Siguiente →
Proxmox “no se pudo resolver el host”: flujo rápido de diagnóstico raíz para DNS/IPv6/proxy

Deja un comentario