Permisos del web root en Debian/Ubuntu: Evita 403 sin 777 (Caso #69)

¿Te fue útil?

Algunos fallos no gritan. Susurran. Un discreto “403 Forbidden” en tu navegador, un ticket de soporte con una captura y un desarrollador jurando que “no tocó nada”. Mientras tanto, el servidor web está en la puerta con una tabla, negando la entrada porque olvidaste un único bit de ejecución en un directorio padre.

Si tu reflejo para arreglarlo es chmod -R 777, quita las manos del teclado. Podemos resolver 403 en Debian/Ubuntu de forma limpia, predecible y sin convertir tu web root en un jardín comunitario.

Guía rápida de diagnóstico

Cuando tienes un 403, no estás “depurando permisos”. Estás localizando el primer punto donde el usuario del proceso del servidor web no puede atravesar o leer. Hazlo en este orden y dejarás de dar palos de ciego.

1) Confirma cuál es realmente el usuario del proceso del servidor web

No asumas www-data. Los valores por defecto de Debian/Ubuntu lo usan a menudo, pero contenedores, endurecimiento o overrides de systemd pueden cambiarlo.

2) Identifica la ruta exacta que se está sirviendo

¿Es el DocumentRoot esperado? ¿Un symlink? ¿Un alias? ¿Un root por vhost? ¿Un “útil” redireccionamiento al home de alguien?

3) Lee la línea del log de errores que corresponde a tu petición

El 403 tiene varias variantes: permisos del sistema de ficheros, listado de directorio deshabilitado, autenticación requerida, o módulos de política (AppArmor/SELinux). El log te dice cuál compraste.

4) Prueba el acceso como el usuario del servidor web, desde el sistema de ficheros

Si el proceso no puede atravesar los directorios padres, no importa que el fichero en sí mismo sea legible.

5) Solo entonces cambia permisos—y cambia lo mínimo necesario

Prefiere acceso basado en grupo o ACLs. Usa el bit de ejecución correctamente. Evita cualquier cosa world-writable en un web root a menos que disfrutes las retrospectivas de incidentes.

El modelo de permisos que realmente provoca 403

403 no es un único bug; es una categoría

En Apache y Nginx, “403 Forbidden” es el servidor diciendo al cliente: “Entendí la petición, pero no la voy a servir”. Eso puede deberse a:

  • Acceso al sistema de ficheros denegado (lo más común).
  • El indexado de directorios está deshabilitado y no hay archivo index.
  • Reglas de acceso que deniegan la petición (Apache Require, Nginx deny).
  • Fallo de autenticación/autorización (a veces 401, a veces 403 según la configuración).
  • Control de acceso obligatorio lo bloqueó (AppArmor o SELinux).

Este artículo trata el ángulo de permisos: los bits clásicos de Unix, propiedad, grupos, umask y ACLs, además de cómo se intersectan con las elecciones de empaquetado de Debian/Ubuntu.

La gran trampa: “execute” en directorios significa traverse

En directorios:

  • read (r) te permite listar nombres.
  • write (w) te permite crear/borrar/renombrar entradas.
  • execute (x) te permite atravesar el directorio y acceder a inodos dentro de él, si ya conoces el nombre.

La caída clásica se ve así: el archivo tiene 644 y parece legible, pero un directorio padre es 750 y pertenece a otro grupo, y el servidor web no puede atravesarlo. El navegador ve 403. El humano ve “pero el archivo es 644”. Ambos tienen razón técnicamente, y por eso esto sigue ocurriendo.

Los symlinks no son portales mágicos

Los symlinks son solo punteros. El servidor necesita permisos en la ruta objetivo, incluidos todos los directorios padres. Además, Apache puede configurarse para negarse a seguir symlinks a menos que esté explícitamente permitido.

Por qué 777 “funciona” y por qué es una trampa

777 da a todos lectura/escritura/ejecución. Para directorios, eso significa que cualquier usuario local (o servicio comprometido) puede dejar o reemplazar ficheros en tu web root. Si tu servidor web ejecuta scripts (PHP, CGI, etc.), efectivamente instalaste un cartel de “ejecuta código arbitrario aquí”.

Primer chiste corto: chmod 777 es como dejar las llaves de casa bajo el felpudo—excepto que el felpudo está en la plaza del pueblo.

