Un 403 es el tipo de error que hace que los equipos discutan con mucha confianza. «Es nginx.» «No, es Apache.»
«Es SELinux.» (En Debian. Claro.) Mientras tanto la página está caída, el de guardia está despierto y alguien está
a un toque de teclado de chmod -R 777.
Este caso trata de detener los 403 de la manera aburrida y correcta: entender la semántica de permisos de Linux para
web roots en Debian/Ubuntu, diagnosticar rápido y establecer un modelo que mantenga producción segura y
desplegable. El objetivo es simple: el servidor web puede leer lo que necesita, escribir solo donde explícitamente
lo permites, y nadie tiene que “intentar 777”.
Ficha de diagnóstico rápido
Cuando ves un 403, estás buscando uno de tres cuellos de botella: la configuración del servidor web lo niega,
el sistema de archivos lo niega, o una capa intermedia (chroot, montaje de contenedor, AppArmor) lo niega.
No adivines. Demuéstralo.
Primero: confirma qué capa está denegando
- Revisa el log de errores del servidor web buscando “permission denied”, “access forbidden by rule” o “directory index forbidden”.
- Verifica el mapeo de la petición: ¿qué ruta en disco está intentando servir?
- Reproduce localmente usando el usuario efectivo del servidor web (
www-dataen Debian/Ubuntu por defecto).
Segundo: recorre la ruta, no solo el archivo
La mayoría de fallos de permisos no están en el archivo. Están en un directorio padre que olvidaste que existía.
Cada directorio en la ruta necesita el bit de execute para la traversería. No “read”. Execute.
Tercero: busca bloqueadores de “política”
- Perfiles AppArmor (Ubuntu) pueden negar lecturas aun cuando los bits de modo parecen correctos.
- Montajes de volúmenes en contenedores pueden cambiar propiedad/modos de formas sorprendentes.
- Symlinks pueden apuntar fuera de raíces permitidas (Apache) o hacia directorios sin traversería.
Si haces esos tres pasos, dejarás de tratar los 403 como un evento sobrenatural.
El modelo de permisos que realmente funciona (y que no acaba en 777)
En Debian/Ubuntu, los procesos worker del servidor web normalmente se ejecutan como www-data.
Tu usuario de despliegue típicamente es deploy o un runner de CI. El error es intentar que un
único conjunto de permisos satisfaga tanto “deploy escribe todo” como “servidor web lee todo” convirtiendo
el árbol entero en un baño público.
Aquí está el modelo sensato que recomiendo para la mayoría de equipos:
Modelo A: web root de solo lectura, subdirectorios escritos explícitamente
- Web root (
/var/www/site) es propiedad de un owner de despliegue (o root), legible porwww-data, sin permiso de escritura para este. - Directorios escribibles se separan:
var/,storage/,uploads/, directorios de cache, sockets si son necesarios. - Los escribibles son propiedad de www-data (o tienen una ACL que otorgue escritura a www-data), y nada más lo es.
Eso te da propiedades de seguridad previsibles: una compromisión del servidor web no concede automáticamente
la capacidad de modificar el código de la aplicación. Aun así puede escribir uploads porque tú decidiste
que puede. No porque entraste en pánico.
Modelo B: grupo compartido para despliegues (cuando debes permitir escrituras)
- Crea un grupo (p. ej.
web) que incluya al usuario deploy y awww-data. - Establece la propiedad de grupo del web root a
web. - Usa
chmod 2775(setgid) en directorios para que nuevos archivos hereden el grupo. - Configura un
umasksensato en tu proceso de despliegue para que los archivos sean legibles por grupo (y escribibles si es necesario).
El Modelo B es más flexible, pero tiene aristas: un umask malo y has creado una lotería de permisos.
Si lo vas a usar, hazlo con disciplina y comprobaciones.
Exactamente una cita, porque el resto deberían ser logs: “La esperanza no es una estrategia.”
— Gene Kranz
Broma #1: chmod 777 es como arreglar una ventana rota quitando toda la pared. Técnicamente ya no queda vidrio que romper.
Hechos y contexto histórico (para que las partes raras tengan sentido)
- El bit execute en directorios originalmente significaba “buscar”: puedes recorrer el directorio y acceder a inodos si ya conoces los nombres.
- 403 vs 404 suele ser una elección: muchos servidores devuelven 404 en problemas de permisos para evitar revelar la existencia de contenido.
- www-data como convención se volvió común en sistemas derivados de Debian porque los paquetes necesitaban un usuario no-login predecible para daemons.
- La postura histórica de Apache sobre symlinks es conservadora: seguir symlinks y permitir overrides ha sido fuente recurrente de incidentes de seguridad.
- Nginx popularizó patrones “try_files” que pueden cambiar qué ruta se accede realmente, confundiendo la resolución de permisos si inspeccionas el archivo equivocado.
- Las ACLs POSIX se introdujeron para expresar permisos más allá de owner/group/other, impulsadas por NFS empresariales y sistemas multiusuario donde los bits Unix eran demasiado toscos.
- Setgid en directorios es viejo pero aún relevante: es una de las pocas formas portables de forzar herencia de grupo sin depender de defaults de ACL.
- Defaults de umask provienen de épocas multiusuario de time-sharing: por defecto permitir escritura al grupo se consideraba arriesgado incluso cuando la gente era más amable.
Tareas prácticas: comandos, salidas, decisiones
Estas son tareas “ejecuta esto ahora”. Cada una incluye qué significa la salida y la decisión que deberías tomar.
Úsalas en orden cuando estás bajo presión, y otra vez después cuando arregles el sistema adecuadamente.
Tarea 1: confirma el usuario del servicio y la identidad del worker activo
cr0x@server:~$ ps -o user,group,pid,cmd -C nginx | head
USER GROUP PID CMD
root root 1021 nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data www-data 1022 nginx: worker process
www-data www-data 1023 nginx: worker process
Significado: Los workers corren como www-data. Esa es la identidad que necesita acceso al sistema de archivos.
Decisión: Todas las pruebas “¿puede leer esto?” deben ejecutarse como www-data.
Tarea 2: obtén la ruta en disco desde la configuración del servidor web
cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '/server_name example.com/,+40p' | sed -n '1,40p'
server {
listen 80;
server_name example.com;
root /var/www/example/current/public;
index index.html index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
Significado: La raíz efectiva es /var/www/example/current/public, no el checkout del repo que estabas mirando.
Decisión: Haz troubleshooting de permisos en esa ruta y en sus padres, incluido el destino del symlink current.
Tarea 3: revisa el log de errores para la redacción exacta de la denegación
cr0x@server:~$ sudo tail -n 8 /var/log/nginx/error.log
2025/12/30 10:12:14 [error] 1022#1022: *884 open() "/var/www/example/current/public/index.html" failed (13: Permission denied), client: 203.0.113.10, server: example.com, request: "GET / HTTP/1.1", host: "example.com"
Significado: El error 13 es una denegación a nivel kernel. Esto no es una regla de “deny” de nginx.
Decisión: Enfócate en permisos del sistema de archivos, propiedad, ACLs y traversería de ruta. Deja los debates de configuración para después.
Tarea 4: recorre los permisos de la ruta completa (la parte aburrida que lo arregla)
cr0x@server:~$ namei -l /var/www/example/current/public/index.html
f: /var/www/example/current/public/index.html
drwxr-xr-x root root /
drwxr-xr-x root root var
drwxr-xr-x root root www
drwxr-x--- deploy web example
lrwxrwxrwx deploy web current -> releases/20251230T1005
drwxr-s--- deploy web releases
drwxr-s--- deploy web 20251230T1005
drwxr-s--- deploy web public
-rw-r----- deploy web index.html
Significado: /var/www/example es drwxr-x---. “Other” no tiene execute, y www-data no es ni owner ni pertenece al grupo web (todavía).
Decisión: O agregas www-data al grupo web (Modelo B), o cambias los derechos de traversería para que www-data pueda atravesar pero no escribir (Modelo A).
Tarea 5: verifica la pertenencia a grupos (y recuerda reiniciar workers si hace falta)
cr0x@server:~$ id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Significado: www-data no está en web. El acceso basado en grupo no funcionará hasta que lo agregues.
Decisión: Si optas por el modelo de grupo compartido, añade la membresía y recarga el servicio para que recoja los grupos.
Tarea 6: prueba acceso como el usuario web (sin adivinar)
cr0x@server:~$ sudo -u www-data test -r /var/www/example/current/public/index.html; echo $?
1
Significado: El código de salida 1 significa “no legible”. Eso coincide con el log.
Decisión: Arregla permisos en directorios/archivos para que la prueba devuelva 0.
Tarea 7: comprueba específicamente el bit execute del directorio (trampa común de ruta)
cr0x@server:~$ sudo -u www-data ls -ld /var/www/example /var/www/example/current /var/www/example/current/public
ls: cannot access '/var/www/example/current': Permission denied
drwxr-x--- 5 deploy web 4096 Dec 30 10:05 /var/www/example
Significado: La denegación ocurre en la traversía de /var/www/example. Aunque los archivos fueran legibles por el mundo, no puedes alcanzarlos.
Decisión: Concede traversía (execute) a www-data mediante pertenencia a grupo, ACL, o execute para “other” donde sea aceptable.
Tarea 8: implementar el Modelo B de forma segura (grupo compartido + setgid)
cr0x@server:~$ sudo groupadd -f web
cr0x@server:~$ sudo usermod -aG web www-data
cr0x@server:~$ sudo usermod -aG web deploy
cr0x@server:~$ sudo chgrp -R web /var/www/example
cr0x@server:~$ sudo find /var/www/example -type d -exec chmod 2775 {} \;
cr0x@server:~$ sudo find /var/www/example -type f -exec chmod 0644 {} \;
Significado: Los directorios pasan a heredar grupo (2 en 2775), legibles/traversables por el grupo. Los archivos pasan a ser legibles por el grupo.
Decisión: Recarga nginx para que los procesos worker recojan el nuevo grupo suplementario; luego vuelve a probar el acceso como www-data.
Tarea 9: recarga el servicio para recoger nuevos grupos (no omitas esto)
cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ ps -o pid,user,group,cmd -C nginx | head -n 4
PID USER GROUP CMD
1021 root root nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
2110 www-data www-data nginx: worker process
2111 www-data www-data nginx: worker process
Significado: El grupo primario sigue siendo www-data (está bien). Los grupos suplementarios no se muestran aquí.
Decisión: Verifica los grupos suplementarios probando acceso de nuevo; si aún falla, el worker puede no tener el nuevo grupo (reinicia en lugar de recargar).
Tarea 10: volver a probar acceso como www-data después del cambio
cr0x@server:~$ sudo -u www-data test -r /var/www/example/current/public/index.html; echo $?
0
Significado: El acceso de lectura funciona ahora.
Decisión: Confirma que la respuesta HTTP es 200 y que no hiciste áreas escribibles demasiado amplias por accidente.
Tarea 11: inspecciona el umask que puede sabotear futuros despliegues
cr0x@server:~$ sudo -u deploy sh -lc 'umask; touch /var/www/example/current/public/.umask-test && stat -c "%a %U:%G %n" /var/www/example/current/public/.umask-test'
0022
644 deploy:web /var/www/example/current/public/.umask-test
Significado: Con umask 0022, los archivos nuevos son 0644. Eso está bien para web roots de solo lectura. Si necesitaras escritura por grupo, esto lo rompería.
Decisión: Prefiere roots de solo lectura. Si realmente necesitas escritura por grupo, establece umask en 0002 en el entorno de despliegue y prueba que se mantenga así.
Tarea 12: identifica directorios escribibles (y asegura todo lo demás)
cr0x@server:~$ sudo find /var/www/example/current -maxdepth 3 -type d -name storage -o -name uploads -o -name cache
/var/www/example/current/storage
/var/www/example/current/public/uploads
/var/www/example/current/var/cache
Significado: Estos son los típicos paths donde la app necesita escribir.
Decisión: Haz que solo estos sean escribibles por www-data. Todo lo demás debería ser de solo lectura para el servidor web.
Tarea 13: establece propiedad explícita para directorios escribibles (híbrido Modelo A)
cr0x@server:~$ sudo chown -R www-data:www-data /var/www/example/current/storage /var/www/example/current/public/uploads /var/www/example/current/var/cache
cr0x@server:~$ sudo find /var/www/example/current/storage /var/www/example/current/public/uploads /var/www/example/current/var/cache -type d -exec chmod 0750 {} \;
cr0x@server:~$ sudo find /var/www/example/current/storage /var/www/example/current/public/uploads /var/www/example/current/var/cache -type f -exec chmod 0640 {} \;
Significado: La app puede escribir donde debe, pero no puede mutar el código en otros lugares.
Decisión: Si tu usuario de despliegue necesita gestionar estos directorios también, usa ACLs en lugar de aflojar modos.
Tarea 14: verifica que no existan permisos world-writable accidentales
cr0x@server:~$ sudo find /var/www/example -xdev -perm -0002 -type f -o -perm -0002 -type d | head
Significado: Sin salida es bueno. Si hay salida significa que algo es world-writable, lo cual normalmente es un incidente esperando a ocurrir.
Decisión: Si hay salida, corrige la propiedad/ACL y elimina world-write. No negociéis con ello.
Tarea 15: diagnostica denegaciones de AppArmor en Ubuntu (cuando los modos parecen correctos)
cr0x@server:~$ sudo journalctl -k -g DENIED -n 5
Dec 30 10:14:02 server kernel: [12345.678901] audit: type=1400 apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/srv/apps/example/current/public/index.html" pid=2110 comm="nginx" requested_mask="r" denied_mask="r" fsuid=33 ouid=1001
Significado: El kernel denegó la lectura por la política AppArmor. Los bits del sistema de archivos no son el problema.
Decisión: Ajusta el perfil AppArmor (o mueve el web root a una ruta permitida). No sigas haciendo chmod; no ayudará.
Tarea 16: atrapa sorpresas de symlinks (especialmente Apache)
cr0x@server:~$ readlink -f /var/www/example/current/public
/var/www/example/releases/20251230T1005/public
Significado: Estás sirviendo desde un directorio de releases. Los permisos deben ser consistentes en cada nuevo path de release.
Decisión: Incorpora la normalización de permisos en el paso de despliegue (o establece ACLs por defecto en directorios padres) para que cada release sea correcta.
Broma #2: Los permisos Unix son simples—hasta que necesitas que sean correctos.
Nginx vs Apache: distintos valores por defecto, misma realidad de sistema de archivos
Modos de fallo de Nginx que parecen problemas de permisos
Nginx suele ser directo respecto a denegaciones de sistema de archivos: verás “(13: Permission denied)” en el log de errores.
Pero la configuración aún puede crear síntomas “tipo permisos”:
- try_files cambia el objetivo: solicitas
/pero nginx intenta/index.php. Los permisos deben funcionar también para el fallback. - autoindex off + falta archivo index: puedes obtener 403 si el listado de directorio está prohibido y no existe un index.
- errores con alias vs root: concatenación de rutas equivocada puede apuntar a un lugar al que no diste permisos.
Modos de fallo de Apache que parecen permisos
Apache tiene su propia marca de “403 por política”, incluso cuando el sistema de archivos está bien:
- Directivas Require: un estricto
Require all deniedheredado desde un contexto padre te 403a todo. - Options FollowSymLinks: los symlinks pueden ser rechazados, produciendo 403 aunque el destino sea legible.
- Overrides en .htaccess: falta de
AllowOverrideo reglas inesperadas pueden negar peticiones de formas que imitan problemas de acceso a archivos.
El truco es dejar de tratar “403” como un único problema. Es una salida. La entrada está en los logs.
ACLs: la alternativa adulta a dar escritura a todo el grupo
Los bits de permisos Unix son una autopista de tres carriles: owner, group, other. Las ACLs te dan callejones laterales:
“www-data puede leer este árbol, deploy puede escribir, CI puede leer y nadie más lo toca.” Eso es
exactamente lo que suelen necesitar los web roots.
Cuándo valen la pena las ACLs
- Tienes múltiples identidades de despliegue (usuario humano, runner de CI, automatización) y no quieres un grupo compartido masivo.
- Quieres que
www-datatenga traversía y lectura, pero nunca escritura, incluso si la propiedad de grupo cambia. - Quieres que los archivos nuevos obtengan automáticamente los permisos correctos sin depender de disciplina en el umask.
Dos reglas de ACL que hacen la mayor parte del trabajo
Establece una ACL de acceso (lo que aplica ahora) y una ACL por defecto (lo que heredan los nuevos archivos) en directorios.
Ejemplo: permite que www-data lea/atraviese el web root, y permite que deploy escriba, sin hacer que sea legible por todos.
cr0x@server:~$ sudo setfacl -R -m u:www-data:rx /var/www/example
cr0x@server:~$ sudo setfacl -R -m d:u:www-data:rx /var/www/example
cr0x@server:~$ getfacl -p /var/www/example | sed -n '1,20p'
# file: /var/www/example
# owner: deploy
# group: web
user::rwx
user:www-data:r-x
group::r-x
mask::r-x
other::---
default:user::rwx
default:user:www-data:r-x
default:group::r-x
default:mask::r-x
default:other::---
Significado: www-data obtiene lectura+traversía. Los nuevos directorios/archivos heredan una entrada ACL por defecto, así que nuevas releases no fallarán al azar con 403.
Decisión: Usa ACLs cuando necesites acceso multi-identidad estable sin aflojar permisos de “other”.
La trampa: la máscara de la ACL
Las ACLs vienen con una entrada mask que limita los permisos efectivos para usuarios/grupos nombrados. La gente olvida que existe la máscara,
luego se pregunta por qué su rwx cuidadosamente establecido no se aplica realmente.
cr0x@server:~$ sudo setfacl -m u:www-data:rwx /var/www/example/current/storage
cr0x@server:~$ getfacl -p /var/www/example/current/storage | grep -E 'www-data|mask'
user:www-data:rwx
mask::rwx
Significado: La máscara es permisiva. Si fuera r-x, los permisos efectivos se reducirían.
Decisión: Cuando las ACLs “no funcionan”, inspecciona la máscara antes de reescribir la mitad de tu política de sistema de archivos.
Despliegues: evita que CI/CD vuelva a romper permisos
Los permisos no son algo que arreglas una vez. Son algo que tu despliegue puede arruinar repetidamente,
con el entusiasmo de un niño en una habitación llena de rotuladores.
De dónde viene la deriva de permisos
- Nuevos directorios de release creados con diferente umask o contexto de usuario.
- Extracción de artefactos que preserva modos desde el build que no coinciden con lo que necesita producción.
- Builds de contenedores copiando archivos como root y dejándolos root-owned en volúmenes compartidos.
- Hotfixes hechos en el servidor como root a las 2 a.m. (ya sabes quién eres).
Normaliza permisos como paso de despliegue
Esto es aburrido y correcto. Añade un paso a tu pipeline de deploy que imponga propiedad/modos/ACLs
en el directorio de release antes de cambiar el symlink current.
cr0x@server:~$ sudo -u deploy sh -lc '
release=/var/www/example/releases/20251230T1005
find "$release" -type d -exec chmod 2755 {} \;
find "$release" -type f -exec chmod 0644 {} \;
'
Significado: Los directorios son traversables; los archivos son legibles. El bit setgid mantiene la herencia de grupo estable.
Decisión: Usa normalización para eliminar el “funciona en mi agente de build” de la ecuación de permisos.
Evita que artefactos propiedad de root lleguen al web root
Si despliegas con sudo, sé explícito sobre el contexto de usuario al escribir archivos.
“Lo corrí con sudo” es cómo terminas con un web root que solo root puede leer.
cr0x@server:~$ sudo -u deploy tar -xf /tmp/release.tar -C /var/www/example/releases/20251230T1005
cr0x@server:~$ stat -c "%U:%G %a %n" /var/www/example/releases/20251230T1005/public/index.html
deploy:web 644 /var/www/example/releases/20251230T1005/public/index.html
Significado: Los archivos son propiedad de la identidad de despliegue, no root. Eso es lo que quieres.
Decisión: Haz de “quién escribe archivos” un parámetro de despliegue de primera clase, no un accidente.
Tres microhistorias corporativas desde las trincheras de permisos
1) Incidente causado por una suposición equivocada: “Archivo legible significa sitio legible”
Una empresa mediana tenía una flota Debian con nginx delante de un generador de sitios estáticos. Un ingeniero
rotó el usuario de despliegue y apretó los permisos en /var/www en todo el árbol, buscando el mínimo privilegio.
Comprobaron un par de archivos. Todo era 0644. Lo desplegaron.
Cinco minutos después, el sitio devolvía 403. El de guardia inicialmente culpó la deriva de configuración de nginx porque el
archivo raíz claramente era legible por el mundo. El líder en el canal de incidentes dijo “no puede ser permisos,
el archivo es 644.”
El problema real vivía un nivel arriba: el directorio se volvió 750 propiedad del usuario deploy y un grupo privado.
Nginx no podía atravesarlo porque “execute en directorios” no estaba en su modelo mental.
El log de errores sí decía “permission denied”, pero la gente leyó lo que esperaba leer.
La solución fue pequeña—conceder traversía vía membresía de grupo y directorios setgid—pero la lección quedó:
las comprobaciones de permisos deben incluir toda la ruta. La acción post-incidente no fue “tener cuidado”.
Fue “usar namei en el runbook y probar como www-data.”
2) Optimización que salió mal: “Hagamos que el servidor web escriba el output del build”
Otro equipo quería despliegues más rápidos para una app PHP. Alguien propuso construir assets en el servidor
para evitar mover artefactos grandes. La forma más rápida era ejecutar el build como la misma identidad que sirve la app. “Si el build corre como www-data, siempre tendrá acceso.”
Funcionó. El tiempo de despliegue mejoró. Los gráficos se vieron mejor. Luego una dependencia fue comprometida en el ecosistema
(no culpa suya), y su proceso de aplicación tenía permisos de escritura en el árbol de código. Eso convirtió una
compromisión de solo lectura en una que se auto-modifica: las peticiones web podían dejar archivos en directorios servidos.
El equipo de respuesta contuvo rápido, pero fue una semana larga verificando qué cambió en disco.
La “optimización” borró un límite de seguridad. Un límite que había sido gratuito.
Revirtieron a construir fuera de producción, hicieron el web root de solo lectura para la identidad servidora, y
permitieron escrituras solo a rutas de uploads/cache. El tiempo de despliegue subió un poco; el riesgo bajó mucho.
Ese es el trade-off que tomas cada vez.
3) Práctica aburrida pero correcta que salvó el día: ACLs por defecto en padres de releases
Un equipo enterprise ejecutaba múltiples sitios bajo /srv/apps con layout al estilo Capistrano. Su CI creaba
un nuevo directorio de release por cada deploy. Con el tiempo tenían un problema de “a veces 403 después del deploy”.
No siempre. Lo suficiente como para ser molesto y erosionar la confianza en la automatización.
La causa raíz era la deriva de umask entre runners. Un runner creaba directorios como 0750, otro como 0755.
A veces Apache podía atravesar; a veces no. Los ingenieros intentaron “estandarizar” añadiendo un paso chmod,
pero se ejecutaba después del flip del symlink. Los usuarios veían una breve caída durante el deploy. Lo odiaban.
La solución no fue ingeniosa. Fue correcta. Pusieron una ACL por defecto en el directorio padre de releases otorgando al
usuario del servidor web rx, y aplicaron normalización de permisos antes del cambio de symlink. Los nuevos árboles de release heredaron acceso sano,
así que el despliegue dejó de depender del humor del runner.
Nada de esto fue emocionante. También hizo que la página “403 después del deploy” se extinguiera. A veces lo aburrido es una característica.
Errores comunes: síntoma → causa raíz → solución
1) Síntoma: 403 para un directorio, pero los archivos funcionan si se solicitan directamente
Causa raíz: Falta archivo index, listado de directorio deshabilitado, o el front controller del framework no es alcanzable.
Solución: Asegura que exista un index (index.html / index.php), verifica la directiva index de nginx, y verifica permisos en el objetivo de fallback en try_files.
2) Síntoma: 403 con “(13: Permission denied)” en logs de nginx/apache
Causa raíz: El sistema de archivos niega acceso. A menudo un directorio padre carece del bit execute (traversía) para www-data.
Solución: Usa namei -l en la ruta completa; concede traversía vía grupo, ACL o ajustes de modo en el directorio.
3) Síntoma: Todo es 644/755, sigue dando 403
Causa raíz: Denegación AppArmor en Ubuntu, o política de Apache negando acceso.
Solución: Revisa logs del kernel/audit por AppArmor DENIED; revisa directivas Require y Options de Apache. No sigas haciendo chmod.
4) Síntoma: Funciona hasta un deploy, luego 403
Causa raíz: Nuevo directorio de release creado con umask restrictivo o propiedad incorrecta; el symlink apunta a un árbol con permisos distintos.
Solución: Normaliza permisos antes de cambiar el symlink; aplica ACLs por defecto en el padre de releases; fuerza el contexto del usuario de despliegue.
5) Síntoma: Uploads fallan a menos que hagas chmod 777 a todo el web root
Causa raíz: La aplicación necesita escribir solo en uploads/cache, pero hiciste que todo el árbol fuera objetivo.
Solución: Haz que solo directorios específicos sean escribibles por www-data o concede ACLs de escritura allí. Mantén el código de solo lectura.
6) Síntoma: “Permission denied” al usar rutas symlinked
Causa raíz: Uno de los componentes del symlink o su ruta objetivo tiene traversía restringida; Apache además puede requerir opciones explícitas para symlinks.
Solución: Usa readlink -f y namei -l en la ruta resuelta; ajusta permisos de traversía; para Apache, permite symlinks apropiadamente.
7) Síntoma: Usuario de deploy puede escribir, servidor web no puede leer (o al revés)
Causa raíz: Excesiva dependencia de bits de owner; estrategia de grupo no implementada; falta setgid; faltan ACLs.
Solución: Elige un modelo (root de solo lectura + directorios escribibles explícitos, o grupo compartido + setgid, o ACLs) e implémentalo consistentemente.
Listas de verificación / plan paso a paso
Checklist: detener el 403 inmediato de forma segura
- Revisa el log de errores para el tipo de denegación (sistema de archivos vs configuración).
- Confirma la ruta en disco configurada (nginx
root/alias, ApacheDocumentRoot). - Ejecuta
namei -len la ruta exacta que falló. - Prueba como
www-dataconsudo -u www-data test -r(o-xpara directorios). - Si es un problema de traversía en un directorio padre, arregla el directorio más estrecho necesario (no todo el árbol).
- Recarga/reinicia el servidor web si hubo cambios en pertenencia a grupos.
- Vuelve a probar: prueba de lectura en línea de comandos, luego petición HTTP.
- Escanea por permisos world-writable no intencionales después del arreglo de emergencia.
Checklist: implementar una política de permisos a largo plazo
- Elige un modelo:
- Preferido: web root de solo lectura; solo subdirs escribibles.
- Aceptable: grupo compartido + setgid + umask aplicado.
- Mejor para organizaciones complejas: ACLs con entradas por defecto.
- Define rutas escribibles explícitas (uploads/cache/sessions/logs/sockets).
- Establece la propiedad de rutas escribibles (a menudo
www-data:www-data), mantiene el código propiedad de deploy/root. - Normaliza permisos durante el deploy antes de cambiar tráfico/symlink.
- Añade una comprobación en CI o post-deploy que ejecute pruebas de lectura como
www-data. - Documenta un comando del runbook:
namei -len la ruta que falla. - Incluye comprobaciones de AppArmor en runbooks de Ubuntu si aplica.
Plan paso a paso: una línea base limpia para un sitio típico
Esta es una línea base práctica que mantiene el código de solo lectura para el servidor web mientras permite uploads y cache.
Ajusta rutas para tu aplicación.
- Crea un grupo compartido para deploy + servidor web (opcional pero útil):
cr0x@server:~$ sudo groupadd -f web cr0x@server:~$ sudo usermod -aG web deploy cr0x@server:~$ sudo usermod -aG web www-data - Establece propiedad de grupo y setgid en el árbol del sitio:
cr0x@server:~$ sudo chgrp -R web /var/www/example cr0x@server:~$ sudo find /var/www/example -type d -exec chmod 2755 {} \; cr0x@server:~$ sudo find /var/www/example -type f -exec chmod 0644 {} \; - Marca directorios escribibles y concede escritura solo allí:
cr0x@server:~$ sudo install -d -o www-data -g www-data -m 0750 /var/www/example/current/public/uploads cr0x@server:~$ sudo install -d -o www-data -g www-data -m 0750 /var/www/example/current/var/cache - Reinicia si se cambió la pertenencia a grupos (reiniciar es más seguro que recargar para esto):
cr0x@server:~$ sudo systemctl restart nginx - Valida con una prueba de lectura como www-data:
cr0x@server:~$ sudo -u www-data test -r /var/www/example/current/public/index.html; echo $? 0
Preguntas frecuentes
1) ¿Por qué un bit execute faltante en un directorio causa 403 aunque el archivo sea 644?
Porque “execute” en un directorio significa “traversía/búsqueda”. Sin él, no puedes acceder a las entradas internas,
aunque conozcas el nombre de archivo. El kernel bloquea la traversía antes de evaluar los bits del archivo.
2) ¿Debería mi web root ser propiedad de www-data?
Normalmente no. Si www-data posee el código, una compromisión web suele convertirse en una compromisión persistente.
Prefiere código propiedad de deploy/root y legible por www-data; haz que solo los directorios de runtime sean escribibles.
3) ¿Qué permisos debería tener un web root estático típico?
Línea base común: directorios 0755, archivos 0644, propiedad de un usuario de despliegue, legible por el servidor web (vía grupo compartido o ACL).
Ajusta “other” si tienes una razón, pero mantén traversía para el servidor web.
4) ¿Usar un grupo compartido (deploy + www-data) es inseguro?
Puede estar bien si mantienes el acceso del servidor web como solo lectura y restringes escrituras a directorios específicos.
Se vuelve riesgoso cuando se habilita group-write de manera amplia y el proceso web puede escribir en rutas de código.
5) ¿Por qué agregar www-data a un grupo no arregló el problema hasta reiniciar?
Los procesos no actualizan mágicamente los grupos suplementarios en vuelo. Los procesos worker mantienen la lista de grupos desde cuando se iniciaron.
Recargar puede no reemplazar workers de forma que recojan grupos; reiniciar es determinista.
6) ¿Cuándo debo usar ACLs en lugar de chmod/chgrp?
Usa ACLs cuando tienes múltiples identidades que necesitan distinto acceso, o cuando quieres permisos heredados
sin depender de umask y setgid. Las ACLs también son útiles cuando quieres evitar abrir permisos a “other”.
7) ¿Por qué veo 403 solo en algunos archivos después del deploy?
Normalmente modos inconsistentes de creación: diferente umask, usuario distinto, o artefactos extraídos preservando modos restrictivos.
La solución es normalización de permisos en el proceso de despliegue y/o ACLs por defecto en el padre de releases.
8) ¿Cuál es la solución rápida más segura bajo presión?
Concede la mínima traversía/lectura necesaria a la identidad del servidor web. Empieza por el primer directorio padre que falla mostrado por
namei -l. Evita chmod -R en todo el árbol; es como crear nuevos problemas mientras arreglas el antiguo.
9) ¿Por qué veo 403 pero los logs no muestran nada?
Puede que estés mirando el log equivocado (vhost incorrecto, contenedor equivocado), o el nivel de logs es demasiado bajo.
También comprueba que la petición llegue realmente al servidor que estás tail-eando (los balanceadores de carga hacen bromas prácticas).
10) ¿Debian/Ubuntu usa SELinux aquí?
Por defecto, no—AppArmor es más común en Ubuntu. Si instalaste SELinux, entonces entra en juego, pero no asumas que está presente.
Verifica con logs y módulos habilitados en lugar de aplicar soluciones copiadas de otra distro.
Conclusión: próximos pasos prácticos
Deja de tratar los 403 como una falla moral. Son un desacuerdo del modelo de permisos, y Linux es extremadamente consistente una vez que haces la pregunta correcta.
La pregunta correcta es “¿puede www-data atravesar cada directorio en la ruta y leer el objetivo?” No “¿hice chmod recientemente?”
Próximos pasos que puedes hacer hoy:
- Añade
namei -ly “probar como www-data” a tu runbook de on-call. - Elige uno de los modelos de permisos e implémentalo consistentemente: web root de solo lectura con directorios escribibles explícitos es la elección adulta por defecto.
- Normaliza permisos como parte de los despliegues, antes de cambiar tráfico o symlinks.
- Si usas Ubuntu, incluye comprobaciones de denegaciones AppArmor para no perder una hora chmod-eando en el vacío.
Mantén tu web root legible, tus directorios escribibles intencionales y tu dedo lejos de la tecla 7 a menos que estés escribiendo una historia aleccionadora.