Configurar Apache + PHP para múltiples usuarios

AVISO: DOCUMENTO DESACTUALIZADO

Atención, este documento está desactualizado. Para la época en que había sido escrito, era una solución aceptable, hoy día no funcionará y la solución descrita no es la mejor.

Mensaje de avertencia

Agradecimiento

En primer lugar quiero agradecer al creador de baifox (Lorenzo Tejera) por la información necesaria para hacer este artículo.

Introducción y problemática

Sáltate esta parte si ya sabes de que va, aquí sólo intento explicar el problema desde varios puntos de vista.

Los que alguna vez hemos intentado montar un servidor de hosting con apache + PHP para varios clientes/dominios, nos encontramos con que no todos los usuarios son de total confianza y hay que asegurar que ninguno de nuestros usuarios se vea perjudicado por un script de otro (que unos no se metan a leer un fichero de configuración de otro). Por ejemplo:

Usuario 1 tiene su home en: 
/www/www.usuario1dominioX.com/html/ -> llamémosla /~1/html/
Usuario 2 tiene su home en:
/www/www.usuario2dominioY.com/html/ -> llamémosla /~2/html/

Lógicamente el Usuario 1 tiene el dominio: www.usuario1dominioX.com y el Usuario 2 posee el dominio: www.usuario2dominioY.com, evidentemente ambos alojados en el mismo servidor y en los directorios previamente descritos.

Ambos usuarios tienen sus respectivos portales con su base de datos de usuarios, hasta aquí todo bien, pero resulta que por casualidades de la vida, los dos tienen montado un phpnuke, resulta que el archivo que tiene el usuario y la contraseña de acceso a la base de datos que tiene la información del portal se llama config.php y en lugar de colocarlo fuera del árbol del directorio web (por ejemplo bajo /~1/privado/config.php ó /~2/private/config.php) para hacer menos predecible un ataque, lo hacen más fácil poniéndolo en /~1/html/.

De modo que cualquiera que acceda a la web: http://usuario1dominioX.com/config.php podrá ver el usuario, contraseña, servidor y nombre de la base de datos que tiene la información de este portal, pero la cosa no es tan simple, simplemente no pasa nada, ya que el archivo config.php que tiene las variables usuario, contraseña, servidor, nombre de la base de datos y algunos otros valores interesantes está protegido mediante algo similar a:

	if (eregi("config.php",$PHP_SELF))
		die("Acceso Denegado. No mires mis datos");

	$usuario="Usuario";
	if(me_da_la_gana)
		echo "Mi usuario es: $usuario, úsalo para hackearme la web por favor.";

Luego cualquier intento de acceder a: http://www.usuario1dominioX.com/config.php obtendrá como página web el texto: «Acceso Denegado. No mires mis datos». Pero entonces ¿De que me estoy preocupando?. Además lo que publique el usuario es cosa de suya y no mía.

Mmmm… ¿y de verdad te crees que he puesto que existen dos usuarios en el mismo servidor para nada si fuese tan fácil?. Pues no, aunque en este caso no debemos preocuparnos de los usuarios exteriores, si debemos preocuparnos de los usuarios que están dentro del mismo sistema.

Imaginemos que sabes programar en PHP y además conoces la web del usuario2, le haces un dig para investigar que ip alberga su página/portal/dominio y BINGO!!! es el mismo servidor que alberga la tuya. Además para mayor problema el usuario2 te cae mal, realmente mal, esta es la tuya piensas, tu sabes que el administrador de tu servidor de hosting es un completo inútil y no sabe proteger nada (situación bastante irreal y que de darse deberíais cambiaros de servidor porque también seríais vulnerables).

Pues accedes a la web de tu otro usuario, la puedes cambiar o bien mirar los datos de su base de datos para meterle una noticia del tipo: «Esta web apesta». Para ello subes un archivo como este:

hackeotuweb.php --------------- 
<?php
fopen("../../www.usuario2dominioY.com/html/config.php","r");
//Rutina que lee el fichero y lo muestra por pantalla, o escribes en él
//También podrías listar su directorio para descargar sus fuentes, etc ?>

Pues bien, el administrador se ha lucido y ha perdido como mínimo un cliente conviertiéndose en el nuevo hazmereír de toda la comunidad libre. ¡Felicidades!. Como queremos evitar esta situación procederemos a arreglar un poco las cosas.

Soluciones problemáticas

Llegados a este punto uno puede decir, actica SuEXEC y php_safe_mode a on, con ello se arregla gran parte (aunque no todo).

Otro diría Un VirtualHost para cada dominio, así además puedes poner en cada VirtualHost unas directivas para que no salga de su HOME como:

php_value open_basedir "/www/www.dominio.com"
php_admin_value safe_mode on

y además te aporta el poder poner cada fichero de log por separado!!!. Que bien!!! (nótese el tono irónico).

Sólo hay un problemilla: resulta que queremos que funcione para 10.000 usuarios. Vaya, ahora que teníamos la solución van y nos dan por donde más nos duele: En la eficiencia.

Resumamos los problemas fundamentales:

  1. Gasto de descriptores de ficheros abiertos (se nos acabarán y nuestro servidor dejará de funcionar correctamente).
  2. Un httpd.conf enorme o miles de inodos desperdiciados en la versión 2 de apache en sites-avaiable/ y los correspondientes enlaces simbólicos hacia sites-enabled/
  3. Complejidad de administración, resulta que cada VirtualHost que añadimos tenemos que reinciar apache, o en el mejor de los casos, si eres más inteligente enviar una señal HUP al proceso apache: kill -HUP apache, para que relea el fichero httpd.conf (en la versión 1.3) o bien que revise los VirtualHosts del directorio sites-enabled/ en la versión 2.