Una máxima fiable de operaciones

Parafraseando una idea de W. Edwards Deming: La mayoría de los problemas vienen del sistema, no del esfuerzo individual. Los fallos por permisos suelen no ser porque alguien sea “descuidado”. Son porque tu ruta de despliegue, modelo de propiedad y valores por defecto no están diseñados.

Hechos y contexto interesantes (por qué sigue ocurriendo)

  1. Los permisos Unix son anteriores a la web por décadas. El modelo se construyó para sistemas de tiempo compartido multiusuario, no para servir ficheros estáticos al planeta.
  2. El bit de ejecución en directorios es más viejo que la carrera de la mayoría. Es “search” o “traverse”, y olvidarlo es un rito de iniciación que nadie pidió.
  3. Debian popularizó usuarios de servicio como www-data como convención de empaquetado. La idea fue mantener los daemons sin privilegios y consistentes entre instalaciones.
  4. La historia temprana de seguridad de Apache influyó en los valores por defecto de hoy. Características como Options FollowSymLinks y Require all granted evolucionaron porque “servir cualquier cosa legible” era un plan malo.
  5. Las ACL POSIX no siempre fueron comunes. Se volvieron mainstream en distribuciones Linux cuando los equipos necesitaron compartición más granular que la que un único grupo podía ofrecer.
  6. Umask es un motor de política silencioso. No es un permiso; es la sustracción por defecto aplicada al crear ficheros. Puede deshacer tu plan cuidadoso mientras duermes.
  7. Muchos incidentes de “permisos” son en realidad denegaciones de módulos de política. AppArmor (Ubuntu) y SELinux (en algunas configuraciones Debian, más común en otros sitios) pueden bloquear el acceso aun cuando los bits de modo parezcan correctos.
  8. Las pilas web adoptaron “mínimos privilegios” lentamente. Ejecutar servidores como root solía ser normal; ahora es una bandera roja. El modelo de permisos en tu despliegue debería coincidir con esa realidad.

Tareas prácticas: comandos, salidas y decisiones

A continuación hay comprobaciones prácticas que puedes ejecutar en Debian/Ubuntu. Cada tarea incluye: comando, qué significa la salida y la decisión que tomas a partir de ello. Estos son los movimientos que detienen 403 sin abrir un cráter de seguridad.

Task 1: Confirmar qué servidor web está ejecutándose y su usuario de proceso

cr0x@server:~$ ps -eo user,comm,args | egrep 'apache2|nginx' | head
root     nginx: master process /usr/sbin/nginx -g daemon on; master_process on;
www-data nginx: worker process
root     /usr/sbin/apache2 -k start
www-data /usr/sbin/apache2 -k start

Qué significa: El proceso maestro puede ser root; los workers deberían ser sin privilegios (a menudo www-data). Ese usuario sin privilegios debe leer/atravesar el web root.

Decisión: Usa el usuario de los workers (www-data aquí) para todas las pruebas de acceso y para diseñar la propiedad/grupo.

Task 2: Identificar el DocumentRoot o la ruta del web root que realmente se usa

cr0x@server:~$ apache2ctl -S 2>/dev/null | sed -n '1,25p'
VirtualHost configuration:
*:80                   example.local (/etc/apache2/sites-enabled/000-default.conf:1)
ServerRoot: "/etc/apache2"
Main DocumentRoot: "/var/www/html"

Qué significa: La configuración efectiva de vhost de Apache apunta a un DocumentRoot específico. Si estás editando otro directorio, estás depurando lo equivocado.

Decisión: Asegúrate de que tus permisos funcionen en la ruta que Apache/Nginx realmente sirve, no en la que desearías que sirviera.

Task 3: Para Nginx, listar bloques de servidor y roots

cr0x@server:~$ sudo nginx -T 2>/dev/null | egrep -n 'server_name|root ' | head -n 12
34:    server_name example.local;
41:    root /srv/www/example/current/public;
78:    server_name static.example.local;
82:    root /srv/www/static;

Qué significa: Nginx puede tener múltiples roots. Un 403 en un hostname puede ser un problema de permisos en un árbol de directorios distinto.

Decisión: Apunta al root correcto para el vhost que falla.

Task 4: Leer el log de errores para la denegación correspondiente

