Errores de permisos de archivos en WordPress: Qué deben ser 755/644 y por qué

¿Te fue útil?

Algunos incidentes en WordPress no parecen ser incidentes. La página de inicio carga, marketing está contento, y entonces un editor intenta subir una imagen y recibe un amable “Unable to create directory.” O las actualizaciones automáticas se quedan estancadas. O el sitio pasa a un 403 después de una “rápida corrección de permisos” que alguien ejecutó a las 2 a. m.

Los permisos de archivos son uno de esos problemas que empiezan como una pequeña molestia y acaban en un informe postmortem. También son uno de los pocos problemas de WordPress donde puedes estar técnicamente en lo cierto y aun así romper producción. Hagamos bien las partes aburridas: qué deberían ser 755/644, por qué existen esos números y cómo diagnosticar errores de permisos rápido sin convertir tu raíz web en un sandbox público escribible.

Qué significan realmente 755/644 (y por qué la gente de WordPress los repite)

Cuando la gente dice “establece directorios a 755 y archivos a 644”, no están recitando un hechizo. Están describiendo una línea base segura para un servidor Linux típico donde:

  • Los directorios necesitan el bit de ejecución para ser transitables (sí, ejecución en directorios significa “se puede entrar”).
  • Los archivos por lo general deben ser legibles por el servidor web pero no escribibles por todo el mundo.
  • El propietario puede editar contenido; el servidor web puede leerlo; usuarios aleatorios en la misma máquina no pueden modificarlo.

Permisos en un párrafo que realmente puedes usar

Los permisos Unix son tres conjuntos de bits: propietario, grupo, otros. Cada conjunto puede tener r (lectura), w (escritura), x (ejecución). La forma numérica es octal: r=4, w=2, x=1. Súmalos y obtienes un dígito. Así que 7 significa rwx, 6 significa rw-, 5 significa r-x, 4 significa r–.

Entonces, ¿por qué 755 para directorios?

755 es:

  • Propietario: 7 (rwx) – puede listar, crear, borrar, y atravesar.
  • Grupo: 5 (r-x) – puede listar y atravesar, pero no modificar.
  • Otros: 5 (r-x) – lo mismo.

Para directorios, “lectura” permite listar nombres, “ejecución” permite atravesarlo (y acceder a archivos si conoces los nombres), y “escritura” permite crear/borrar/renombrar entradas. Un directorio con 644 suele estar roto porque le falta ejecución, así que no puedes entrar incluso si puedes “leer” la lista.

¿Y por qué 644 para archivos?

644 es:

  • Propietario: 6 (rw-) – puede editar.
  • Grupo: 4 (r–) – puede leer.
  • Otros: 4 (r–) – puede leer.

Esto es sensato para archivos PHP, imágenes, CSS, JS. El servidor web necesita leerlos. No necesita editarlos en sitio.

La parte que la gente olvida: la propiedad y el usuario del proceso

Los permisos son solo la mitad de la historia. La otra mitad es: ¿qué usuario Unix está ejecutando el servidor web (o PHP-FPM)? Si tu directorio de WordPress es propiedad de deploy:deploy pero PHP-FPM se ejecuta como www-data, entonces 755/644 no permitirán cargas a menos que las rutas escribibles sean propiedad o tengan permiso de grupo en una forma controlada.

Consejo opinado: no soluciones esto poniendo todo a 777. Eso no arregla; es reemplazar una cerradura por una nota adhesiva que dice “por favor no robar”.

Broma #1: Poner la raíz web a 777 es como esconder la llave de tu casa debajo del felpudo y luego tuitear las coordenadas GPS del felpudo.

A qué necesita escribir realmente WordPress (y a qué nunca debería escribir)

WordPress es una aplicación que quiere modificarse a sí misma. Eso es práctico en hosting compartido. En producción, es una decisión de riesgo.

Rutas que WordPress suele necesitar escribir

  • wp-content/uploads/ – cargas de medios, redimensionado de imágenes, archivos generados.
  • wp-content/cache/ (o directorios de caché específicos de plugins) – dependiendo de los plugins de caché.
  • wp-content/upgrade/ – temporal durante actualizaciones.
  • wp-content/languages/ – paquetes de idioma (a veces).
  • wp-content/plugins/ y wp-content/themes/ – solo si permites instalaciones/actualizaciones in-place desde la interfaz de administración.