Solución razonable

En apache existe una cosa llamada VirtualDocumentRoot, que de utiliarse de la forma:

VirtualDocumentRoot /www/%0/html

dentro del fichero httpd.conf o apache2.conf permitiría que cualquier petición a tu servidor de por ejemplo: www.midominiopreferido.com buscase por el bajo el directorio: /www/www.midominiopreferido.com/html (hay formas de ahorrar inodos para no tener que tener todos los dominios bajo el mismo directorio, para más información ir al manual de VirtualDocumentRoot en apache) de no encontrar un directorio con el nombre del dominio, devolvería un error 404 NO ENCONTRADO esto lo busca por cada petición que se le hace al servidor web. Por tanto:

  1. No tiene cargado un fichero en memoria ni tiene que leerlo
  2. Puede hacer cache de los dominios más usados para agilizar las cosas y olvidar el resto hasta que se lo soliciten
  3. Al añadir un dominio nuevo no es necesario enviar ninguna señal al apache
  4. Más fácil de administrar
  5. Problemas: permisos que hemos visto arriba.

Podemos volver a pensar, ¿y si añadimos algo como esto al httpd.conf o al apache2.conf:

VirtualDocumentRoot /www/%0/html
#php_value open_basedir "/www/%0"
#php_admin_value safe_mode on

Pues que resulta que el apache con PHP no podría salir y estaría todo solucionado ¿No?. Craso error. Con lo que hemos puesto, solo limitamos a las carpetas html de todos los usuarios (es decir un usuario no puede salir de /www sin embargo puede acceder a los homes del resto de usuarios.

Según PHP, es un error arquitectónico intentar resolver problemas de permisos desde PHP y que estos deberían ser resueltos mediante apache (mediante suexec, creando muchos usuarios, etc). Aún así prefiero solucionarlo desde PHP y que cada usuario en su casa y el root en la de todos por mucho error de base que esto sea, ya que:

  1. Va más rápido y mejor
  2. No es ningún error de seguridad ni ningún error hacerlo desde PHP
  3. Los de PHP no tienen pensado hacerlo

Después de todo esto, os voy a describir la solución que me han ayudado a encontrar (mirar agradecimientos).

Hay que modificar el código fuente del PHP para que el OPENBASEDIR, que no permite que vayas mas abajo de tu directorio con los includes, use el VIRTUAL DOCUMENT ROOT del apache del dominio en ese momento, poniendo algo así:

/*  Special case VIRTUAL_DOCUMENT_ROOT When using mod_vhost_alias the      DOCUMENT_ROOT = PATH_TRANSLATED - SCRIPT_NAME(request_uri)       This allows for protected mass dynamic hosting */  
} else if ((strcmp(PG(open_basedir), "VIRTUAL_DOCUMENT_ROOT") == 0) && SG(request_info).path_translated && *SG(request_info).path_translated ) { strlcpy(local_open_basedir, SG(request_info).path_translated, sizeof(local_open_basedir)); local_open_basedir_sub=strstr(local_open_basedir,SG(request_info).request_uri); /* Now insert null to break apart the string */ if (local_open_basedir_sub) *local_open_basedir_sub = '\0';

El fichero fopen_wrappers.c se encuentra para bajar al final de la página. Hay que recompilar el código de PHP, para ello, vas a la página de PHP y te bajas los fuentes: http://www.php.net/downloads.php, asegurate que te bajas los de la versión 4 que es para los que ha sido probado, despues no hay más que recompilar como dicen las instrucciones y listo.

Posteriormente, en el apache, en su fichero de configuración (ya sea httpd.conf o apache2.conf) hace falta añadir lo siguiente:

UseCanonicalName Off
ServerName *
VirtualDocumentRoot /www/%0
php_admin_flag engine 1
php_admin_value safe_mode 1
php_admin_value open_basedir "VIRTUAL_DOCUMENT_ROOT"
php_admin_flag display_errors 1
php_admin_value error_reporting 2039
php_admin_flag track_errors 0

Bueno, eso y los logs hacia donde apuntarán (os aconsejo definirlos en un único fichero para ahorrar descriptores), luego si los guardáis con el siguiente formato:

 LogFormat "%V %h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined

podréis usar una herramienta llamada split-logfile que parte el fichero grande de log (/var/log/apacheX/access.log en archivos www.dominioX.tld.log, www.dominioY.tld.log) y guardar estos archivos donde queráis (por ejemplo dejárselos ver al usuario) antes de rotar el fichero (saber como funciona minimamente rotatelogs).

Por último, recomendar la herramienta: baifox, que es un panel de control al estilo CPanel pero GPL para controlar correos, web, etc. Está orientado a debian aunque debería funcionar en cualquier otra distribución.

Descarga del fichero modificado

fopen_wrappers.c

Errores/Dudas/Consultas

A la papelera, esto… bueno, si queréis preguntar, hacedlo a mi correo, aunque no puedo prometer que conteste, ya que suelo estar bastante ocupado, espero que esto le sirva a alguien. Aun así se agradece mucho que mandéis algún email, anima bastante a continuar.