cr0x@server:~$ sudo tail -n 20 /var/log/nginx/error.log
2025/12/30 10:31:42 [error] 19214#19214: *55 open() "/srv/www/example/current/public/index.html" failed (13: Permission denied), client: 10.10.10.23, server: example.local, request: "GET / HTTP/1.1", host: "example.local"

Qué significa: (13: Permission denied) es permisos del sistema de ficheros (o política MAC). Esto no es una directiva Nginx deny ni falta de archivo index.

Decisión: Procede a comprobaciones a nivel de sistema de ficheros (bits de modo, propiedad, ACL, AppArmor/SELinux).

Task 5: Comprobar permisos a lo largo de toda la ruta (“namei” es tu amigo)

cr0x@server:~$ namei -l /srv/www/example/current/public/index.html
f: /srv/www/example/current/public/index.html
drwxr-xr-x root     root     /
drwxr-xr-x root     root     srv
drwxr-x--- deploy   deploy   www
drwxr-xr-x deploy   deploy   example
drwxr-xr-x deploy   deploy   current
drwxr-xr-x deploy   deploy   public
-rw-r--r-- deploy   deploy   index.html

Qué significa: El problema es visible: /srv/www es drwxr-x--- propiedad de deploy:deploy. El servidor web no puede atravesarlo porque “other” no tiene execute, y no está en el grupo.

Decisión: Arregla los permisos de traverse en los directorios padres, idealmente vía membresía de grupo o ACL—no abriendo todo al mundo.

Task 6: Probar acceso como el usuario del servidor web (la prueba más honesta)

cr0x@server:~$ sudo -u www-data -s -- bash -lc 'test -r /srv/www/example/current/public/index.html && echo READ_OK || echo READ_NO; test -x /srv/www && echo TRAVERSE_OK || echo TRAVERSE_NO'
READ_NO
TRAVERSE_NO

Qué significa: El usuario worker no puede atravesar /srv/www, por lo que no puede leer nada debajo.

Decisión: Elige una estrategia de permisos (grupo, ACL o propiedad dedicada del web-root) e implémentala consistentemente.

Task 7: Comprobar propiedad y grupos, incluyendo si www-data está en el grupo esperado

cr0x@server:~$ id www-data
uid=33(www-data) gid=33(www-data) groups=33(www-data)

Qué significa: www-data no tiene grupos extra. Si planeas usar un grupo compartido como web o deploy, debes añadirlo y recargar servicios si es necesario.

Decisión: Decide si añadir www-data a un grupo (común) o usar ACLs (más limpio cuando múltiples equipos comparten rutas).

Task 8: Inspeccionar los bits de modo del directorio y detectar bits de ejecución faltantes

cr0x@server:~$ stat -c '%A %U:%G %n' /srv/www /srv/www/example /srv/www/example/current/public
drwxr-x--- deploy:deploy /srv/www
drwxr-xr-x deploy:deploy /srv/www/example
drwxr-xr-x deploy:deploy /srv/www/example/current/public

Qué significa: El directorio de nivel superior es el cuello de botella. Tu servidor web no necesita escribir allí, pero sí necesita execute (traverse) y lectura en los ficheros que sirve.

Decisión: Ajusta lo mínimo requerido: a menudo 751 o basado en grupo 750 dependiendo de tu modelo.

Task 9: Comprobar si hay ACLs en juego (y si hay defaults establecidos)

cr0x@server:~$ getfacl -p /srv/www/example/current/public | sed -n '1,30p'
# file: /srv/www/example/current/public
# owner: deploy
# group: deploy
user::rwx
group::r-x
other::r-x

Qué significa: No hay entradas ACL especiales; solo aplican los bits clásicos. Si esperabas una ACL que otorgue a www-data, no está presente.

Decisión: Si usas la estrategia ACL, aplícala en el nivel de directorio correcto y establece ACLs por defecto para que los ficheros nuevos hereden el acceso.

Task 10: Detectar “sabotaje de umask” en el entorno del usuario de despliegue

cr0x@server:~$ sudo -u deploy -s -- bash -lc 'umask; touch /tmp/umask-test-file; stat -c "%A %n" /tmp/umask-test-file; rm -f /tmp/umask-test-file'
0027
-rw-r----- /tmp/umask-test-file