Rutas a las que WordPress no debería poder escribir en un entorno bien gestionado

  • Archivos core como wp-admin/, wp-includes/.
  • Configuración del servidor como configuraciones de Nginx/Apache (obvio, pero lo he visto).
  • Cualquier cosa fuera de la raíz del sitio (a menos que la hayas diseñado explícitamente así).

Chequeo de realidad del “FS_METHOD”

Si WordPress no puede escribir donde quiere, puede pedir credenciales FTP o fallar las actualizaciones. Eso es WordPress intentando compensar entornos de hosting. En un servidor gestionado correctamente, normalmente eliges uno de estos modos operativos:

  • Código inmutable + pipeline de despliegue: WordPress no puede actualizar core/plugins/themes; las actualizaciones ocurren vía CI/CD (lo mejor para fiabilidad).
  • Solo wp-content escribible: permitir a administradores gestionar plugins/themes; mantener core como solo lectura para el usuario web (compromiso razonable).
  • Todo escribible por el usuario web: camino más rápido hacia el arrepentimiento (evitar).

Una cita que la gente de operaciones tiene por ley

“La esperanza no es una estrategia.” — idea parafraseada comúnmente atribuida a círculos de fiabilidad y operaciones

Los permisos son donde la esperanza va a morir. Modela los usuarios, modela las escrituras, y aplica el mínimo privilegio que aún permita a la operación funcionar.

Guía rápida de diagnóstico (comprobar 1/2/3)

Si estás en medio de un incidente, no empieces con chmod recursivos. Comienza con el conjunto más pequeño de comprobaciones que identifiquen al actor que falla (usuario/proceso) y la ruta que falla.

1) Identifica la ruta exacta que falla y el síntoma a nivel de syscall

  • Busca “Permission denied”, “Operation not permitted”, “Read-only file system”, “No such file or directory”, “SELinux is preventing”, o “open_basedir restriction.” Son problemas distintos que se parecen.
  • Revisa logs del servidor web y de PHP por la ruta.

2) Identifica qué usuario Unix está intentando escribir

  • ¿Usuario del pool PHP-FPM? ¿Módulo de Apache? ¿Usuario de worker de Nginx? ¿Usuario CLI que ejecuta WP-CLI?
  • Confírmalo con la configuración del servicio y la lista de procesos en vivo.

3) Comprueba propiedad + modo + flags de montaje + controles MAC

  • Propiedad y modo: namei -l y stat en la ruta completa.
  • Flags de montaje: ro, noexec, nosuid, y peculiaridades de overlays en contenedores.
  • SELinux/AppArmor: si están habilitados, los permisos DAC pueden parecer bien pero aún así fallar.

Si haces esas tres comprobaciones, normalmente llegas a una solución real en lugar de un juego de whack-a-mole de permisos.

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

Cada tarea abajo incluye: un comando, qué significa una salida típica y la decisión que tomas. Están escritas para sistemas Debian/Ubuntu-ish usando www-data; ajusta para tu distro (apache, nginx, etc.).

Task 1: Find the web/PHP user actually serving WordPress

cr0x@server:~$ ps -eo user,comm | egrep 'php-fpm|apache2|httpd|nginx' | head
root     nginx
www-data nginx
root     php-fpm8.2
www-data php-fpm8.2

Qué significa: Nginx workers y PHP-FPM se están ejecutando como www-data (bien; consistente).

Decisión: Los archivos/dirs que deben ser escribibles por la app deberían ser escribibles por www-data (vía propiedad o estrategia de grupo). Si ves usuarios mixtos (por ejemplo, Nginx como nginx, PHP-FPM como www-data), elige un modelo y alinéalo.

Task 2: Confirm PHP-FPM pool user and group (the source of truth)

