Llevo un tiempo queriendo hacer algunas pruebecillas con el tema de los proxys, los proxys inversos, balanceadores de carga, etc y hoy por fin de la mano de Docker vamos a montar un pequeño sistema en local, en muy pocos pasos y ultrafacil.
Antes de liarnos a descargar imágenes y tal vamos a dar un paso a tras y ver qué es realmente un proxy inverso y en que se diferencia de un proxy normal. Conceptualmente son bastante parecidos pero hay unas diferencias sutiles. Podríamos decir que la diferencia mas importante es a quien da servicio. Empezaremos con un proxy. Pues un proxy es una pieza intermedia entre un usuario y un servicio en internet que cachea o filtra ciertos contenidos dependiendo del tipo de usuario o tipo de categoría del servicio. Imagina que un empleado en un empresa decide que es buena idea acceder a Facebook en horario laboral. El proxy detectaría la categoría de ese empleado y sus privilegios asociados y podría prohibir el acceso a este servicio. Por otro lado imagina que ciertos empleados acceden a la web corporativa de su filial en Japón, y tras la primera consulta el proxy guardaría una copia de dicha web para que el segundo empleado no tuviera que esperar a que se descargue todo el contenido desde Japón.
Con esto ganamos muchísimo rendimiento y una mejora de la experiencia de usuario importante. Un proxy de esta naturaleza no está exento de riesgos por que podría cachear una versión vieja de dicha página y mostrar contenido obsoleto.
Vayamos ahora a ver en qué consiste un proxy inverso. Este carácter 'inverso' es lo que define este tipo de proxies. Un proxy inverso es una pieza que sitúa entre internet y los servidores en una empresa. Imaginemos que una empresa alberga sus servidores web y de correo. Evidentemente tienen que ser accesibles desde internet, lo que puede suponer un riesgo el tener expuestas las máquinas. ¿Como solucionamos esto? pues anteponiendo una maquina que 'oculte' todos los servidores, la topología, sistemas operativos empleados, cantidad, etc. A un usuario no le interesa saber si hay 5 servidores Apache sirviendo una web de transacciones económicas, y no le interesa a nadie las ips de los cinco servidores ni si están corriendo una RedHat o una Debian. Disponer esta información para un hacker, puede ser muy útil. Averiguamos que un servidor web tiene un Apache 2.2.6 que quizás tenga alguna vulnerabilidad y podemos lanzar algún ataque aprovechándolo.
Un proxy inverso tiene varias características:
Anonimato: La primera y más evidentes es que oculta la arquitectura real que sirve el servicio. Como ya he contado protegería la infraestructura y sus detalles hacia el exterior.
Cifrado: El cifrado es una de las características mas interesantes de un proxy reverso. Imaginemos que queremos securizar nuestro servicio de transacciones económicas, así que hemos comprado un certificado a entidad competente y en lugar de añadir este certificado a cada una de las maquinas servidoras se lo instalamos al proxy. De esta manera liberamos de esa parte del trabajo los servidores últimos.
Proteccion adicional: Un hacker puede probar "recetas" típicas para intentar adivinar que lenguaje se ha empleado para desarrollar la web, PHP? ASP Node? una de las funciones del proxy puede ser filtrar peticiones incorrectas.
Balanceo de carga: Otra función muy interesante podría ser la del balanceo de carga, lo veremos mas tarde. Imaginemos que el proxy delante del cluster de servidores fuera distribuyendo las peticiones entre los distintos servidores para intentar distribuir la carga, y así agilizar la respuesta. Esto no es trivial, ya que si un usuario inicia sesión en una de las máquinas, y posteriormente el balanceador cambia de servidor puede tener implicaciones. Hay maneras para hacer que un usuario siempre este conectado al mismo servidor y así mantener la sesión, manteniendo sticky sessions.
Cacheado: Una de las funciona típicas de los proxies es la de cacheo de los recursos. Los estáticos pueden ser servidos por el proxy aliviando la carga de backend.
vamos a ponernos manos a la obra y a montarnos un pequeño escenario con un proxy que nos va a hacer las funciones de balanceo de carga. Para ello vamos a usar unos de los más típicos proxies, el Nginx. Para ello nos vamos a ayudar de Docker, descargaremos una imagen de Nginx y copiaremos un archivo de configuración.
Nos hacemos un clone de este repo:
git clone https://github.com/danielgimeno/proxiinverso
Tras descargarnos el repo nos encontramos un archivo de configuracion donde está la configuracion del nginx.
upstream miprojecto {
server WWW.XXX.YYY.ZZZ:1234;
server WWW.XXX.YYY.ZZZ:1235;
server WWW.XXX.YYY.ZZZ:1236;
server WWW.XXX.YYY.ZZZ:1237;
}
server {
listen 80;
server_name mitest.com;
location / {
proxy_pass http://miprojecto;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $server_name;
}
error_page 404 /errorPages/404.html;
}
La configuración se lee sola. Definimos nuestros servidores en el backend, una por linea con el formato server IP:port y le damos un nombre al conjunto. Posteriormente cada uno de los directivas Server define el puerto en el que escucha, el nombre del sitio, el que será visible al exterior, como un unico host. Si el proxy tuviera más sitios o servicios configurados añadiríamos uno por cada Server. A continuación vienen un conjunto de directivas proxy_set_header que permitiran pasarle ciertos parametros al servidor del backend. Vamos a ver su valor. para poder lanzar esta prueba, he creado 4 servidores web 'on the fly', el de PHP que nos viene como anillo al dedo para esto. Así que me he creado 4 carpetas y he creado dentro un archivo test-php
daniel@server ~/proyectos/localserver cat test.php
<?php
echo "Que pasa!, Woooorld!!, soy el servidor con el puerto: 1234";
//capturo todas las cabecereas que me envia el Proxy y las pinto
foreach (getallheaders() as $nombre => $valor) {
echo "$nombre: $valor";
}
Cada carpeta tiene un test.php distinto, pero por que quiero poder diferenciar cada vez que invoco mitest.com cual de los servidores va a responder. Y los ejecuto así:
daniel@server ~/proyectos/localserver php -S 0.0.0.0:1234
Ese comando me ejecuta un servidor que viene integrado con PHP, muy útil para estos menesteres, y me entrega una traza por cada petición:
daniel@server ~/proyectos/localserver php -S 0.0.0.0:1234
PHP 7.2.24-0ubuntu0.18.04.7 Development Server started at Wed Dec 30 21:32:44 2020
Listening on http://0.0.0.0:1234
Document root is /home/daniel/proyectos/localserver
Press Ctrl-C to quit.
[Wed Dec 30 21:33:56 2020] 192.168.1.91:39320 [200]: /test.php
[Wed Dec 30 21:33:59 2020] 192.168.1.91:39322 [200]: /test.php
[Wed Dec 30 21:34:03 2020] 192.168.1.91:39326 [200]: /test.php
[Wed Dec 30 21:34:10 2020] 192.168.1.91:39332 [200]: /test.php
Perfecto, pues ya solo me queda que levantar el contenedor. En la maquina donde este Docker, nos vamos donde esta nuestro dockercompose y lanzamos un docker-compose up
daniel@dockerserver:~/proyectos/proxyinverso/nginx$ docker-compose up
Starting proxyinverso ... done
Attaching to proxyinverso
proxyinverso | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
proxyinverso | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
proxyinverso | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
proxyinverso | 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
proxyinverso | 10-listen-on-ipv6-by-default.sh: info: /etc/nginx/conf.d/default.conf differs from the packaged version
proxyinverso | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
proxyinverso | /docker-entrypoint.sh: Configuration complete; ready for start up
Ya tenemos todas las piezas corriendo los servidores detrás, y delante nuestro nginx, pues vamos nuestro navegador, y ponemos el server-name indicado en la directiva Server de mi nginx. Yo usaré un curl:
daniel@med030 ~/proyectos/localserver curl mitest.com/test.php
Que pasa!, Woooorld!!, soy 1236
Host: mitest.com
X-Real-IP: 192.168.1.35
X-Forwarded-For: 192.168.1.35
X-Forwarded-Host: mitest.com
Connection: close
User-Agent: curl/7.58.0
Accept: */*
daniel@med030 ~/proyectos/localserver curl mitest.com/test.php
Que pasa!, Woooorld!!, soy 1234
Host: mitest.com
X-Real-IP: 192.168.1.35
X-Forwarded-For: 192.168.1.35
X-Forwarded-Host: mitest.com
Connection: close
User-Agent: curl/7.58.0
Accept: */*
daniel@med030 ~/proyectos/localserver curl mitest.com/test.php
Que pasa!, Woooorld!!, soy 1237
Host: mitest.com
X-Real-IP: 192.168.1.35
X-Forwarded-For: 192.168.1.35
X-Forwarded-Host: mitest.com
Connection: close
User-Agent: curl/7.58.0
Accept: */*
Y, efectivamente en cada llamada atiende un servidor de mi back. El proxy le pasa a mis fake servers, el host con el que llame al proxy, la ip desde lo que lo llame, etc. Si lo invoco desde el navegador, en otro ordenador de mi red obtengo:
Que pasa!, Woooorld!!, soy 1235 Host: mitest.com X-Real-IP: 192.168.1.65 X-Forwarded-For: 192.168.1.65 X-Forwarded-Host: mitest.com Connection:
close Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Purpose: prefetch Accept-Encoding: gzip, deflate Accept-Language: en-GB,en;q=0.9,es-ES;q=0.8,es;q=0.7,en-US;q=0.6