Qué significa: Un umask 0027 crea ficheros que no son legibles por el mundo y no son escribibles por el grupo. Si tu servidor web depende de la legibilidad por “other”, tendrás 403 intermitentes dependiendo de cómo se crearon los ficheros.

Decisión: Alinea el umask con tu estrategia de permisos. Para compartir por grupo: a menudo 0002 es razonable. Para configuraciones más cerradas: mantén umask restrictivo pero usa ACLs.

Task 11: Comprobar por denegación en la configuración de Apache que parezca un problema de permisos

cr0x@server:~$ sudo apache2ctl -t -D DUMP_RUN_CFG 2>/dev/null | egrep -n 'User|Group|ServerRoot|DocumentRoot' | head
3:User: name="www-data" id=33
4:Group: name="www-data" id=33
16:ServerRoot: "/etc/apache2"
22:DocumentRoot: "/var/www/html"

Qué significa: Puedes ver el usuario/grupo en tiempo de ejecución y el DocumentRoot. Si tu vhost apunta a otro sitio, confirma con apache2ctl -S. Si el DocumentRoot es correcto pero sirves desde /home vía un alias, te has creado un rompecabezas de permisos a propósito.

Decisión: Si la configuración y el sistema de ficheros discrepan, corrige primero la configuración. Los permisos deben coincidir con la intención, no con accidentes.

Task 12: Para Nginx, comprobar si el usuario worker se ha sobrescrito en la configuración

cr0x@server:~$ egrep -n '^\s*user\s+' /etc/nginx/nginx.conf
2:user www-data;

Qué significa: Confirma qué usuario lee los ficheros. Si no es www-data, ajusta tus pruebas y el plan de permisos en consecuencia.

Decisión: Mantén estable el usuario worker. Cambiarlo para “arreglar permisos” suele ser un olor a problema.

Task 13: Comprobar si el sistema de ficheros está montado con opciones que puedan bloquear el comportamiento esperado

cr0x@server:~$ findmnt -no TARGET,FSTYPE,OPTIONS /srv
/srv ext4 rw,relatime

Qué significa: Para contenido estático, las opciones de montaje rara vez causan 403, pero flags como noexec importan si sirves CGI ejecutables o ejecutas artefactos de build en sitio.

Decisión: Si dependes de ejecución en ese montaje, elimina noexec (con cuidado) o cambia la arquitectura (mejor: no ejecutar desde el web root en absoluto).

Task 14: Comprobar el estado de AppArmor y si probablemente está involucrado

cr0x@server:~$ sudo aa-status | sed -n '1,18p'
apparmor module is loaded.
24 profiles are loaded.
12 profiles are in enforce mode.
   /usr/sbin/nginx
   /usr/sbin/apache2

Qué significa: Si el perfil está en modo enforce, puede denegar acceso a rutas fuera de las ubicaciones esperadas aun con permisos Unix correctos. Los logs mencionarán denegaciones de AppArmor.

Decisión: Si tu DocumentRoot es poco convencional (como /srv/www), verifica que el perfil de AppArmor lo permita, o mueve el root a una ruta estándar.

Task 15: Confirmar los permisos efectivos reales de un archivo incluyendo máscaras ACL

cr0x@server:~$ getfacl -p /srv/www/example/current/public/index.html
# file: /srv/www/example/current/public/index.html
# owner: deploy
# group: deploy
user::rw-
group::r--
other::r--

Qué significa: Este archivo es legible por el mundo, pero eso no ayuda si un directorio padre bloquea el traverse. Además, si añades ACLs después, la entrada mask puede limitar silenciosamente los derechos efectivos.

Decisión: Arregla primero los permisos de la ruta; luego asegura que los ficheros sean legibles por el principal previsto (grupo o usuario específico vía ACL).

Task 16: Validar que tu “arreglo” no introdujo directorios world-writable

cr0x@server:~$ sudo find /srv/www/example/current/public -type d -perm -0002 -maxdepth 3 -print | head

Qué significa: Sin salida significa que no se encontraron directorios world-writable (al menos dentro de maxdepth). Si aparece salida, listaría directorios que cualquiera puede escribir.

Decisión: Si algo aparece, quita el permiso de escritura para el mundo y cambia a escritura por grupo o a un path de subida controlado.