cr0x@server:~$ sudo grep -R "^\s*user\s*=" /etc/php/*/fpm/pool.d/*.conf
/etc/php/8.2/fpm/pool.d/www.conf:user = www-data
cr0x@server:~$ sudo grep -R "^\s*group\s*=" /etc/php/*/fpm/pool.d/*.conf
/etc/php/8.2/fpm/pool.d/www.conf:group = www-data

Qué significa: Los scripts PHP se ejecutan como www-data:www-data.

Decisión: Cualquier directorio que WordPress necesite escribir debe ser escribible por www-data, directamente o vía permisos de grupo/ACLs.

Task 3: Verify which filesystem path WordPress is using (docroot)

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R "root " -n | head
cr0x@server:~$ sudo grep -R "root " -n /etc/nginx/sites-enabled | head
/etc/nginx/sites-enabled/example.conf:12:    root /var/www/example/current/public;

Qué significa: Tu docroot real es /var/www/example/current/public, no lo que recuerdas vagamente de hace seis meses.

Decisión: Ejecuta todas las comprobaciones de permisos contra la ruta real, no contra un objetivo de symlink viejo o un directorio de staging.

Task 4: Inspect permissions on the failing directory (quick glance)

cr0x@server:~$ ls -ld /var/www/example/current/public/wp-content/uploads
drwxr-xr-x 2 deploy deploy 4096 Dec 27 09:14 /var/www/example/current/public/wp-content/uploads

Qué significa: El directorio es 755 pero es propiedad de deploy:deploy. El usuario web www-data es “other” aquí, por lo que no puede escribir.

Decisión: Cambia la propiedad o adopta una estrategia de grupo/ACL para que www-data pueda escribir solo donde sea necesario.

Task 5: Check the full path traversal permissions with namei (catches the parent directory gotcha)

cr0x@server:~$ namei -l /var/www/example/current/public/wp-content/uploads
f: /var/www/example/current/public/wp-content/uploads
drwxr-xr-x root   root   /
drwxr-xr-x root   root   var
drwxr-xr-x root   root   www
drwxr-x--- deploy deploy example
lrwxrwxrwx deploy deploy current -> releases/2025-12-27_0910
drwxr-xr-x deploy deploy releases
drwxr-xr-x deploy deploy 2025-12-27_0910
drwxr-xr-x deploy deploy public
drwxr-xr-x deploy deploy wp-content
drwxr-xr-x deploy deploy uploads

Qué significa: El directorio /var/www/example es 750 (drwxr-x---) y es propiedad de deploy:deploy. Incluso si arreglaste uploads, www-data puede que no pueda atravesar el árbol.

Decisión: Asegura que cada directorio padre sea transitables por el usuario que necesita acceso (a menudo vía ejecución de grupo). Esta es la causa oculta del “pero uploads está a 777 y aún falla”.

Task 6: Check file mode and ownership precisely with stat

cr0x@server:~$ stat -c '%A %a %U:%G %n' /var/www/example/current/public/wp-config.php
-rw-r----- 640 deploy deploy /var/www/example/current/public/wp-config.php

Qué significa: wp-config.php es 640 y no es legible por “otros”. Si PHP se ejecuta como www-data y no está en el grupo deploy, WordPress puede fallar con un 500.

Decisión: O hazlo legible por el grupo y pones el usuario PHP en ese grupo, o cambias la propiedad apropiadamente. No hagas chmod 644 a lo loco si puedes mantenerlo más restringido con el grupo correcto.

Task 7: Confirm group memberships (does www-data belong to the deploy group?)

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

Qué significa: www-data no está en deploy. Lecturas/escrituras basadas en grupo no funcionarán hasta que ajustes grupos o uses ACLs.

Decisión: Elige: añade www-data a un grupo compartido (común en servidores single-tenant), o usa ACLs para las rutas necesarias (más limpio en entornos multi-tenant).

Task 8: Fix writable path ownership safely (uploads only)

cr0x@server:~$ sudo chown -R www-data:www-data /var/www/example/current/public/wp-content/uploads
cr0x@server:~$ ls -ld /var/www/example/current/public/wp-content/uploads
drwxr-xr-x 2 www-data www-data 4096 Dec 27 09:14 /var/www/example/current/public/wp-content/uploads

Qué significa: Ahora el usuario PHP puede escribir en uploads sin hacer que todo tu código sea mutable.

Decisión: Si usas herramientas de despliegue que rotan releases, considera un directorio persistente compartido para uploads y enlazarlo con symlink; de lo contrario, el siguiente deploy puede restablecer la propiedad.

Task 9: Set baseline perms (dirs 755, files 644) without clobbering special cases

cr0x@server:~$ sudo find /var/www/example/current/public -type d -print0 | sudo xargs -0 chmod 755
cr0x@server:~$ sudo find /var/www/example/current/public -type f -print0 | sudo xargs -0 chmod 644

Qué significa: Estandariza los modos. Si esto “arregla” el sitio, probablemente alguien ejecutó un chmod recursivo a algo incorrecto como 600 o 700.

Decisión: Inmediatamente vuelve a endurecer archivos sensibles (como wp-config.php) después de la línea base y asegura que los directorios escribibles permanezcan escribibles para el usuario correcto. Las líneas base son andamiaje, no arquitectura.

Task 10: Detect world-writable files and directories (security tripwire)

cr0x@server:~$ sudo find /var/www/example/current/public -perm -0002 -ls | head
  412310    4 drwxrwxrwx   2 www-data www-data     4096 Dec 27 09:15 /var/www/example/current/public/wp-content/cache

Qué significa: Algo es world-writable (...rwx para otros). A veces los plugins hacen esto. A veces los humanos lo hacen en pánico.

Decisión: Quita world-writable a menos que tengas una razón explícita. Prefiere 775 con membresía de grupo controlada o ACLs.

Task 11: Check for immutable bit (the “chmod did nothing” mystery)

cr0x@server:~$ lsattr -d /var/www/example/current/public/wp-content/uploads
-------------e------- /var/www/example/current/public/wp-content/uploads

Qué significa: No hay flag inmutable. Si ves una i en los atributos, el kernel bloqueará cambios incluso para root.

Decisión: Si el bit inmutable está establecido sin querer, quítalo para la ruta específica (con cuidado), no para todo el árbol.

Task 12: Confirm the filesystem isn’t mounted read-only

cr0x@server:~$ findmnt -no TARGET,OPTIONS /var/www
/var/www rw,relatime

Qué significa: El montaje es lectura-escritura. Si fuera ro, WordPress nunca podría subir archivos sin importar chmod/chown.

Decisión: Si ves ro, deja de tocar permisos. Arregla el problema de almacenamiento: remonta, resuelve errores del sistema de ficheros, revisa el overlay del contenedor o investiga por qué el host remonto en modo solo lectura.

Task 13: Check SELinux status (permissions can be “correct” and still fail)

cr0x@server:~$ sestatus
SELinux status:                 disabled

Qué significa: SELinux está deshabilitado; puedes ignorar contextos.

Decisión: Si SELinux está en modo enforcing, debes revisar contextos en directorios escribibles (y corregirlos) en lugar de ampliar modos Unix de forma indiscriminada.

Task 14: AppArmor status (common on Ubuntu)

cr0x@server:~$ sudo aa-status | head
apparmor module is loaded.
10 profiles are loaded.

Qué significa: AppArmor está activo. Un perfil podría estar denegando acceso de escritura a PHP incluso si los permisos Unix lo permiten.

Decisión: Si sospechas de AppArmor, revisa los logs de denegación y ajusta el perfil para las rutas de escritura específicas. No lo “arregles” con 777.

Task 15: Reproduce the write as the PHP user (tests the real permission boundary)

cr0x@server:~$ sudo -u www-data bash -lc 'touch /var/www/example/current/public/wp-content/uploads/.permtest && echo ok'
ok

Qué significa: El usuario de la app puede escribir. Si esto falla con “Permission denied”, tu problema es propiedad/modo/ACL/SELinux/flags de montaje.

Decisión: Usa esto para validar tu arreglo antes de pedir a los editores que reintenten las cargas.

Task 16: Inspect ACLs (when permissions look fine but aren’t)

cr0x@server:~$ getfacl -p /var/www/example/current/public/wp-content/uploads | sed -n '1,12p'
# file: /var/www/example/current/public/wp-content/uploads
# owner: www-data
# group: www-data
user::rwx
group::r-x
other::r-x

Qué significa: No hay reglas ACL extra. Si ves entradas ACL, pueden otorgar o restringir acceso más allá de los bits de modo básicos.

Decisión: En entornos multiusuario, las ACLs suelen ser la forma más limpia de otorgar acceso de escritura a www-data sin darle propiedad.

Task 17: Find recently changed permissions (the “who touched this?” question)

cr0x@server:~$ sudo find /var/www/example/current/public -printf '%TY-%Tm-%Td %TH:%TM %m %u:%g %p\n' | sort -r | head
2025-12-27 09:16 755 www-data:www-data /var/www/example/current/public/wp-content/uploads
2025-12-27 09:12 640 deploy:deploy /var/www/example/current/public/wp-config.php

Qué significa: Puedes ver qué cambió recientemente, lo que ayuda a correlacionar con despliegues o “arreglos rápidos”.

Decisión: Si un despliegue cambió la propiedad de directorios compartidos, actualiza tus scripts de despliegue. La deriva de permisos es un bug de automatización, no un problema de entrenamiento humano.

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

1) “Unable to create directory wp-content/uploads/…”

Síntoma: Fallan las subidas de medios; el panel de WordPress se queja de creación de directorio.

Causa raíz: uploads o un directorio padre no es escribible/transitable por el usuario PHP; a veces el padre es 750 y bloquea el recorrido.

Arreglo: Comprueba con namei -l. Asegura que la ruta completa sea transitables (bit x) y que el directorio uploads sea escribible por el usuario PHP vía propiedad, grupo o ACL. Evita world-writable.

2) Instalación de plugin/tema pide credenciales FTP

Síntoma: WordPress pide FTP aunque estés en tu propio servidor.

Causa raíz: WordPress no puede escribir en wp-content/plugins o wp-content/themes, así que recurre a métodos tipo FTP.

Arreglo: Decide tu modelo operativo. Si permites instalaciones desde el dashboard, haz esos directorios escribibles por el usuario PHP (preferiblemente con escritura de grupo y un grupo compartido). Si no, deshabilita escrituras en el sistema de ficheros y actualiza vía pipeline de despliegue.

3) 403 Forbidden tras “apretar permisos”

Síntoma: El sitio entero devuelve 403; los assets estáticos también pueden fallar.

Causa raíz: Directorios configurados a 644 o 600, quitando el bit de ejecución para que el servidor web no pueda atravesarlos.

Arreglo: Los directorios deberían ser generalmente 755 (o 750 si controlas explícitamente el acceso por grupo). Usa find -type d -exec chmod 755 y valida con namei -l.

4) 500 Internal Server Error, logs muestran “failed to open stream: Permission denied” para wp-config.php

Síntoma: PHP no puede leer la configuración; el sitio muestra pantalla en blanco.

Causa raíz: wp-config.php es 600 o 640 con grupo equivocado; el usuario PHP no tiene permiso de lectura.

Arreglo: Hazlo legible por el usuario PHP vía estrategia de grupo o cambio de propietario. Evita hacerlo legible por todo el mundo si puedes; 640 con el grupo correcto está bien.

5) “Operation not permitted” al ejecutar chown/chmod

Síntoma: Incluso root no puede cambiar propiedad, o los cambios se revierten.

Causa raíz: Bit inmutable establecido, o la ruta está en un sistema de archivos que prohíbe chown (algunos montajes de red), o estás dentro de un contenedor con mapeo de UIDs.

Arreglo: Comprueba lsattr, tipo de montaje y mapeo UID del runtime del contenedor. Arregla la restricción subyacente; no fuerces con violencia.

6) Todo parece correcto, pero las escrituras siguen fallando

Síntoma: Modos/propiedad parecen correctos; touch como root funciona; WordPress sigue sin poder escribir.

Causa raíz: Denegaciones SELinux/AppArmor, remonte en solo lectura, o pool PHP-FPM ejecutándose bajo un usuario diferente al esperado.

Arreglo: Confirma el usuario del pool, revisa el estado/logs del sistema MAC, verifica opciones de montaje. Prueba con sudo -u www-data touch ... para validar el actor real.

Tres microhistorias corporativas desde las trincheras de permisos

Mini-historia 1: El incidente causado por una suposición errónea

Habían heredado una instalación de WordPress que “siempre funcionó”. Un nuevo ingeniero tuvo la tarea de moverla a una VM recién construida. Nginx al frente, PHP-FPM detrás, TLS moderno. La checklist de migración era ordenada: rsync de archivos, importar base de datos, actualizar DNS. Se lanzó a tiempo.

Dos horas después, el equipo de contenido empezó a subir imágenes para una campaña. Todas las subidas fallaban. El de guardia hizo lo habitual: chmod -R 755 en directorios, chmod -R 644 en archivos, y reintentó. Seguía roto. Alguien sugirió la opción nuclear: chmod -R 777 wp-content. Las subidas funcionaron al instante. Todos respiraron.

Dos días después, un escaneo de seguridad marcó el host como comprometido. El postmortem fue desagradable pero educativo. La suposición equivocada no era “755/644 es suficiente.” Era “el usuario de la app puede atravesar la ruta.” La docroot vivía bajo /srv/sites/clientA que era 750 y propiedad de deploy:deploy. Así que el usuario PHP ni siquiera podía alcanzar wp-content de forma fiable. El cambio a 777 enmascaró el problema de recorrido y convirtió el resto del árbol en un patio escribible.

Lo arreglaron correctamente estableciendo un grupo compartido para el sitio, haciendo solo los subdirectorios necesarios escribibles por grupo, y validando el recorrido con namei -l. El compromiso fue simple: dejar que WordPress escriba en uploads y cache, pero mantener el código inmutable. El equipo de seguridad dejó de llamar. El equipo de marketing nunca supo lo que casi pasó, que es como debe ser.

Mini-historia 2: La optimización que salió mal

Otra empresa quiso “despliegues más rápidos”. Su respuesta fue montar wp-content/uploads en un sistema de archivos de red para que varios servidores app compartieran medios. Meta razonable, implementación arriesgada. Usaron un montaje que no se comportaba como un ext4 local: el mapeo de propiedad era inconsistente y la semántica de chmod/chown no era la esperada.

La primera señal de problema: el redimensionado de imágenes fallaba de forma intermitente. Los thumbnails aparecían para algunas subidas y no para otras. Los editores culpaban a WordPress. Los ingenieros culpaban al plugin de caché. Todos se culpaban entre sí. Los logs mostraban Permission denied en archivos que parecían escribibles cuando se comprobaban en un host, pero no en otro.

La “optimización” también fue una regresión de fiabilidad. Su configuración de sistema de archivos de red tenía diferencias sutiles de traducción de permisos entre nodos, y un despliegue a veces creaba directorios con una umask por defecto que los hacía no escribibles para el usuario PHP en la mitad de la flota. No era un fallo constante. Era peor: era intermitente.

La solución final no fue “chmod más fuerte”. Estandarizaron las opciones de montaje, forzaron la creación de directorios con setgid en el directorio compartido (para que los nuevos archivos heredaran el grupo correcto), y añadieron una comprobación de salud que realizaba una prueba de escritura como el usuario PHP en cada nodo. También documentaron el modelo operativo: si vas a compartir uploads, debes tratarlo como un sistema de almacenamiento, no como una carpeta.

Mini-historia 3: La práctica aburrida pero correcta que salvó el día

Una organización retail ejecutaba WordPress como parte de una pila mayor. Nada sofisticado. El equipo hizo una cosa profundamente poco sexy: hicieron que el código de WordPress desplegado fuera de solo lectura y almacenaron uploads fuera del directorio de release en una ruta persistente. Los despliegues creaban un symlink desde public/wp-content/uploads a /srv/wordpress-uploads/site1.

Entonces una vulnerabilidad de plugin apareció en las noticias. No teórica: del tipo donde bots escanean la red buscando archivos PHP escribibles. Ya la habían parcheado en staging, pero el parche en producción tuvo que esperar la ventana de mantenimiento porque el equipo de marketing estaba en plena campaña (siempre es temporada de campaña).

Durante esa ventana, los bots hicieron lo que hacen. Encontraron la página de login y probaron. Encontraron endpoints de plugins y probaron. Lograron alcanzar la ruta de código vulnerable pero no pudieron persistir un web shell donde querían porque los directorios de código no eran escribibles por el usuario PHP. Los uploads eran escribibles, pero ejecutar PHP desde uploads estaba bloqueado a nivel del servidor web.

La respuesta al incidente fue tranquila. No hubo carrera para diffar miles de archivos, ni arqueología de “¿se supone que este archivo exista?”. Parchearon, rotaron secretos y siguieron. La práctica aburrida—separar datos mutables de inmutables—convirtió un posible compromiso en ruido de fondo.

Broma #2: Los permisos son como un código de vestimenta—demasiado estricto y nadie hace su trabajo, demasiado suelto y de repente es “viernes casual de spear-phishing”.

Datos interesantes y contexto histórico (porque Unix es viejo y quisquilloso)

  1. El bit de ejecución en directorios existe desde antes de la mayoría del software web. Significa “search” (atravesar), no “ejecutar”. Confuso a propósito, aparentemente.
  2. La notación octal se volvió la abreviatura humana porque los bits de permisos se agrupan naturalmente de tres en tres. No es arbitraria; es el sistema numérico que encaja con los datos.
  3. El “sticky bit” en directorios (como /tmp) existe porque los sistemas multiusuario necesitaban espacios escribibles compartidos sin permitir que los usuarios borraran archivos de otros.
  4. Setgid en directorios es un truco clásico de colaboración: los nuevos archivos heredan el grupo del directorio. Es anterior a la mayoría de pipelines CI/CD y a menudo más fiable.
  5. Umask es por qué dos servidores con los mismos comandos chmod aún pueden producir permisos diferentes para archivos recién creados. Es una máscara por defecto a nivel de proceso, no una propiedad del sistema de ficheros.
  6. El modelo histórico de Apache de muchos procesos influyó en mucho del consejo legado “simplemente chown a www-data”. PHP-FPM y los runtimes de contenedor cambiaron la forma del problema.
  7. NFS y semántica de permisos han quemado a incontables equipos. Algunos montajes mapean root a nobody (root-squash), y algunos no soportan propiedad Unix como asumías.
  8. SELinux fue diseñado porque el control discrecional de acceso (permisos Unix) no puede expresar “este proceso puede escribir solo en estos directorios etiquetados.” WordPress a menudo tripa esa regla.
  9. El comportamiento de auto-actualización de WordPress es producto de sus raíces en hosting compartido. La práctica moderna en producción suele deshabilitar mutación in-place y tratar el código como artefactos.

Listas de verificación / plan paso a paso

Paso a paso: arregla errores de permisos sin hacer todo escribible

  1. Confirma el actor: determina el usuario en tiempo de ejecución (www-data, nginx, etc.) vía ps y la configuración del pool PHP-FPM.
  2. Encuentra la ruta que falla: desde el error de WordPress, logs de PHP o logs del servidor web. No adivines.
  3. Valida el recorrido: ejecuta namei -l en la ruta que falla y busca un directorio padre que bloquee la ejecución.
  4. Decide tu modelo:
    • Código inmutable + CI/CD, o
    • wp-content escribible (o partes de él), o
    • Mutable completo (no lo hagas).
  5. Arregla la propiedad solo para rutas escribibles: típicamente wp-content/uploads y quizá directorios de cache. Usa chown -R www-data:www-data en esas rutas.
  6. Aplica modos base: directorios 755, archivos 644 para el árbol de código. Endurece archivos especiales después (wp-config.php).
  7. Ejecuta una prueba de escritura como el usuario PHP: sudo -u www-data touch ... en el directorio específico que falló.
  8. Comprueba montaje/MAC: asegúrate de que el sistema de ficheros es rw; revisa SELinux/AppArmor si aplica.
  9. Prevén la deriva: incorpora la propiedad/modos deseados en la automatización de despliegue. Si un deploy restablece permisos, volverás aquí la próxima semana.

Checklist: permisos endurecidos pero funcionales para WordPress (VM típica de un solo sitio)

  • Directorios de código: propiedad de deploy (o root), legibles por el usuario web, no escribibles por el usuario web.
  • wp-content/uploads: escribible por el usuario web; preferiblemente una ruta persistente separada con permisos controlados.
  • No hay directorios world-writable en la docroot.
  • Opcionalmente bloquea ejecución PHP en uploads a nivel del servidor web (no es un bit de permiso, pero importa).
  • Usa estrategia de grupo o ACLs en lugar de “todo propiedad de www-data” si humanos necesitan editar archivos.

Checklist: multi-servidor o almacenamiento compartido (donde la cosa se complica)

  • Estandariza el usuario en tiempo de ejecución entre nodos.
  • Usa setgid en directorios compartidos para preservar el grupo.
  • Valida opciones de montaje y mapeo de propiedad.
  • Añade una “prueba de escritura en uploads” a tus checks de salud por nodo.
  • Prefiere almacenamiento de objetos para uploads si tu arquitectura lo permite; los montajes POSIX compartidos son caros operativamente.

Preguntas frecuentes

1) ¿Son 755 y 644 siempre correctos para WordPress?

No. Son una línea base segura para legibilidad y recorrido. La corrección depende de la propiedad, el usuario PHP y qué directorios deben ser escribibles. En algunos entornos, 750/640 con grupos adecuados es mejor.

2) ¿Por qué los directorios necesitan permiso de ejecución?

Porque ejecución en un directorio significa “puede atravesarlo/buscarlo”. Sin él, el proceso no puede acceder a archivos dentro, incluso si puede listar el directorio.

3) ¿Debe wp-config.php ser 644?

A menudo funciona, pero no es el mejor valor por defecto. Prefiere 640 con el grupo correcto para que solo el propietario y el grupo web/PHP puedan leerlo. Si tu servidor es single-tenant y confías menos en los usuarios locales que en el proceso web, mantenlo más restringido.

4) ¿Es seguro chown todo a www-data?

Es funcional y común, y también una forma fácil de permitir que un proceso web comprometido modifique tu código de aplicación. Si te importa la contención, mantén el código propiedad de un usuario deploy y sólo concede acceso de escritura a uploads/cache.

5) Las actualizaciones de WordPress fallan a menos que haga plugins/themes escribibles. ¿Qué hago?

Elige una política. Si quieres actualizaciones con un clic, debes permitir escrituras en directorios de plugins/themes (idealmente vía escritura de grupo/ACLs, no 777). Si quieres un sistema más seguro, deshabilita actualizaciones in-place y despliega cambios de plugins/themes por CI/CD.

6) ¿Cuál es la diferencia entre “Permission denied” y “Operation not permitted”?

“Permission denied” suele significar que el usuario actual no tiene derechos (modo/ACL/MAC). “Operation not permitted” a menudo indica una restricción más fuerte: atributo inmutable, reglas del sistema de ficheros, o restricciones de namespaces/contendedores.

7) Mis permisos son correctos pero las subidas siguen fallando. ¿Qué más podría ser?

Culpables comunes: sistema de ficheros montado en solo lectura, denegación SELinux/AppArmor, pool PHP-FPM con usuario equivocado, disco lleno (las escrituras fallan), o un directorio padre bloqueando el recorrido. Revisa eso antes de cambiar modos otra vez.

8) ¿Necesito 775 en lugar de 755?

Sólo si usas intencionadamente colaboración basada en grupo (por ejemplo, usuario deploy y PHP comparten un grupo). 775 concede escritura al grupo. Está bien cuando la membresía del grupo está controlada e intencionada.

9) ¿Qué hay de 700 para directorios?

700 es genial para directorios privados que solo el propietario debe atravesar. Es terrible para una raíz web a menos que el proceso web sea el propietario. Usa 750 si quieres “solo propietario+grupo”.

10) ¿Cómo evito la deriva de permisos después de los despliegues?

Hazlo responsabilidad de la automatización. Asegura que los scripts de despliegue establezcan propiedad/modos al crear releases, preserven directorios persistentes (uploads) y verifiquen con una prueba de escritura como el usuario en tiempo de ejecución.

Conclusión: próximos pasos que no te despertarán a las 3 a. m.

La mayoría de incidentes de permisos en WordPress no se tratan de números mágicos. Se trata del desajuste entre quién está escribiendo y dónde accidentalmente bloqueaste el recorrido o la propiedad. 755/644 son buenos valores por defecto, pero no son una estrategia.

Haz esto a continuación

  1. Escribe tu modelo operativo: ¿puede WordPress actualizarse a sí mismo o no? Decide y luego haz cumplirlo con permisos.
  2. Separa lo mutable de lo inmutable: mantiene uploads (y quizá cache) escribibles; mantiene core y la mayor parte del código en modo solo lectura para el usuario web.
  3. Automatiza el estado deseado: los scripts de despliegue deben establecer propiedad/modo y verificarlos. El chmod manual es como crear folklore de permisos.
  4. Añade una comprobación de salud: una simple prueba de escritura como el usuario PHP en el directorio uploads detecta deriva antes de que los editores la noten.

Si sigues esto, 755/644 pasará a ser lo que siempre debió ser: una línea base que entiendes, no un ritual que repites.

← Anterior
ZFS dnodes y metadatos: por qué los metadatos pueden ser tu verdadero cuello de botella
Siguiente →
Ubuntu 24.04 «No route to host»: lista de verificación ARP/gateway/ACL que elimina las conjeturas (caso n.º 32)

Deja un comentario