Tres pequeñas historias corporativas desde las trincheras

1) Incidente causado por una suposición errónea: “www-data puede leer /home, ¿verdad?”

En una empresa mediana, un equipo montó un dashboard interno rápido. Lo querían “temporal”, lo que significó que vivía en el home de un desarrollador porque era rápido. Nginx apuntaba a /home/alex/dashboard/public. Funcionó en staging, funcionó en una caja de producción, y luego dejó de funcionar.

La falla fue clásica: 403 solo en un nodo detrás del balanceador. El ingeniero de guardia comparó configuraciones Nginx, vio que eran idénticas y empezó a sospechar caching, luego DNS, luego “tal vez el LB hace algo”. El log de errores tenía Permission denied, pero se descartó porque “los ficheros son 644”.

La diferencia fue un directorio padre: en un nodo, /home/alex era 750. En otro, era 755. El dashboard “funcionó” solo donde los home eran traversables por el mundo. Seguridad había endurecido recientemente los permisos por defecto de home vía /etc/adduser.conf, y nadie relacionó ese cambio con una app web viviendo en el home de una persona.

La solución no fue aflojar la seguridad de los home. La solución fue mover el dashboard a /srv/www con un grupo apropiado y propiedad predecible, y luego actualizar AppArmor para la nueva ruta. Tras eso, el 403 desapareció y no volvió.

La lección real: nunca ancles raíces web de producción en rutas diseñadas para humanos. Los humanos inician sesión. Los servicios no deberían depender de la política de home de humanos.

2) Optimización que salió mal: “Ajustémoslo todo a 750”

Otra organización tuvo una auditoría de seguridad que puso a todos nerviosos. Alguien propuso “apretar permisos” en web roots poniendo directorios a 750 y ficheros a 640, propiedad de deploy:deploy. La idea era sensata en abstracto: no dejar que “other” lea código.

Lo desplegaron con un chmod recursivo en un paso de despliegue. Pasó en un entorno porque su sistema de deploy ejecutaba Nginx como deploy (no recomendado, pero enmascaró el problema). En producción, Nginx corría como www-data, y el paso de deploy removió puntualmente el bit de traverse para “other” en un directorio padre. La siguiente petición devolvió 403.

Entonces llegó la solución de pánico: chmod -R 755 para restaurar el servicio. Ese cambio sí devolvió la web, pero también hizo todo el árbol traversable por el mundo, deshaciendo el objetivo de la auditoría, y creó un delta desordenado entre nodos porque no todas las cajas se arreglaron igual.

La solución sostenible fue implementar un modelo de grupo compartido con setgid en directorios, y hacer que la herramienta de deploy cree artefactos con permisos legibles por grupo. Acabaron con ajustes estilo 2750/2640 en lugares, pero siempre con el grupo correcto y herencia predecible.

La lección: “apretarlo todo” no es un plan. Los modelos de permisos son planes. Un chmod recursivo es una aplanadora en una habitación llena de cristal.

3) Práctica aburrida pero correcta que salvó el día: “namei en el runbook”

Un equipo de pagos corría múltiples instancias de Nginx sirviendo assets estáticos de checkout. Nada emocionante. Luego, un deploy introdujo una nueva capa de directorio: /srv/www/payments/releases/2025-12-30/public. La app servía bien en la mayoría de nodos, pero uno devolvía 403. El despliegue parecía idéntico en todos lados, pero un nodo fallaba justo antes del pico de tráfico.

Esto fue aburrido —en el buen sentido— porque el equipo tenía un paso en el runbook: “Ejecuta namei -l en la ruta exacta.” Sin debate, sin adivinar. Lo ejecutaron y vieron al instante que /srv/www/payments tenía el grupo equivocado en ese nodo, probablemente por un hotfix manual meses antes.

Corrigieron la propiedad de grupo, aseguraron que el bit setgid estuviera presente, y volvieron a ejecutar su job de “chequeo de deriva de permisos” que comparaba modos críticos entre la flota. Sin chmod recursivo, sin 777 de pánico, sin reinventar el usuario del servidor web.

Más tarde, en la review post-incidente, la acción más valiosa no fue una herramienta. Fue social: mantuvieron el runbook corto, obligatorio y específico. Los procedimientos aburridos ganan porque quitan la tentación de improvisar a las 2 a.m.

Errores comunes: síntomas → causa raíz → arreglo

Esto es lo que veo repetidamente en flotas Debian/Ubuntu. Los síntomas son engañosamente similares; los arreglos no lo son.

1) Síntoma: 403, el log de errores dice “Permission denied (13)”

Causa raíz: falta del bit de ejecución en un directorio padre (no hay traverse).

Arreglo: asegúrate de que cada directorio en la ruta sea traversable por el principal del servidor web (grupo o ACL). Usa namei -l para encontrar el primer bloqueador.

2) Síntoma: Ficheros son 644, directorios 755, aun así 403

Causa raíz: denegación por AppArmor/SELinux o configuración de Apache que deniega acceso.

Arreglo: revisa logs de auditoría del kernel / denegaciones de AppArmor y reglas Require de Apache. No uses chmod para evitar un motor de políticas.

3) Síntoma: 403 solo después del deploy; se arregla reiniciando y luego vuelve

Causa raíz: el deploy creó nuevos directorios/ficheros con umask restrictivo o grupo equivocado; el contenido viejo aún tenía permisos correctos.

Arreglo: impón umask en el entorno de despliegue, coloca setgid en directorios, o usa ACLs por defecto para que el contenido nuevo herede acceso.

4) Síntoma: 403 en una ruta symlink; la ruta directa funciona

Causa raíz: Apache no permite seguir symlinks, o los permisos en la ruta objetivo difieren.

Arreglo: asegúrate de que la config permita symlinks donde sea intencionado (Options FollowSymLinks o alternativas más seguras) y que la ruta objetivo sea legible/traversable.

5) Síntoma: 403 en la URL de un directorio, las URLs de archivos funcionan

Causa raíz: falta archivo index y autoindex deshabilitado, o falta bit de ejecución en el directorio.

Arreglo: añade un archivo index, habilita indexado intencionalmente (rara vez), o arregla permisos de directorio.

6) Síntoma: 403 solo en un nodo del clúster

Causa raíz: deriva de permisos (hotfix manual), umask distinto, membresía de grupo distinta, o versión de política MAC distinta.

Arreglo: compara salidas de stat/getfacl entre nodos, y audita membresía de grupos y estado de enforcement de políticas.

7) Síntoma: Todo funciona hasta que añades a un nuevo miembro del equipo

Causa raíz: permisos dependen de que un usuario específico sea propietario del árbol; no hay estrategia de grupo/ACL compartida.

Arreglo: pasa a propiedad por grupo y directorios setgid, o a ACLs. Evita “propiedad por quien desplegó último”.

8) Síntoma: Lo “arreglaste” con 777 y ahora seguridad llama

Causa raíz: falta de modelo de permisos; la solución de emergencia se convirtió en política.

Arreglo: revierte permisos world-writable, implementa estrategia de grupo/ACL, y separa directorios escribibles (uploads/cache) del código/activos de solo lectura.

Listas de verificación / plan paso a paso

Plan A: Arreglar un 403 en vivo de forma segura (cambio mínimo, certeza máxima)

  1. Captura la URL fallida exacta y mapea a una ruta de sistema de ficheros (desde la config del vhost y los logs).
  2. Lee la línea del log de errores para esa petición; confirma si es (13: Permission denied) o una denegación por config.
  3. Ejecuta namei -l sobre la ruta exacta y identifica el primer directorio que carece de traverse para el usuario del servidor web.
  4. Ejecuta una prueba de acceso como el usuario del servidor web con sudo -u www-data para confirmar el modo de fallo.
  5. Aplica el menor cambio de permisos necesario en el directorio bloqueador (prefiere execute de grupo o execute por ACL).
  6. Vuelve a probar acceso como el usuario del servidor web. Luego prueba vía HTTP.
  7. Escanea por directorios world-writable accidentales y revierte cualquiera que encuentres.
  8. Anota qué cambiaste y por qué; si no puedes explicarlo, no lo arreglaste—tuviste suerte.

Plan B: Estandarizar un web root para un equipo (modelo de grupo)

  1. Crea un grupo dedicado (web) para “puede leer contenido web”.
  2. Añade www-data y usuarios deploy a web.
  3. Establece la propiedad del web root a deploy:web (o root:web para control de cambios más estricto).
  4. Configura modos de directorio a 2775 y modos de ficheros a 664 donde corresponda.
  5. Establece umask de deploy a 0002 para que la legibilidad por grupo sea consistente.
  6. Separa directorios escribibles (uploads, cache) con controles más estrictos y configuración explícita de la app.
  7. Añade una comprobación de deriva: un job periódico o regla de gestión de configuración para verificar propiedad/modos.

Plan C: Estandarizar con ACLs (cuando múltiples grupos son un dolor)

  1. Mantén la propiedad con el usuario deploy/app; no luches contra tu organigrama en /etc/group.
  2. Aplica una ACL que dé a www-data rx en directorios y r en ficheros.
  3. Aplica ACLs por defecto en directorios para que los ficheros nuevos hereden la regla.
  4. Verifica que la máscara ACL no reduzca accidentalmente los permisos efectivos.
  5. Documenta la decisión de usar ACLs en tu runbook para que nadie la “limpie” más tarde.

Ejemplos de implementación concreta (usa uno, no todos)

Ejemplo 1: Modelo de grupo compartido bajo /srv/www/example

cr0x@server:~$ sudo groupadd -f web
cr0x@server:~$ sudo usermod -aG web www-data
cr0x@server:~$ sudo usermod -aG web deploy
cr0x@server:~$ sudo chown -R deploy:web /srv/www/example
cr0x@server:~$ sudo find /srv/www/example -type d -exec chmod 2775 {} +
cr0x@server:~$ sudo find /srv/www/example -type f -exec chmod 0664 {} +

Qué indica la salida: Estos comandos suelen ser silenciosos si tienen éxito. El cambio efectivo es que los directorios ahora heredan el grupo web y son traversables por el grupo.

Decisión: Si usas este modelo, asegúrate de que los procesos de deploy creen ficheros legibles por el grupo; de lo contrario seguirás “arreglando” permisos después de cada deploy.

Ejemplo 2: Modelo ACL para dar a www-data lectura/traverse sin cambiar grupos

cr0x@server:~$ sudo setfacl -R -m u:www-data:rx /srv/www/example/current/public
cr0x@server:~$ sudo find /srv/www/example/current/public -type f -exec setfacl -m u:www-data:r {} +
cr0x@server:~$ sudo setfacl -m d:u:www-data:rx /srv/www/example/current/public
cr0x@server:~$ sudo getfacl -p /srv/www/example/current/public | sed -n '1,25p'
# file: /srv/www/example/current/public
# owner: deploy
# group: deploy
user::rwx
user:www-data:r-x
group::r-x
mask::r-x
other::r-x
default:user::rwx
default:user:www-data:r-x
default:group::r-x
default:mask::r-x
default:other::r-x

Qué significa: La ACL otorga a www-data traverse/lectura. La ACL por defecto asegura que los nuevos directorios hereden la misma regla.

Decisión: Usa ACLs cuando la propiedad por grupos es políticamente u operativamente inestable. Pero comprométete con ello; los modelos mixtos se vuelven raros rápidamente.

Ejemplo 3: Arreglar solo el bit de traverse del directorio bloqueador (arreglo quirúrgico en vivo)

cr0x@server:~$ sudo chmod o+x /srv/www
cr0x@server:~$ namei -l /srv/www/example/current/public/index.html | sed -n '1,6p'
f: /srv/www/example/current/public/index.html
drwxr-xr-x root     root     /
drwxr-xr-x root     root     srv
drwxr-x--x deploy   deploy   www
drwxr-xr-x deploy   deploy   example

Qué significa: o+x en el directorio permite traverse pero no listado. Eso puede ser un compromiso razonable si no quieres que el mundo enumere el contenido del directorio.

Decisión: Esto está bien para restauración rápida, pero considera migrar a un modelo de grupo/ACL para la cordura a largo plazo.

Preguntas frecuentes

1) ¿Por qué un bit de ejecución faltante en un directorio causa 403?

Porque sin execute en un directorio, el proceso no puede atravesarlo—aun cuando el archivo dentro sea legible. El servidor web no puede alcanzar el inodo, por eso falla con permission denied.

2) ¿Debo hacer que el web root pertenezca a www-data?

Normalmente no. Si el servidor web puede escribir su propio contenido servido, una brecha se transforma en modificación persistente. Prefiere un usuario deploy que posea el contenido, con el servidor web en solo lectura vía grupo o ACL. Haz que solo directorios específicos de subidas/cache sean escribibles y mantenlos fuera de rutas ejecutables.

3) ¿Es chmod 755 en directorios siempre seguro?

Es común y a menudo aceptable para contenido estático público. Pero concede traverse y lectura al mundo. En sistemas corporativos o multi-tenant, puedes preferir acceso por grupo o ACLs, especialmente para código fuente, plantillas o configuraciones que no deberían ser legibles por todos.

4) ¿Qué es mejor: grupos o ACLs?

Los grupos son más simples y visibles. Las ACLs son más precisas y evitan meter a todos en un solo grupo. Si tienes un equipo y una pipeline de deploy, los grupos están bien. Si tienes múltiples equipos, runners CI y infraestructura compartida, las ACLs mantienen la paz.

5) ¿Por qué aparecen 403 solo después de despliegues?

Los ficheros nuevos heredan permisos del proceso que los crea: umask, ACLs por defecto y el bit setgid del directorio padre importan. Los ficheros antiguos pueden seguir accesibles, así que el problema parece intermitente. Arregla la política de creación, no solo los ficheros actuales.

6) ¿Cómo compruebo si es AppArmor en lugar de permisos Unix?

Empieza por el log de errores: si dice permission denied pero los bits parecen correctos, revisa el estado y logs de AppArmor. En Ubuntu, AppArmor suele estar en enforcing para servidores web. Una denegación será registrada por los componentes de auditoría del sistema y referenciada por el perfil.

7) ¿Por qué ayuda “other execute pero no read” en directorios?

o+x permite traverse sin permitir el listado del directorio. Eso significa que alguien que no conoce los nombres no puede enumerarlos fácilmente. No sustituye un control de acceso adecuado, pero puede reducir la divulgación casual de la estructura de directorios.

8) ¿Necesito reiniciar Apache/Nginx después de cambiar permisos del sistema de ficheros?

No para bits de modo y cambios de propiedad; el kernel los aplica inmediatamente. Puede que necesites reiniciar si cambiaste la membresía de grupos del usuario en ejecución—los procesos no recogen automáticamente grupos suplementarios nuevos.

9) ¿Por qué añadir www-data a un grupo no funciona de inmediato?

Porque los procesos worker existentes tienen su lista de grupos establecida al iniciar. Tras cambiar la membresía de grupos, recarga/reinicia el servicio para que los nuevos workers hereden la lista actualizada.

10) ¿Cuál es la disposición más segura para rutas escribibles como uploads?

Coloca directorios escribibles fuera del árbol de código/activos estáticos, da a la aplicación exactamente los permisos de escritura que necesita, y asegúrate de que el servidor web no ejecute contenido desde esas rutas. Si debes servir subidas, sirvelas como estáticas, no ejecutables.

Conclusión: próximos pasos que no te morderán después

Los 403 por permisos no son misteriosos. Son deterministas: en algún punto de la ruta, el usuario del servidor web no puede atravesar o leer. Arreglar eso no requiere 777. Requiere un modelo de propiedad que puedas explicar a un compañero sin sueño a las 3 a.m.

Haz lo siguiente:

  1. Añade namei -l y “probar como www-data” a tu runbook. Hazlo obligatorio.
  2. Elige un patrón de permisos (grupo compartido con setgid, o ACLs) e implémentalo para cada sitio. La consistencia es la característica de seguridad.
  3. Separa las rutas escribibles del web root. Tu yo de respuesta a incidentes te lo agradecerá.
  4. Audita directorios world-writable y elimínalos. Si algo se rompe, no es razón para rendirse—es prueba de que necesitabas la auditoría.

Si tratas los permisos como arquitectura en lugar de limpieza, los 403 se vuelven arreglos rápidos, no un personaje recurrente en tu rotación de on-call.

← Anterior
Debian 13: tormentas de writeback en disco — ajustar vm.dirty sin riesgo de pérdida de datos (caso #45)
Siguiente →
Debian 13: Arreglar «Demasiadas redirecciones» en Nginx corrigiendo bucles canónicos y HTTPS (caso nº71)

Deja un comentario