Servidores web más seguros con Docker (II)

En la anterior entrada Servidores web más seguros con Docker, expliqué cómo debería ser una arquitectura de un servidor LAMP con Docker para hacerlo lo más seguro posible, aislando cada apartado de cada web lo más posible para limitar los daños que de un posible atacante.

En esta entrada quiero cubrir la instalación de la arquitectura propuesta, pero sólo la parte más básica, ya que en el artículo anterior cubrí todas las medidas posibles a tomar y la instalación sería realmente compleja.

Partiré de una instalación limpia de Debian 9.6 Stretch. Si ya tienes otras cosas instaladas en tu servidor no debería ser problema, igual hay algunos paquetes que ya tienes instalados. Si tu distribución no es Debian ni basada en ella (como CentOS o SuSE) la instalación de paquetes tendrás que adaptarla a tu distro, pero la mayoría de los pasos son los mismos.

Requisitos:

  • Servidor Linux (Windows, OSX y BSD también funcionan pero no los tengo en cuenta en este artículo)
  • Kernel para 64 bits 3.10 o superior
  • 2GB de RAM disponibles
  • 5GB de disco disponible
  • Distribución Linux suficientemente nueva como para correr Docker.
  • El servidor no puede estar dentro de un contenedor pero sí puede ser una máquina virtual; es decir, si es un VPS XEN o KVM funciona, pero si es un VPS con OpenVZ seguramente no funcione.

Como se puede ver los requisitos son muy básicos. Sobre si tu distribución es lo suficientemente reciente, si tiene menos de 5 años seguro que funciona. Si es más antigua, probablemente hayan guías para hacerla funcionar. En Debian desde la versión 7 (Wheezy), en Ubuntu desde la 14.04 (Trusty).

Preinstalación

En el caso de que sea una máquina nueva, aquí están los pasos que yo he seguido, muy básicos, para prepararla para la instalación real.

Si aún no tienes “sudo” habilitado en el servidor, haz login como root y ejecuta:

# adduser usuario sudo

Luego sal de la sesión y vuelve a entrar. Sudo debería funcionar ahora.

Lo primero que yo suelo hacer en un servidor es instalar aptitude. No es necesario, pero lo prefiero personalmente a apt:

$ sudo apt-get install aptitude

En el caso de que estés usando VirtualBox, es conveniente instalar las Guest additions. Sin ellas funciona bien, pero instalarlas agrega una mayor integración con el sistema anfitrión.

Monta el CD usando el menú:

Y copia el contenido a una carpeta dentro de tu $HOME:

$ mkdir vbox
$ cp /media/cdrom vbox/ -R

Necesitaremos los paquetes necesarios para poder compilar módulos para el kernel que está corriendo:

$ sudo aptitude install linux-headers-amd64 gcc make perl

Ahora ya debería funcionar el programa de instalación de las Guest Additions para Linux:

$ cd vbox
$ sudo ./VBoxLinuxAdditions.run

También habilita en el administrador de VirtualBox el compartir el portapapeles:

Y reinicia la máquina virtual. Ahora el portapapeles debería funcionar, así como el modo fluido.

Instalación

Vamos a seguir primero los pasos de Docker CE (Community Edition) para Debian:
https://docs.docker.com/install/linux/docker-ce/debian/

Agregamos los paquetes para APT sobre HTTPS:

$ sudo apt-get install apt-transport-https ca-certificates curl gnupg2 software-properties-common

Agregamos la clave GPG de Docker:

curl -fsSL https://download.docker.com/linux/debian/gpg | sudo apt-key add -

Y agregamos el nuevo repositorio:

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/debian $(lsb_release -cs) stable"

Actualizamos el repositorio e instalamos docker-ce:

sudo apt-get update && sudo apt-get install docker-ce

Comprobamos si está correctamente instalado ejecutando el hello world de Docker:

sudo docker run hello-world

Este debería ser el resultado (o similar):

Instalaremos también docker-compose:

sudo apt-get install docker-compose

Para comprobar que esté instalado, simplemente ejecutamos “docker-compose” y deberíamos ver esto: (la salida es mucho más larga, sólo pongo las primeras líneas en la captura)

Y con esto, tenemos el servidor listo y funcionando.

Configuración inicial

Vamos a empezar con un fichero de docker compose que nos inicie un Nginx y más adelante agregaremos un contenedor de php-apache y los conectaremos vía túnel proxy.

Como el contenedor de Nginx vamos a exponerlo al puerto 80, hay que vigilar que no tengamos otro servicio activo en el mismo puerto. Podemos usar el siguiente comando para inspeccionar si hay algo escuchando en el puerto 80:

sudo lsof -i -nP | grep LISTEN | grep :80

Si devuelve algo, es que tenéis un servidor web u otro programa escuchando en el puerto y no podréis exponer el puerto de NGINX al puerto 80. La solución sería desinstalar el programa que esté activo en el puerto o reconfigurarlo para que use otro puerto.

Pero para facilitar el testeo, en la guía en vez del puerto 80 voy a usar el 85. Así, si tenéis algo sobre el puerto 80 que sea importante no hace falta que lo quitéis. Cuando queráis usar los contenedores como servidor web principal sólo tenéis que cambiar donde veáis 85 a 80 y reiniciar docker compose.

Creando nuestro primer fichero para docker compose

Creo que estamos listos para empezar de verdad. Vamos a empezar con una carpeta vacía y agregar un fichero llamado “docker-compose.yml”. Este fichero es el que “docker-compose up” leerá (cuando estemos desde la misma carpeta en la consola) y seguirá sus instrucciones.

Nuestra primera versión contendrá sólo las siguientes líneas:

version: '3'
services:
master-webserver:
image: nginx:1.14
ports:
- "85:80"

Guardamos y ejecutamos “docker-compose up”, como nuestro usuario habitual, no necesitamos sudo. El usuario principal debería tener permisos para ejecutar docker sin ser root. La salida se parecerá a algo como esto:

Accedemos a http://localhost:85 y deberíamos ver lo siguiente:

Esto es indicativo de que Nginx está funcionando correctamente en el contenedor. Pasemos a analizar cómo funciona el fichero de docker compose, para que podáis configurarlo vosotros mismos.

docker-compose.yml como su extensión ya indica, es un fichero YAML. Si no habéis trabajado con YAML antes, es como JSON pero más fácil de leer. De hecho acepta JSON sin problemas. Al final es un set de diccionarios y listas que contienen textos o números. No tiene mucho secreto.

El fichero, por líneas:

  • version: ‘3’ – Es el número de versión del formato del fichero. Por temas de compatibilidad, cuando docker compose ejecuta el fichero revisa este valor, de forma que cuando introducen nuevos cambios que son incompatibles en versiones nuevas no afectan a ficheros que antes funcionaban. La versión actual es 3.
  • services: – este bloque indica que vamos a definir los contenedores (servicios) que se pondrán en marcha
  • master-webserver: – este es el nombre de un servicio a nuestra elección. Puede ser cualquier cosa, pero debería seguir las normas de un nombre de dominio porque luego se tiene que resolver por DNS. Cuando otro contenedor quiera acceder a este, tendrá que ir a http://master-webserver/; de ese modo, si contiene caracteres extraños podría dar problemas.
  • image: nginx:1.14 – aquí le decimos la imagen que tiene que cargar. Puede que sea del Docker Hub o que la hayamos creado localmente. Si el nombre lo puede localizar docker, funciona. El formato es igual que en docker: “nombre-imagen:nombre-tag”. Hay que poner siempre el tag o descargará la última versión, que un día puede que contenga una versión incompatible y nos rompa el sistema que hemos montado.
  • ports: Aquí le indicamos un listado de puertos en el formato “puerto-anfitrion:puerto-contenedor”. Esto hará que enrute los puertos automáticamente al iniciar. Como ya veis aquí he indicado el 80 del contenedor al 85 del anfitrión. Si queréis que este contenedor sea el principal para servir webs en la máquina, simplemente cambiadlo por “80:80”.

Esto está muy bien para empezar, pero quiero agregar una cosa más que será útil luego. Ahora mismo ni podemos cambiar la web que sirve, ni tampoco podemos configurar el servidor al gusto. Si queremos agregar más adelante un proxy_pass para phpmyadmin necesitamos poder cambiar la configuración.

Una forma sería crear nuestra propia imagen a partir de la de Nginx y copiar los ficheros que queramos. Pero no es necesario. Podemos jugar con los puntos de montaje, que en docker se llaman volumes (volúmenes). Basta con agregar una clave “volumes:” y un listado de “path-anfitrion:path-contenedor” con los montajes que queramos. Pueden ser carpetas o simples ficheros.

La ventaja de usar volúmenes en lugar de copiar dentro de la imagen es que el efecto es inmediato y no necesitamos regenerar la imagen. Para empezar vamos a crear una carpeta “master-webserver” y un fichero dentro “index.html”. Dentro de éste ponemos un HTML básico de ejemplo para probar. En mi caso, agregué:

<!doctype html>                       
<html>
<head>
<title>Welcome to Nginx - The Master Webserver</title>
</head>
<body>
<h1>Welcome to Nginx - The Master Webserver</h1>
<p>This is just a sample page</p>
</body>
</html>

Ahora queremos que Nginx sirva este fichero en lugar del que viene por defecto. Pero, ¿dónde está el fichero de Nginx por defecto? No importa, agregamos nosotros un fichero de configuración y sobreescribimos la configuración por defecto.

Creamos un fichero, en la misma carpeta que el anterior y lo llamamos nginx.default-site.conf; El contenido sería el siguiente:

server {
listen 80 default_server;
listen [::]:80 default_server;

root /var/www/html;
  index index.html index.htm;

  server_name _ localhost;

  location / {
  # First attempt to serve request as file, then
  # as directory, then fall back to displaying a 404.
  try_files $uri $uri/ =404;
  }

  # deny access to .htaccess files
  location ~ /\.ht {
  deny all;
  }
}

Como veis es un fichero básico que sería para localhost o para dominios que no están registrados (acceso vía IP sin dominio, por ejemplo). La parte interesante aquí es que estamos especificando la raíz en /var/www/html. Esta es la carpeta donde pondremos el index.html.

Ahora solo falta agregar los puntos de montaje para estos dos ficheros:

volumes:  
 - ./master-webserver/nginx.default-site.conf:/etc/nginx/conf.d/default.conf
- ./master-webserver/index.html:/var/www/html/index.html

El orden no importa; puede estar antes de “ports:” o después. O al inicio. Donde le veáis más sentido. Lo importante es que tiene que estar correctamente indentado dentro de “master-webserver”.

En la primera línea lo que hacemos es sobreescribir la configuración por defecto de Nginx. En esta imagen de Docker hub está en conf.d/default.conf, pero en otras instalaciones podría estar en otro lado.

Para inspeccionar la imagen yo lo que hago es ejecutar bash en un contenedor nuevo:

$ docker run -it --rm nginx:1.14 bash

Esto lo que hace es crear un contenedor temporal que se borrará al salir (–rm), que es interactivo (-it) y en lugar de ejecutar el comando por defecto, ejecuta bash. A partir de ahí es como una terminal SSH a otro ordenador. Podéis revisar los ficheros, e incluso borrar lo que queráis. Cualquier cosa que hagáis se deshará al salir de la sesión.

La segunda línea en volumes es bastante obvia, simplemente enlazamos el fichero a la carpeta que habíamos dicho en la configuración: /var/www/html

Ahora, al hacer “docker-compose up” y acceder a localhost:85 de nuevo, vemos esto:

Y con esto terminamos la primera parte de la configuración. Si os habéis perdido en algún momento o simplemente queréis descargar el código sin tener que seguir los pasos, podéis ir a mi GitHub para descargar o examinar el resultado final:

https://github.com/deavid/docker-lamp/tree/v0.1

Agregando PHP

Vamos a seguir un poco más y agregar un segundo contenedor para PHP. Este contenedor queremos que tenga su propio Apache, por lo que Nginx tendrá que conectar con él vía proxy HTTP.

Empiezo con esta configuración y no con FastCGI porque es más sencilla de entender y porque es muy habitual que las webs de los clientes dependan de Apache porque contienen ficheros .htaccess que deben ser obedecidos.

Para esto vamos a agregar un nuevo servicio llamado “php-apache-1” y lo vamos a apuntar a la imagen php:7.3-apache-stretch. No necesitará puertos, ya que Nginx debe conectar internamente y no a través del anfitrión. Pero de momento, para probar, agregaremos un entrutado al puerto 86. Sería agregar el siguiente bloque de texto:

  php-apache-1:
image: php:7.3-apache-stretch
ports:
- "86:80"

Al ejecutar “docker-compose up” por primera vez veremos como descarga la imagen. Si os fijáis, hay una primera capa (layer) que ya existe y aparece como “Already exists”, esto quiere decir que el sistema operativo se está reusando entre la imagen de nginx y php, que no se almacena dos veces.

Al abrir http://localhost:86/ vemos lo siguiente:

El resultado no es que sea muy prometedor, pero ya tenemos Apache funcionando sobre nuestro puerto 86, y supuestamente tiene PHP instalado.

Vamos a copiar el mismo fichero html al mismo sitio a ver si nos funciona. Agregamos el mismo volumen que antes:

volumes:  
- ./master-webserver/index.html:/var/www/html/index.html

Esto no sería muy correcto porque el fichero que está en la carpeta para un servicio lo estamos usando para otro, pero de momento nos sirve.

Abrimos de nuevo la web y:

Voilà! Funciona! ¿Pero porqué dice Nginx? Obviamente, porque el fichero index.html contiene “Nginx” literalmente. Pero el puerto es el 86, así que todo en orden. Tenemos Apache funcionando.

Ahora vamos a probar a poner un PHP en lugar de un HTML y ver si le gusta. Creemos unas carpetas que emulen un servidor web multidominio y pongamos dentro un fichero php. Por ejemplo www/php-apache-1/index.php. El punto de montaje sería:

volumes:
- ./www/php-apache-1:/var/www/html/

El fichero php de ejemplo puede ser como éste:

<html>
<head>
<title>Prueba de PHP</title>
</head>
<body>
<?php echo '<p>Hola Mundo</p>'; ?>
<?php echo time(); ?>
</body>
</html>

Reiniciamos docker compose y recargamos localhost:86; nos aparecerá esto:

Funciona! De hecho cada vez que recargamos la página, el número cambia (porque es la hora unix). Esto es lo que nos demuestra que estamos delante de una página dinámica.

Ahora vamos a conectar Nginx al contenedor de PHP. Para ello se usa proxy_pass. Hay que modificar el fichero de configuración. Añadiremos otra sección location como la que sigue:

location /php-apache-1/ {
rewrite /php-apache-1/?(.*) /$1 break;
proxy_set_header X-Forwarded-For $remote_addr;
  proxy_pass http://php-apache-1/;
}

Con esto le estamos diciendo que cuando el usuario acceda a una URL que empieza con /php-apache-1/ tiene que enviarla a http://php-apache-1/ (que es el nombre que le hemos dado a nuestro servicio), agregar una cabecera X-Forwarded-For con la IP de origen (que más adelante usaremos para convertirla en la IP real desde Apache) y transformar la URL para eliminar la parte inicial de /php-apache-1/ cuando se envía a Apache; ya que Apache no tiene esta carpeta.

Con esto, accedemos ahora a http://localhost:85/php-apache-1/ y vemos que funciona:

Bingo! Ahora tenemos un contenedor conectado a otro, sirviendo una web desde otro servidor web. Es fácil imaginar cómo agregar más contenedores como éste y conectarlos todos a la vez.

Esto tiene un par de pegas. Porque Nginx en el contenedor está en el puerto 80 pero enrutado al 85, cuando abres una URL sin la barra final que no es un fichero, Nginx devuelve un HTTP 301 Moved Permanently a la url correcta con la barra al final, pero cambia el puerto al 80. Esto no sucedería si los dos puertos son iguales. Es decir, si te molesta, pasa el contenedor al puerto 85; o en cambio, si lo enrutas al puerto 80 del anfitrión, también se soluciona.

El otro problema es que es una subcarpeta y no otro dominio. Esto provoca que tengamos que usar el rewrite para solucionarlo. En la realidad usaríamos diferentes dominios, pero para probar era más fácil en carpetas porque los dominios si no resuelven al servidor, tienes que editar el fichero hosts en tu ordenador para simularlo. Pero funciona de modo muy similar, sólo hay que seguir una guía cualquiera de Nginx para configurar virtual hosts.

Si queréis descargaros el ejemplo, aquí está la dirección de github:

https://github.com/deavid/docker-lamp/tree/v0.2

Veréis que en la configuración de Nginx está solucionado el problema de la redireción 301 que comentaba antes. sólo son 3 líneas.

Y hasta aquí llego por hoy. Nos leemos en el siguiente artículo!

Networking features that Docker that I would like to see

I have been researching a lot lately on Docker & Docker Compose and I’ve been impressed how well Docker Compose behaves and how easy makes things. With a few Kb of configs you can setup a really complex collection of containers properly interconnected. Also it simplifies A LOT the setup phase of those, as usually it is just “docker-compose up -d” and everything else is being taken care for you.

One of the most challenging things to do properly is networks; docker-compose leverages a lot on this matter, so it is way easier now. But on my research I found that still falls very short on slightly advanced setups.

Let me discuss which features I would like to see implemented in the future. Beware that any feature described here is purely fictional.

Current limitations

Let’s begin by explaining what are the current pitfalls as of today:

  • userland-proxy enabled by default. As it seems that for old Linux distributions disabling it still has bugs, seems sensible to have it “ON” by default. But this has the drawback that servers that expect to log incoming request or block by IP do not work. One user setting up fail2ban in a container could end easily being blocked because other attacker has tried too many attempts, and for the container everything is the same IP address.
  • No firewall rules per container in docker-compose. This means that any Docker image or docker-compose repository willing to setup anything special needs the user to configure the host with complex iptables rules.
  • IP Tables / Firewall rules are not configurable from the container. This makes almost impossible or really hard for a container to dynamically change the rules. Some secure setups require this.
  • The host sees all networks. While this is nice, it clutters “ip” commands and alike; and also it means that any userland program in the host is able to communicate by default with all container ports. This is insecure.
  • Networks and Builds are not smartly compared. So if you change the docker-compose or a Dockerfile you have to remember either to use the –build switch or to remove the network so it gets recreated.

My desired features

Here are a collection of ideas that could led Docker to be even easier to use and we could see really complex setups in docker-compose that would be just plug&play.

Userland-proxy per container or exposed port

As for my understanding of the userland-proxy is kind of a “socat” program listening on the host IP and bridging to the actual container network, effectively making a NAT route purely in userspace.

When it is disabled, extra iptables rules are added to cover full NAT support.

If that’s the case, nothing stops docker for enabling it on by exposed port basis. It could be enabled by default, and on a container for docker-compose we could have this syntax:

ports:
- target: 80
published: 8080
protocol: tcp
userland-proxy: prefer-disabled

The values for “userland-proxy” would be “prefer-enabled/prefer-disabled/require-enabled/require-disabled”. Docker can in its server config use the “userland-proxy” as a default and add another key like “userland-proxy-container-allow” to enable containers switch the behaviour. “prefer” or “require” would be used for making the container fail if the requirement is not met, or just ignore the fact that the setting was not met.

This can enable “nginx” images to log proper IP’s on default settings and avoid annoying users with bugs that are not using the image. Users with serious issues can still disable the feature.

Also, imagine that one docker image is making use of fail2ban; with this they could “require” the userland-proxy to be disabled in order to run the image, so this avoid potential disasters.

It would be extra-nice for a container to get some information on those settings on boot, so for example an optional fail2ban in an image could be disabled on boot if the userland-proxy is still enabled.

Extra networking restrictions

In a default docker-compose setup, every container has access to every other. While this is nice for starting is dangerous, as one hacker gaining access to one container could start attacking other containers on non-exposed ports.

The way I solve this currently is adding one network per container, and attaching containers just to the networks they require. The problem with this is that we easily end with 10 networks, which are a pain on “ifconfig” outputs. Also it seems overkill.

Docker already manages a bunch of iptables rules, so I guess it can handle a few more to further filter down connectivity. For example, in docker-compose we could have something like:

services:
php-fpm:
image: php-fpm
networks:
- www
firewall-internal: # Between containers only
input-allow-only:
- service: nginx
dst-port: 3301
output-allow-only:
- service: mysql
dst-port: 3306
firewall-external: # To and from internet
input-allow-only:
- ip-address: 133.133.133.133/24
dst-port: 22
output-allow-only:
- dns: www.wordpress.com # for automatic updates...
dst-port: 80,443

How nice would be that? Docker could take care of all rules and we just forget. Easy to read and very restrictive. Still there could be challenges as allowing a certain DNS as a destination; it would need a “ipset”, do the query on boot and add every IP to the set. Also a refresh of that DNS every hour would be required in order to keep track of new IP addresses of the servers.

Currently the only way to do this is by manually adding iptables at the host level and taking care of loading them on boot.

IPTables updatable inside containers

Without extra permissions and compatible with docker-compose. That was obvious, right?

May seem crazy, but with the use of “ipset” we could achieve a high degree of personalization without compromising security. Following the prior example, consider this one:

services:
php-fpm:
image: php-fpm
firewall-external:
input-allow-only:
- match-set: trusted_hosts
dst-port: 22
admin:
build: ./admin
  volumes:
  - ./firewall-sets/trusted_hosts.txt:/trusted_hosts.txt
firewall-sets:
trusted_hosts:
type: "hash:ip"
options:
timeout: 3600
file: ./firewall-sets/trusted_hosts.txt

The idea here is leveraging the work to “ipset”. We can have a file that docker can monitor for changes and update the ipset accordingly

Having the list of addresses or address:port in its own file can be already a great advantage. But being able to mount it in other container to change it is awesome, it allows us to build admins to do port-knocking mechanisms. All of this inside docker-compose and without extra privileges.

Include directive in Compose

I found a pair of GitHub Issues (#1599) and (#318). I’m really in favour of the first one, where it declares that several files should be parsed first:

includes:
- db-common.yml
- redis-common.yml
web:
extends:
file: web-common.yml
service: web
command: bundle exec rails s -p 80 -b 0.0.0.0 thin

It can help big projects to manage nicely huge amounts of configuration splitted onto files. If we have to decide which path we should take into account, I would say go with the parent (caller) path. As this is how it is done for the extends.

Conclusion

I had to stop at some point suggesting features. Docker Compose is a great tool and I would like to see it growing in the next years. Containers are still a young technology far from displaying all its capabilities. Stay tuned as things are very likely to improve shortly as it gains even more traction.

Hope someone from Docker finds this and thinks these would be as cool to have as I think they are!

Cómo conseguir que los contenedores Docker vean la IP de origen

Siento soltar el rollazo de las aventuras, desventuras y lo mal que lo pasé intentando solucionarlo, pero tengo que hacerlo. Si no te apetece leerlo, ve al capítulo “TL;DR” que allí explico cómo hacerlo. Y te ahorras 3 horas o más de sufirimiento.

Lo importante a tener en muy en cuenta es que por defecto los contenedores no ven la IP externa que se está conectando, por lo que todos los logs de IP, baneos, filtros y firewalls no sirven de nada. A no ser que te dé absolutamente igual quien acceda desde internet al contenedor, esto es algo que hay que corregir.

…la ira lleva al odio, el odio lleva al sufrimiento, el sufrimiento al lado oscuro

Estaba preparando un gran artículo sobre Docker para poder filtrar las IP de internet que pueden acceder al contenedor, y he terminado cabreadísimo con el NAT, las redes, iptables y conmigo mismo.

Había acabado de agregar a la cookie de sesión información variada para verificar la autenticidad del usuario, como la fecha de login, el user agent del navegador y la IP con la que se logueó. Y al comprobar, veo que Flask no me devuelve la IP de mi equipo, sino la IP del gateway del router en el que está encerrado.

Me voy a ver la documentación, StackOverflow y Google, y me dan una solución: mismo resultado. Empiezo a ver si es que Nginx no está pasando correctamente el dato, pruebo otras tantas cosas. Tampoco. Y entonces veo el log de Nginx y me doy cuenta de que también registra la misma IP del gateway!

Mierda, esto no va a acabar bien…

Empiezo a buscar sobre Nginx no respeta la ip entrante, y me encuentro un montón de gente con Docker con el mismo problema. Docker tiende a hacer esto con los contenedores. Así que empiezo a tirar de “lsof” y “nc” (netcat) para comprobarlo. Efectivamente, lsof reporta la IP del gateway como origen dentro del contenedor, y en el host reporta la IP de mi equipo como destino.

Busco más y básicamente dan dos opciones, o pasar el contenedor de Nginx a la red de tipo “host” en lugar de “bridge” o desactivar algo llamado “userland-proxy”. Lo de cambiar la red no parece buena idea, así que indago más en la segunda; en cuanto me dí cuenta que había que desactivarlo para globalmente en el servidor de Docker y no por contenedor, vuelvo atrás y miro lo de la red en “host”. Esto efectivamente tiene que funcionar pero comparte/reusa la misma red que el anfitrión, cosa que no es agradable.

Pero al arrancar los contenedores de nuevo me doy cuenta de que Docker Compose se niega a que un contenedor esté a la red “host” y en la red “bridge” a la vez; ésta última es la responsable de las conexiones entre contenedores. Pero claro, el anfitrión está en todas las redes a la vez, así que la red “host” tiene que tener acceso a todos los contenedores igualmente, ¿verdad? Pues sí pero no, porque Docker en este modo no crea sus nombres en /etc/hosts por lo que cuando Nginx intenta hacer “proxy_pass http://apache&#8221; falla porque “apache” no puede resolverlo en las DNS.

Así que otra vez a Google a ver cómo hacer que el anfitrión tenga acceso a los nombres, o que ese contenedor los tenga. Todas las soluciones son rebuscadas y/o con software que hay que instalar en el anfitrión. Y en ese momento me dí cuenta…. aunque funcione, ¿los servidores FTP y SFTP van a tener el mismo problema? Va por iptables, así que… pues sí, lo tienen igualmente. Así que los servidores FTP también en modo “host”, ¿no? Ni de coña. Por ahí sí que no paso. Si por dar algo más de seguridad tengo que abrir las puertas a todo, algo está tremendamente mal. Y aquí empieza mi cabreo.

Me voy a iptables, a seguir depurando con lsof/ncat y a intentar entender qué cojones está haciendo Docker. Todo lo que tiene que hacer es un puñetero NAT, desde la IP del equipo a la IP del contenedor. Lo mismo hace un router de toda la vida y los servidores web detrás del router ven a qué IP están atacando. ¿Qué está haciendo Docker con las iptables? Tuve que revisar lo que era un “masquerade” en redes e inspeccionar las iptables a mano a ver si había algo raro. Nada.

Y entonces me doy cuenta que en lsof aparece un proceso “docker-proxy” que parece estar intermediando entre el anfitrión y el contenedor, haciendo el enrutado a mano. Es decir, como si fuese un programa “socat” en ambos lados, uno leyendo los paquetes y el otro emitiéndolos. Normal que la IP parezca venir desde dentro ¡porque viene desde dentro! Argh! ¿Cómo le digo a Docker que deje de hacer esa “basura” de NAT y haga uno como dios manda?

Después de buscar sobre el susodicho “docker-proxy” y salirme 50 enlaces sobre imágenes de Docker para proxies, consigo encontrar algún artículo que explica que este proceso es el encargado de hacer el “userland-proxy”. Esto es lo mismo que al principio, pero la verdad es que si se puede eliminar globalmente, ahora que entiendo la “m” que está haciendo Docker con el NAT, mejor quitarlo por completo. Fuera, pero ya

Quitarlo es sencillo, se agrega {“userland-proxy”: false} a “/etc/docker/daemon.json” y se reinicia Docker. ¿Funciona? Pues no, yo veo lo mismo otra vez, consulto desde el ordenador con la IP pública del equipo (eth0) y sigo viendo como origen la IP del gateway. A buscar otra vez, hay muchos que les da problemas y otros tantos que no les funciona. Al final encuentro en GitHub issues un proyecto que dicen que a veces las reglas NAT arrancan tarde, la conexión se hizo antes y se queda guardado en las tablas de enrutado. Que haga “conntrack -D” y solucionado… pues tampoco. Así que reinicio el ordenador entero, a ver si….

Creo que ya veis el patrón, ¿verdad? Otra vez lo mismo. Y lo peor, que ahora hay otro proceso de docker, en este caso el servicio maestro, que está escuchando en la IP. Después de revisar otra vez las iptables, me doy por vencido, que le den: voy a agregar una regla a mano en iptables y a enrutarlo por mi cuenta.

Después de pelearme un buen rato con IpTables y pensar que Docker estaba borrando mis reglas me dí cuenta que “iptables” es un poco especialito para listar las reglas. Hay diferentes tablas que contienen cadenas; y por defecto filtra por la tabla “filter”. Si agrego algo en la tabla “nat” es normal que no lo vea. Al agregar la regla y verificar que estaba allí, sigo sin que un simple “nc” haga ni la intención de conectar. Empecé a pensar que la regla de Docker estaba tomando precedencia; así que la subo. Tampoco. Saco los contadores de paquetes: ni uno. Cambio la regla para filtrar por IP en lugar de por interfaz, tampoco, ni un paquete.

Vale que la regla NAT, una sola, no es suficiente para hacer un NAT en condiciones; no espero que funcione, pero al menos el primer paquete de conexión debería activar la regla y los contadores ni se movían.

Aquí ya me empecé a dar cuenta de que había algo fundamental que estaba pasando por alto desde el principio. Me vino a la cabeza muchos routers que cuando hacen NAT son incapaces de enrutar desde dentro, hacia adentro; y que las reglas de iptables para solucionarlo son bastante rebuscadas…. ¿podría ser eso? ¿todas estas horas perdidas aquí sólo porque estoy probando desde mi equipo al contenedor de dentro, en lugar de usar un segundo equipo para comprobar?

Menudo escalofrío me vino. Por un lado, deseando que sea eso y por otro deseando no haber perdido tres horas por soberana estupidez. Y me voy al portátil, abro una consola, escribo “nc 192.168.1.10 1026” y… conecta. Comorrr? si desde mi equipo no va!

Compruebo los conteos de mi regla y efectivamente ha capturado y redirigido correctamente, no solo eso, sino que la conexión es perfecta. Estupefacto me quedo. Pues si es eso, probemos el puerto 80 con Nginx y a ver qué IP saca, tal cual sin agregar reglas.

Y funciona. Los logs reportan la IP del portátil; pero si hago la prueba desde local veo la IP del gateway. Esto lo explica perfectamente, parece que las reglas de la cadena PREROUTING no se ejecutan cuando la conexión sale del mismo equipo.

Y qué más da si las conexiones locales se ven como 127.0.0.1, 192.168.1.10 o 172.16.144.1; una IP u otra, es local y para el caso es lo mismo. Para las IP externas funciona. Victoria, aunque agridulce. Qué mal trago.

NOTA: Si te ha parecido un rollazo (que lo es) tengo que agregar que he pasado por alto la mitad de las pruebas y cosas que busqué. Tres horas de pánico dan para mucho.

TL;DR

Os explico cómo solucionarlo: Básicamente hay que desactivar el “userland-proxy”. Basta con crear el fichero “/etc/docker/daemon.json” con el siguiente contenido:

{
"userland-proxy": false
}

Luego hay que reiniciar Docker:

$ sudo service docker restart

Y lo más importante, para probarlo TIENE QUE SER DESDE OTRO EQUIPO

Hay algunos usuarios que han reportado problemas con IPv6 después de hacer esto, otros con problemas al arrancar Docker. Pero todos los casos tienen algo en común: ejecutaban CentOS/RedHat o una versión antigua de Ubuntu (12.04).

Si la distribución es reciente no debería dar problemas. A mí desde luego no me ha dado ninguno. CentOS y RedHat publican una release cada cuatro años y tienen soporte de diez; no es raro ver equipos RedHat con más de 10 años de antiguedad, eso explica que también les de problemas.

Espero haberos ahorrado unas cuantas horas!

Servidores web más seguros con Docker (I)

Si alguna vez has administrado servidores web multidominio con decenas (o cientos) de webs, seguramente te habrás encontrado en algún momento con el problema de que un hacker consigue hacerse con una de las webs y desde ahí, empieza a contaminar todo el servidor. Llega el punto a veces que puede hacerse con el control del servidor completo, pero habitualmente roban credenciales de otras webs y empiezan a hackearlas también. Es muy complicado deshacer los cambios y devolver el servidor a un estado mínimamente seguro de nuevo.

Docker es un sistema de contenedores de aplicaciones y una de sus principales aptitudes es aislar una aplicación de su anfitrión de forma que los recursos a los que puede acceder están todos en el contenedor, y a priori, no puede acceder a nada externo. Hay muchas sutilezas y detalles que explicar de Docker, pero para resumir, Docker agrega mucha seguridad y sin prácticamente agregar carga al sistema anfitrión.

La idea en la que se basa esta serie de artículos es que si tienes 100 webs distintas de distintos clientes y una es hackeada, al menos el hacker quedará contenido dentro de Docker, por lo que sólo esa web quedará afectada, pudiendo restringir sus recursos, apagarla, etc. Pero tanto el anfitrión como los demás clientes en el servidor deberían estar a salvo.

Medidas a tener en cuenta antes de plantearse Docker

Docker no va a ser la solución a todo. Aunque las distintas webs estén totalmente aisladas o tuviésemos un VPS contratado por web: si éstas son fácilmente hackeables tenemos un gran problema, robo de datos, credenciales, puede que ejecuten código malicioso, que envíen Spam o algo peor, usando nuestros servidores. Los ISP se pueden cabrear bastante si no hacemos algo y Docker no soluciona esto.

Veamos algunos puntos típicos en materia de seguridad de servidores web:

  • Permisos de ficheros. Los permisos de acceso a los distintos ficheros de la web y del sistema operativo son la última barrera de seguridad que impide que un hacker se haga con la web. Hacer “chmod 0777 . -R” es por norma general una irresponsabilidad. Además, hay que usar correctamente los distintos usuarios y grupos para evitar que un proceso escriba donde no debería.
  • Revisar y eliminar configuraciones inseguras. PHP, Apache, MySQL, todos los programas tienen ficheros de configuración que a veces vienen configurados de forma irresponsable. A veces es así por defecto, o porque la instalación de la época así lo hacía. Para cada uno de ellos hay que seguir las guías disponibles en internet para asegurarse que todo está en orden. A veces para empezar configuramos los servicios de forma insegura porque es un hastío tener que proveer contraseñas o para ciertos aspectos. Pero lo que para empezar era aceptable, cuando es producción todo esto se tiene que revisar y corregir. Y no uséis la misma contraseña varias veces!
  • Actualizar el software y estar pendiente de nuevas vulnerabilidades. Casi todos los servidores FTP han tenido vulnerabilidades graves en algún momento, SSH ha tenido diversas vulnerabilidades gravísimas en el pasado, Linux ha tenido otras tantas. Mejorar a la última versión no es necesario, pero al menos que la versión que uses no tenga ninguna vulnerabilidad conocida que pueda impactar en el negocio.
  • Contraseñas. Evitar que ningún usuario en ningún servicio tenga una contraseña débil, conocida o que se pueda deducir. En algunos casos como SSH es posible hasta eliminarlas por completo y acceder por certificado. Evitar compartir la contraseña a toda costa con cualquier persona, página web, etc.

En mi experiencia, la mayor parte de los hackeos que he visto han venido por dos flancos: Contraseñas descubiertas o Software web con vulnerabilidades.

Desgraciadamente en algunos casos, es inevitable. Una vez se le robaron credenciales FTP a un cliente vía virus; supongo que el programa detectó la conexión, la espió, y obtuvo IP, usuario, contraseña y la comunicó. En otros casos el cliente quería administrarse un WordPress y instaló unos plugins con unas vulnerabilidades terribles.

En el caso de FTP, se puede usar SFTP, pero configurarlo para que SFTP esté limitado correctamente a una carpeta es complicado. En el caso de “yo me lo administro” lo veo realmente difícil a no ser que seamos expertos en la plataforma y podamos limitar efectivamente lo que puede instalar o lo que puede configurar.

En fin, lo que vengo a decir es que usar Docker no te exime de seguir todas las prácticas y consejos de seguridad posibles para servidores web sin Docker.

Arquitectura de un Servidor Web con Docker

Hay muchas formas de organizar Docker. Normalmente en cada Docker se suele introducir una única aplicación, por lo que es mejor tener el servidor web, PHP/Python/… y bases de datos separadas entre sí.

Diagrama de ejemplo de la arquitectura propuesta

Base de datos

Las bases de datos, por simplicidad, lo mejor es dejarlas en el anfitrión, sin Docker alguno; con un único servicio (MySQL) compartido para todos. Normalmente las vulnerabilidades de las bases de datos no van a permitir que se acceda al sistema anfitrión, y si tenemos los permisos correctos teniendo un usuario separado para cada base de datos correctamente limitado, no debería ser problema. En el caso de PostgreSQL es planteable tener uno por cliente, ya que se rinde bastante bien cuando hay distintos servidores PostgreSQL en la misma máquina. Para MySQL no es demasiado recomendable, ya que consume mucha memoria y suele ser más interesante unificar; no obstante sí es factible, y también se puede plantear en “tiers”, es decir, teniendo 2-5 servidores MySQL donde en cada uno agrupamos un cierto número de clientes.

El problema de Docker con las bases de datos es que Docker asume que los datos en disco van a ser bastante estáticos. Una base de datos dentro de Docker va a requerir que configuremos volúmenes de datos, o bien que compartamos una carpeta del anfitrión; y muchas veces las bases de datos no están en un lugar que facilite separarlo en otra carpeta, por lo que requiere configuración a medida.

Si tienes interés en realizar contenedores para MySQL, dale un vistazo a la imagen oficial de Docker y lee al completo la documentación en la página:
https://hub.docker.com/_/mysql/

Para PostgreSQL existe también una imagen oficial y las instrucciones son casi idénticas:
https://hub.docker.com/_/postgres

Hay que tener en cuenta en el caso de querer lanzar varias instancias a la vez, que todas las instancias del mismo servicio abrirán el mismo puerto. Esto no es un problema si al conectar agregamos la IP a la que queremos conectar; pero si queremos exponer el puerto en el anfitrión (localhost para conexiones internas, o a la IP pública para poder conectarse desde fuera), entonces habremos de numerar los puertos. No hace falta cambiar el puerto interno, pero el externo al compartir IP, tiene que ser distinto. Por ejemplo, se puede enrutar PostgreSQL del 5432 al 54321 para el contenedor 1, al 54322 para el contenedor 2, etc.

Servidor Web (Apache/Nginx)

Como ya he sugerido antes, necesitamos un servidor web común a todas las webs. La razón de esto es que normalmente queremos que todas las webs sean accesibles en el puerto 80 en la misma IP.

Si tuviésemos suficientes IPs disponibles para alojar cada web en una IP dedicada, el servidor web maestro no sería necesario o se podría lanzar un contenedor por web. Esto sería factible con IPv6, ya que se pueden pedir rangos de 256 IPs de forma relativamente sencilla. Con IPv4 esto no es el caso, y hasta que IPv4 no quede completamente en desuso, el servidor web va a tener que seguir sirviendo en una sola IP.

En el caso de que tengas varias IP disponibles, es posible segmentar las webs entre distintos servidores web; teniendo por ejemplo 3 servidores web para 50 webs. No es que vaya a haber ninguna diferencia, pero puede permitirte diferentes configuraciones a la vez.

También puede ser interesante lanzar otros servidores web en distintos puertos bajo la misma IP, pero dudo que tenga alguna utilidad inmediata.

Para NGINX la imagen la tenéis aquí:
https://hub.docker.com/_/nginx/

Y para Apache, aquí:
https://hub.docker.com/_/httpd

En ambos casos, la configuración está ya dentro de la imagen, por lo que si queréis configurarla al detalle, tendréis que personalizar la imagen vosotros mismos. Otra opción podría ser montar la carpeta de la configuración desde una carpeta en el anfitrión.

Y también, hay que montar la carpeta con las webs (p.ej. /var/www) dentro de la imagen, para que se pueda servir el contenido estático directamente. A no ser, claro, que queráis que cada contenedor de aplicación tenga su servidor web propio, en cuyo caso el servidor web maestro serviría de proxy. Puede ser útil para permitir configuraciones esotéricas por cada web, pero tener un proxy de por medio complica las cosas a la hora de detectar correctamente la IP de la que proceden las peticiones.

Yo recomiendo que las aplicaciones corran por un protocolo tipo CGI, como es FastCGI o SCGI y que el contenido estático sea servido por el servidor web maestro. Consume menos recursos y las webs serán más rápidas.

A la hora de los puntos de montaje, lo mejor es que el servidor web tenga todos los puntos de montaje en solo lectura, ya que se supone que el servidor web no escribe sus configuraciones ni tampoco escribe en las webs. Si tenéis logs por web, podéis agregar un punto de montaje específico por cada web que sí tenga acceso de escritura. Esto evita que en el remoto caso de que el servidor web sea comprometido, el atacante pueda escribir en el disco, y esto prácticamente detendrá todos los ataques. Además garantiza que al reiniciar el contenedor todo volverá a su sitio.

Aplicaciones (PHP/Python/Ruby/…)

Aquí es donde las cosas se ponen interesantes. Las aplicaciones son el mayor riesgo de seguridad, y la mayoría de ataques empiezan aquí. Es por eso que voy a hacer más énfasis en este apartado en las diferentes técnicas que se pueden usar para limitar el daño.

El contenedor contendrá una sola web y contendrá el menor software posible en él. Además los permisos de escritura estarán limitados lo máximo posible.

Antes de entrar en detalle en cómo hacer la seguridad, vamos a ver cómo lo crearíamos.

La conexión con el servidor web maestro pueden ser básicamente dos, dependiendo cual queramos la imagen de Docker es una u otra. Una opción es tener un servidor web dentro de éste contenedor de aplicación y servir las peticiones vía proxy; otra opción es tener un servicio FastCGI sin servidor web, en el caso de PHP. En el caso de Python/Ruby, normalmente es SCGI, pero ambos protocolos funcionan casi igual.

Para PHP, tenemos las imágenes aquí:
https://hub.docker.com/_/php/

De todas las opciones que hay para PHP, nos interesan las versiones x.x.x-fpm para FastCGI y x.x.x-apache para Apache.

La documentación es demasiado larga para la página de Docker y ha salido cortada; para leerla completa hay que ir a este enlace:
https://github.com/docker-library/docs/blob/master/php/README.md

Hay que montar los datos de la web en /var/www/html y debería funcionar.

Para Python, las imágenes están aquí:
https://hub.docker.com/_/python

En el caso de Python, Ruby y otros, no se incluye servidor web o servidor SCGI. Lo más normal es que haya que crear nuestra propia imagen a partir de éstas para agregar los servicios necesarios para servir páginas web.

Si montáis un Apache por contenedor, recordad que hay un plugin llamado RemoteIP que permite leer la cabecera X-Forwarded-For y usarla como IP entrante a todos los efectos, para que la web no pierda la información de la IP del usuario: https://httpd.apache.org/docs/2.4/mod/mod_remoteip.html

Seguridad adicional en contenedores de aplicación

Lo primero, hay que revisar cómo funciona la web en cuestión que queremos levantar. ¿Tiene carpetas de ficheros temporales, uploads, etc donde la web tiene que escribir? Si no es el caso, lo mejor es montar la carpeta como sólo lectura. Si tiene ciertas carpetas donde se supone que va a escribir, se pueden separar en dos directorios distintos en el anfitrión, el directorio principal lo montamos como sólo lectura y el de uploads lo montamos dentro, en su ubicación real, como de escritura.

Aseguráos que el servidor web no ejecute en ningún caso los directorios marcados como de escritura; y que no sirva los ficheros php/python en ningún caso sin procesarlos primero.

Por defecto, las imágenes de Docker se montan en el contenedor como lectura-escritura. Esto está muy bien para la mayoría de los casos y por simplicidad, pero para los contenedores de aplicación esto es un riesgo de seguridad. Si se compromete la web, hay posibilidades de que el atacante intente escribir en el sistema operativo. Aunque ésto no va a sobrevivir un reinicio del contenedor, mientras esté en marcha sigue siendo un problema de seguridad grave.

Las imágenes de Docker al crear el contenedor se pueden marcar como de sólo lectura. Hay algún handicap en esto y es que la mayoría de aplicaciones esperan que se pueda escribir en /tmp y otros directorios. Pero una vez solucionado, el contenedor queda muy bien protegido ya que aparte de servir la web, no puede hacer prácticamente nada. Dadle un vistazo a éste enlace:
https://www.projectatomic.io/blog/2015/12/making-docker-images-write-only-in-production/

Otra medida de seguridad que podemos agregar es crear nuestra imagen propia basada en la imagen que queramos y eliminar todos los ejecutables del sistema operativo que no se vayan a usar. La mayoría están en /bin /sbin /usr/bin y /usr/sbin. Esto va a impedir que un atacante use los comandos disponibles para hacerse con el sistema.

Y se me olvidaba lo más básico, el usuario bajo el que se ejecuta PHP o Python, tiene que ser un usuario propio que sirva únicamente para leer los ficheros necesarios y escribir donde deba hacerlo. Nada más.

Como última medida, muy a tener en cuenta, es agregar reglas de firewall al contenedor. La aplicación no debería ser capaz de acceder a internet y realizar peticiones. Esto impedirá que el posible hacker envíe spam o se descargue herramientas maliciosas desde sus servidores. Hay que tener en cuenta que sí que queremos normalmente que envíe email, pero que no pueda conectarse a alguna weeb para comprobar actualizaciones o instalar plugins, porque eso significa que la web puede sobreescribirse a sí misma. Para el email, lo mejor es habilitar sólo las IP y puertos de los servidores de email a los que se va a conectar, siempre vía SMTP con contraseña. De este modo el hacker, en caso de enviar spam tendrá que usar los servidores que nosotros controlamos. Aquí hay una guía:
https://dev.to/andre/docker-restricting-in–and-outbound-network-traffic-67p

Con todas estas medidas, un posible hacker que consiga encontrar una vulnerabilidad está limitado a las siguientes tareas maliciosas:

  • Ejecutar un minero, normalmente de Monero, programado en PHP o Python. Es extremadamente ineficiente, y al final los resultados no los va a poder obtener fácilmente.
  • Enviar spam usando nuestros servidores de correo. Desgraciadamente las credenciales tienen que ser accesibles a la aplicación web y no es posible distinguir un acceso legítimo de los que no lo son. Por lo menos lo va a tener más complicado porque de normal intentan emitir el correo directamente o usar el demonio de correo local, que no requieren credenciales. Si quiere enviar va a tener que localizar las credenciales entre el código para reusarlas; o usar el mismo framework que use la web.
  • Cargar ficheros en las carpetas de upload o temporales e intentar ejecutarlos. Si los permisos están correctos, ésto no debería ser posible.
  • Robar credenciales. Va a poder acceder a los ficheros, por lo que cualquier contraseña en ellos seguramente será comprometida. Los usuarios y contraseñas de base de datos o de correo deberían funcionar sólo desde el contenedor o desde el servidor anfitrión, para evitar que el hacker las use desde sus servidores. Y por supuesto, no reusar esas contraseñas con ningún otro servicio o usuario.
  • Modificar la base de datos. Con las credenciales o haciendo uso del mismo framework, se pueden cambiar los datos de la web. Si el framework/CMS que usamos aloja parte de la lógica de la aplicación o código PHP en la base de datos (que es muy mala práctica), podrá ejecutar más código y éste código sobrevivirá un reinicio del contenedor. Se puede contener haciendo que el usuario de la web no tenga acceso de escritura a las tablas peligrosas.

Transferencia de ficheros (FTP)

No hay que dejar de lado las FTP, ya que son otro agujero de seguridad importante; no es la primera vez que un atacante modifica una web a través del servidor FTP.

Como servidor FTP yo recomiendo vsftpd, que funciona rápido y es probablemente el más seguro. Pero no hay imágenes oficiales de Docker para vsftpd ni para ningún servidor FTP, así que tendremos que construirnos la nuestra.

Otra cosa importante es que FTP es inseguro y lo recomendado es SFTP. SFTP no es más que un acceso vía SSH a la máquina y se debe configurar correctamente. Pero con Docker, ésto es mucho más fácil.

Mi recomendación aquí sería tener FTP sólo si es estrictamente necesario y configurarlo con SFTP si es posible. Un contenedor por web, y cada uno en un puerto distinto. No debería ser un problema si el cliente tiene que acceder al puerto 2101/2201 para una web y al 2102/2202 para otra. Al final, esto se queda guardado en configuraciones de los programas como FileZilla y sólo es hacer doble clic.

El contenedor debería estar creado como sólo lectura, no debería necesitar de temporales, tal vez requiera de escribir logs para lo que podemos montar una carpeta en el anfitrión y que no se borren cuando se reinicia el contenedor. Agregaríamos el punto de montaje /var/www a la carpeta del anfitrión donde estén los datos que tenga que poder modificar, y ya está.

Otros consejos para contenedores de aplicación

Con lo que he explicado más arriba, las vulnerabilidades restantes son básicamente el envío de SPAM, el robo de credenciales y la necesidad de un administrador para instalar plugins. Veamos qué se puede hacer al respecto.

SPAM

A la hora de tratar el envío de SPAM, lo más apropiado sería tener un servidor de correo dedicado para las webs, o un contenedor de correo por web. De este modo, si algo va mal, se puede apagar el contenedor de correo problemático sin afectar al resto. Un servidor de correo puede delegar la entrega a otro servidor, de forma que si queremos que todo email esté gestionado por la misma plataforma no es problema, sólo le tenemos que configurar que reenvíe los correos al servidor de email que elijamos.

Otra forma de contenerlo es agregar a estos contenedores una limitación de ratio de emails por hora. Si se excede puede generar una alarma, limpiar la cola y detener el servicio durante un par de horas para evitar males mayores. Esto bloquearía parte del SPAM incluso a través de formularios de contacto. La mayor ventaja de este sistema es que los ISP se suelen enfadar más por el volumen de SPAM que por el SPAM en sí mismo, por lo que puede evitar muchos problemas con un ISP.

Como último método, se pueden instalar herramientas antiSpam en el servidor que analizen el correo saliente, bloqueando lo que sea sospechoso. SpamAssasin es una buena forma de empezar, pero para afinar mejor Bogofilter funciona muy bien, pero hay que entrenarlo y vigilarlo.

Robo de credenciales

En este apartado hay poco que hacer, ya que las credenciales se tienen que poder leer desde los programas del contenedor, así que si un hacker consigue ejecutar código malicioso, ten por seguro que va a poder leer las credenciales.

Las únicas cosas que puedo recomendar aquí son, nunca reusar las contraseñas y en caso de duda cambiarlas de vez en cuando. Y por supuesto, los servicios a los que se accede con esas credenciales que no se puedan acceder desde internet.

La única alternativa, que me parece bastante surreal, es conseguir abrir conexión con los servicios requeridos al iniciar el programa, realizar un fork y cerrarlo dentro de un chroot para que no pueda acceder a las credenciales. Pero esto puede ser más un riesgo de seguridad que una ventaja, ya que para hacer chroot tienes que tener permisos de root inicialmente. El montaje sería realmente complejo y fácilmente se nos puede escapar algo que sea realmente más peligroso que el robo de credenciales.

El programa que ejecutamos, si es Python o PHP, es posible que tenga opciones para limitar a qué carpetas puede acceder, pero esto sería permanente y no podríamos pasar las credenciales fácilmente.

La única opción que se me ocurre es pasar las credenciales vía una variable de entorno, y al arrancar el programa y conectar con el servidor, borrarla. Pero esto en la mayoría de páginas web requiere modificar su código fuente muy a medida.

Modo de administrador para instalar plugins

Algo habitual que comenté antes es que el administrador a menudo quiere poder actualizar el software desde la misma página web o instalar plugins. Esto requiere una configuración muy insegura, que de estar activa, un hacker puede usarla para modificar la web permanentemente y puede ser realmente complicado de deshacerlo más tarde.

Una solución a esto es tener un segundo contenedor con los permisos relajados y servir esta versión bajo otro puerto o subdominio. El administrador accedería a esta versión únicamente cuando tenga que actualizar o instalar plugins. Pero este contenedor no puede estar siempre abierto a todo el mundo, pues sería lo mismo al final ya que un hacker podría encontrar fácilmente la segunda web y atacarla allí.

Una primera solución es bloquear el acceso a la web a nivel del servidor web usando un plugin de HTTP Auth, que pida usuario y contraseña a la vieja usanza para acceder a cualquiera de las páginas. Este protocolo no es demasiado seguro, por lo que si alguien espía la conexión a través de HTTP obtendrá las credenciales. La solución es servir esta web alternativa sólo bajo HTTPS. Se puede configurar usando LetsEncrypt, ya hice un artículo sobre esto en el blog:
https://deavid.wordpress.com/2018/06/12/sedice-https-y-autorenovacion-de-certificados/
https://deavid.wordpress.com/2018/05/20/como-http-2-acelera-tu-sitio-web/

Adicionalmente, se puede bloquear el acceso a todas las IP excepto una lista de IPs determinadas. Esto sería lo más seguro pero el problema es que muchos clientes tienen IP dinámica, por lo que en caso de hacerlo habría que habilitar rangos, por ejemplo habilitar sólo las IP de los proveedores de internet de España.

Otra opción es que el contenedor para administrar esté normalmente detenido y lanzarlo durante un tiempo máximo (1 hora) cuando el administrador quiera acceder. Para hacer esto, hay que tener algún tipo de sitio web o aplicación que permita al administrador encender la administración cada vez que la necesite.

Eligiendo imágenes del Docker Hub

En el Docker Hub hay montones de imágenes disponibles para cualquier cosa y a veces puede parecer difícil que elegir. Para complicarlo más, dentro de cada imagen hay montones de tags que apuntan a distintas variantes de imágenes. ¿Cual es mejor?

Primero que nada, ante la duda, elegid siempre imágenes oficiales. Si no, que sean certificadas, o de un autor verificado. Si tenéis que elegir otra que no cumple nada de eso, al menos que esté generada directamente desde GitHub, esto se puede comprobar porque la imagen tiene una pestaña llamada “Builds” y un bloque a la derecha llamado “Source Repository”:

Las imágenes en Docker Hub pueden contener código malicioso y arruinar la seguridad que estamos intentando agregar. Por lo tanto, sólo debemos usar las que sean de confianza. Las que no tienen ninguna garantía, pero son compiladas directamente desde GitHub al menos sabemos que no han sido manipuladas y cualquier código malicioso estará en GitHub también. No es garantía, pero hay menor probabilidad de que tenga un backdoor o algo por el estilo.

Sobre los tags, la regla de oro es jamás uséis tags que se llamen “-latest”, ya que se actualizan a la última versión cada vez y os puede romper los contenedores algún día.

Las imágenes pueden compartir diversas capas entre sí. Si dos imágenes heredan de la misma imagen Debian Stretch, ahorraréis espacio en disco ya que la imagen padre se comparte para ambas. No tiene demasiado sentido tener un contenedor Red Hat, otro Debian y otro Suse, ya que descargaríamos todos los sistemas operativos. Pero incluso si es el mismo sistema operativo y versión, si no es la misma imagen, provocará que se descargue también varias veces ya que sólo funciona si es la misma imagen padre.

Yo recomiendo usar las basadas en Debian Stretch, en su versión completa (porque también hay una versión lite). Esto hace que todos los paquetes que preinstala sean compartidos por todos los contenedores.

También hay tags que van directamente a la última versión mayor y otros especifican hasta la versión menor. Los que especifican la versión menor no se actualizan, y los que especifican la versión mayor se actualizan dentro de esa versión mayor. Lo mejor es quedarse en un término medio, donde podamos recibir actualizaciones menores, que normalmente son de seguridad, pero no traen nada nuevo y no rompen nada.

Por ejemplo:

  • Apache tag: 2.4
  • Nginx tag: 1.14
  • MySql tag: 8.0
  • PHP tag: 7.3-apache-stretch / 7.3-fpm-stretch
  • Python tag: 3.7-stretch
  • VSFTPD: Crear imagen nueva basada en debian:stretch
  • Exim4/Postfix: Crear imagen nueva basada en debian:stretch

Conclusiones

Agregar Docker para aislar los diferentes aspectos de un servidor LAMP no es sencillo, pero abre la puerta a muchas técnicas de mejoran seguridad que antes no eran factibles.

El montaje del servidor es más sencillo con Docker Compose, una herramienta que permite definir contenedores, su estado y interrelación de forma muy sencilla. Esto intentaré cubrirlo en un próximo artículo explicando la instalación paso a paso, aunque no podré cubrir la mayoría de las medidas aquí descritas ya que se hace demasiado extenso.

Qué es la programación Ágil, SCRUM y cómo empezar

Ya hace tiempo que quería dedicar un artículo a la programación Ágil. Llevo año y medio trabajando en Dublín y aquí todo es “Agile Programming”. Hoy voy a explicar a nivel muy básico qué es, qué beneficios trae y cómo ser un poco más ágil a la práctica, para los que no queráis cambiar la mecánica de trabajo por completo.

La programación Ágil en esencia son una serie de técnicas o de ideas para mejorar el desarrollo de software; cómo implementarlas depende de cada uno, aunque también hay algunas muy conocidas somo SCRUM o KanBan. El objetivo siempre es que se desarrolle más rápido, con menos errores, ser más adaptable a los cambios y sobretodo, mejorar la comunicación.

El método más conocido y usado es SCRUM, que aunque funciona tremendamente bien, igual es demasiado cambio para un equipo que no está familiarizado. Bajo mi corta experiencia, creo que hay ciertos aspectos de SCRUM que son mucho más importantes que otros, y al final si hay que empezar que sea por aquello que más efecto vaya a tener, ¿verdad?

Release Early, Release Often

Creo que este es el primer concepto que hay que poner sobre la mesa. La mayoría de equipos de desarrollo hoy en día, aunque no estén haciendo programación ágil ya siguen RERO.

La idea es la siguiente: Cuanto más tarda el código para estar en producción, más peligros hay de errores o cambios. Y es que la teoría es una cosa y la práctica es otra. Reduciendo al máximo posible el tiempo que una aplicación o una nueva feature tarda en desplegarse en producción se reducen los riesgos enormemente.

Otra forma de verlo es que si pasan muchos meses desde que se subieron cambios al cliente o se publicó la última versión, la siguiente vez va a costarnos mucho. Hacerlo a menudo implica que van a haber menos cambios entre versiones y que por tanto va a ser un proceso más ameno y menos doloroso.

A la práctica, las actualizaciones deberían enviarse entre cada dos semanas y dos meses, intentando preferir los periodos más cortos posibles.

Mejorar la Comunicación

Uno de los problemas más comunes es la ausencia de comunicación entre los miembros del equipo. Y como equipo aquí no nos referimos a desarrolladores, nos referimos a todo el equipo que hace el producto posible. Esto implica que administradores de sistemas, desarrolladores, analistas, consultores, comerciales e incluso gerencia deben comunicar entre sí lo mejor posible.

La forma en que SCRUM soluciona ésto es con un stand-up diario (o “plantadillo” si lo preferís en español) donde tanto desarrolladores como consultores se juntan para ponerse al día del estado de las tareas y para comunicar cualquier problema. Debe ser rápido, de unos diez minutos y de las primeras tareas de la mañana. Sin entrar en detalles técnicos dentro de lo posible.

Si hacer una reunión diaria de diez minutos os parece mucho, porque supone parar a mucha gente, una forma de evitarlo sería que todo el mundo genere un reporte vía email al jefe del equipo al final del día anterior que puede constar en una sola línea, describiendo cómo van las tareas, si los tiempos se van a cumplir y si cualquier cosa que crean que se debería discutir. El jefe del equipo junta todo en un solo correo a la mañana siguiente, que se reenvía a todo el mundo y en función del contenido determina si debe hacerse la reunión y la asistencia puede ser a los interesados.

La clave aquí es que todo el mundo debe estar al día de todos los aspectos. Los que tratan con el cliente deben tener una idea general de cómo les va a los programadores, y éstos últimos deben entender cuales son los problemas de los consultores y los comerciales. Hay que eliminar barreras. Dejar a la gente aislada es bastante malo.

Eficiencia

Todos estaremos de acuerdo en que el tiempo es dinero cuando hablamos de desarrollo de software o cualquier otro ámbito de servicios. Por lo que si se pierde tiempo se está perdiendo dinero.

En programación, la experiencia es que cambiar de tarea puede conllevar más de media hora perdida cambiando el foco a la nueva tarea. Minimizar la cantidad de reuniones que tiene el equipo de desarrollo es crucial, pero también encadenar las reuniones una detrás de otra. Hacer una reunión, luego una hora para programar y luego otra reunión es desperdiciar la hora completa de toda la gente.

Otros aspectos no tan evidentes son la multitarea y los cambios de prioridad de última hora. Si el que está programando tiene que estar a dos o tres cosas a la vez, no trabajará ni la mitad de rápido y será mucho más probable que cometa errores en alguna de las tareas que está realizando. Si alguien está haciendo una tarea concreta y se le pide que pare para cambiar a otra cosa y luego volver a lo que hacía, se va a perder más de una hora de trabajo por el camino además de que al volver a la tarea anterior puede que cometa errores al no recordar exactamente el análisis.

Una de las claves aquí está en tener una cola de tareas asignadas a cada uno, que debe ser siempre pequeña. La idea es que el programador sepa qué tarea puede empezar al terminar lo que tenía y al no tener muchas donde elegir, el trabajo va a salir de una forma mucho más predecible.

Para poder tener las urgencias y otras tareas menores funcionando sin tener que esperar demasiado lo que hay que hacer es dividir las tareas en subtareas mucho más pequeñas y finitas. En vez de “crear la funcionalidad X”, la tarea puede ser “crear los campos para la funcionalidad X”. Esto hace que el ámbito que tiene que cubrir el programador sea mucho más pequeño y pueda empezar antes. Al ser más pequeñas hay mayor frecuencia, por lo que entre una y otra puede saltar a urgencias o cualquier otra cosa. Las tareas, aunque pequeñas, deben tener sentido y poder ser subidas a producción sin que haya ningún problema; aunque no entreguen ningún valor de negocio.

Esto se puede mejorar con un tablero KanBan, a veces incluso si es físico donde la gente tiene que ir y cambiar los post-it de sitio puede que de un empujón de moral a la gente. Lo bueno de un tablero KanBan es que da una vista general de cómo va el trabajo del equipo.

El Backlog

Un concepto bastante común a todos los sistemas es la existencia del “backlog”, que no es otra cosa del listado de tareas pendientes de asignar.

Como ya comentaba antes, cada persona tiene asignada una lista de tareas muy pequeña para ir haciendo. ¿Qué pasa con el resto de tareas? Todo se tiene que registrar, por lo que en cuanto se sepa que hay algo que hacer se tiene que dar de alta el ítem en el sistema que uséis (Jira/Redmine/Asana/…).

Estas tareas, los consultores que son los que saben las prioridades para entregar cada cosa las ordenarán en el backlog por prioridad, para que luego sean movidas dentro del “tablero” según se van terminando las tareas anteriores. Normalmente quien se encarga de mover las tareas es el jefe del equipo o alguien particular designado para ello. Las tareas en el backlog pueden tener a alguien pre-asignado para saber quién es el mejor candidato para realizarlas pero lo normal es que no tengan a nadie, para que el primero que tenga capacidad la tome.

Los Sprints

Principalmente los Sprints vienen de SCRUM, la idea es dar un intervalo de tiempo en el cual una serie de tareas han de ser completadas. Las razones de hacer esto pueden ser muy diversas. Por ejemplo, se puede hacer un Sprint por release, de modo que sabemos de antemano qué es lo que vamos a publicar en la siguiente iteración. Otra posible razón es tener un punto de control cada poco tiempo para que si algo va mal no se nos vaya de las manos. Los sprints facilitan mucho ver cuando una tarea sea alarga más de lo normal porque agrega una fecha de entrega ficticia.

Para usar los Sprints es necesario saber cuantas tareas se pueden realizar en un período de tiempo. No es preciso saber con exactitud el tiempo de cada una, pues la incertidumbre es inherente al desarrollo de software.

Hay que medir de algún modo el tiempo estimado por tarea. El método más habitual para hacerlo es por “Story points” o puntos de tarea. Se trata simplemente de asignar un número que se corresponde con la complejidad de la tarea. Normalmente se usa la serie de fibonnacci, por lo que se puede asignar a una tarea 1, 2, 3, 5, 8 o 13 puntos; siendo 1 una tarea sencilla y 13 una tarea muy compleja que no sabemos cuando podrá estar lista.

A lo largo del tiempo la gente empieza a comparar las tareas de anteriores Sprints con las del nuevo, por lo que una tarea tendrá 3 puntos si parece igual de dificil que otra de 3 puntos que hicieron en el pasado.

En la práctica, hay tareas que tienen 1 punto y se tardan 5 minutos y otras con 1 punto que se tardan dos días. No es problema, al final como tienes 30 tareas dentro del Sprint estas discrepancias se compensan y la media se cumple. Por lo que si en el anterior Sprint se realizaron 30 puntos lo lógico es que el siguiente también sean más o menos 30. Con esto podemos determinar cuantas tareas hay que poner en un Sprint.

Otras reuniones útiles

Antes de finalizar, también quería comentar brevemente otras reuniones que se suelen hacer en SCRUM:

  • Sprint Planning: Antes de empezar el siguiente Sprint se reune el equipo para ver qué tareas se podrán cubrir en el intervalo de tiempo, se asignan a cada persona y si no están estimadas, se estiman en ese mismo momento. Es también un buen momento para revisar si la tarea está clara; y si no lo está se deja para el siguiente sprint hasta que todo esté bien atado.
  • Backlog Grooming: Normalmente a mitad del Sprint se reunen todos para revisar las tareas que hay en la parte de arriba del backlog que deben haberse priorizado previamente por los consultores. Es el momento para estimarlas y hacer preguntas. Las que no estén claras se marcan para que sean revisadas en los siguientes días.
  • Retrospectiva: Una reunión que sirve para preguntar regularmente a cada miembro del equipo qué ha ido bien, qué ha ido mal y en qué podemos mejorar. Es un buen momento para proponer ideas. Es importante para que la gente del equipo se sienta integrada y parte de él. Como resultado, se deben tomar una o dos ideas que nos parezcan tener más retorno y empezar a aplicarlas a ver qué sucede.
  • Post-mortems / Análisis de causas: Cuando algo imprevisto sucede, que no debería haber pasado, se suele hacer una reunión o al menos un esfuerzo por hallar cual ha sido la raíz del problema. Cuando se identifica hay que ver qué medidas se pueden tomar para reducir la probabilidad de que suceda de nuevo.

Con esto voy a terminar el artículo. Es muy básico y mi intención sólo es rascar la superficie de lo que es la programación ágil. Espero que le sirva a la gente que es completamente ajena al tema.

Si tenéis cualquier duda o sugerencia, no dudéis en ponerlo en los comentarios!

Introducción a Docker y Docker Compose

En paralelo a la serie de artículos que voy a publicar en Enero sobre cómo realizar un servidor LAMP seguro con Docker, quiero intentar explicar un a nivel básico cómo usar Docker y Docker Compose.

¿Qué es Docker?

Ya expliqué en otro artículo qué es Docker y sus ventajas: https://deavid.wordpress.com/2017/01/03/docker-y-los-contenedores/

A un nivel más técnico y abreviado, Docker es una interfaz de contenedores que por debajo usa LXC, la plataforma de contenedores para Linux. Un contenedor es un chroot mejorado. Las aplicaciones siguen ejecutándose en la misma máquina, pero usa todas las medidas de seguridad del kernel para aislarla. Está a medio camino entre una máquina virtual (Virtualbox/VMWare) y un chroot.

En Docker los contenedores son de aplicación, no de sistema operativo. El funcionamiento es que ejecutan una sola aplicación con los ficheros mínimos para ella y no corren diferentes servicios a la vez. LXC provee de contenedores de S.O. y se parece más a un VirtualBox porque cargamos un sistema operativo completo.

Seguridad aparte, la principal ventaja de Docker es que permite que nuestro equipo de desarrollo local pueda trabajar con el mismo sistema que el servidor; siendo idéntico en versiones de software y configuración. De esta forma se elimina el “en mi ordenador funciona” y las diferencias entre los equipos de desarrollo y el servidor se desvanecen.

¿Qué es Docker Compose?

Docker Compose es una aplicación sencilla para orquestrar contenedores Docker que está muy bien integrada con él.

La idea es que si queremos ejecutar una serie de servicios como diferentes contenedores donde tienen que interactuar entre sí, Docker Compose se encarga de tenerlos en marcha y de arreglar sus conexiones de red para que todo funcione de la forma que esperamos.

Por ejemplo, en un servidor LAMP tendríamos Apache, PHP y MySQL. Con Compose es sencillo levantar tres contenedores y comunicarlos entre sí. Además provee de una forma sencilla de arrancarlos cuando el equipo se inicia. También se encarga de reiniciar los contenedores que fallen.

Sobre Docker Hub

Docker tiene un sitio web llamado “Docker Hub” en https://hub.docker.com/. Esto es muy similar a GitHub, donde la gente puede de forma fácil contribuir y publicar sus imágenes.

Docker tiene aquí su propia cuenta y proporciona sus imágenes oficiales. Se puede identificar en la URL porque su nombre de cuenta es un guión bajo “_”. Por ejemplo, si buscamos Redis, el primer resultado es:

https://hub.docker.com/_/redis

Las imágenes ya hechas son muy útiles, porque crearlas lleva su tiempo y a veces podemos cometer errores. Pero hay que tener en cuenta que estamos descargando software de terceros, y como en cualquier cosa en internet, no debemos confiar en ejecutar lo primero que veamos.

Por eso mismo, las imágenes oficiales son unas de las de mayor confianza. Después también hay otras que son de autores verificados.

Otra de las cosas que hacen a Docker Hub un servicio excelente, es que de forma gratuita compilan imágenes a los usuarios que se registren, siempre y cuando el código esté en GitHub de forma pública. Esto hace que por el simple hecho de tener cuentas en los dos servicios tengamos una integración contínua. Podemos hacer “git push” y Docker Hub se encargará del resto.

Esta opción de compilación automática tiene un efecto colateral inesperado pero muy bueno: Mucha gente lo usa, por lo que hay garantías en esos casos de que la imagen viene exactamente del código fuente de GitHub; y éste se puede inspeccionar y verificar. Esto hace que las imágenes compiladas así tengan más garantías que las normales. Además nos permite hacer un fork de forma sencilla y personalizarlas. Aún así, ojo, que una imagen puede ser hoy buena y mañana puede que tenga una actualización con malware.

De todos modos la empresa de Docker hace un trabajo excelente para mantener el repositorio lo más seguro y limpo posible de malware.

¿Cómo funciona Docker?

Docker se instala vía “apt-get” sin demasiados problemas. En su misma web hay una guía de instalación para Debian y Ubuntu que recomienda sus repositorios propios. Esto es aconsejable si queremos la última versión de Docker. Como es un proyecto muy activo últimamente, tener la última versión es bastante recomendable.

https://docs.docker.com/install/linux/docker-ce/debian/

Una vez instalado, tened en cuenta que para usarlo necesitáis o ser root o pertenecer al grupo “docker”.

El comando más básico es:

docker run -it --rm nombre_imagen

Por ejemplo, si lo usamos con “danielkraic/asciiquarium”, podremos ver un bonito acuario en Ascii.

El comando “run” crea un contenedor con la imagen que le digamos. Los argumentos “-it” sirven para que cree una consola interactiva; en caso de que sea un servicio no hace falta. El argumento “–rm” borra el contenedor.

Este comando también descarga la imagen de Docker Hub si no la tenemos en nuestro sistema. Lo cual está muy bien y simplifica las cosas.

Si queremos ver qué contenedores están corriendo, usaremos “docker ps”, si queremos detenerlos “docker stop”. Para ver las imágenes que tenemos descargadas “docker images”.

Dockerfile

Con Docker, para crear imágenes usamos ficheros llamados “Dockerfile”, éstos llevan una serie de instrucciones de cómo generar la imagen. Empiezas por una imagen padre que ya exista, por ejemplo una básica de Debian, y haces que siga los pasos para instalar los paquetes que quieras, configurarlos y dejar el sistema a punto para lanzar la aplicación que quieras.

Docker, al ejecutar “docker build” ejecutará los pasos y lo guardará como una imagen que luego puedes usar para crear múltiples contenedores. Ésta imagen la puedes subir también a Docker Hub para que otros la usen.

Por ejemplo:

FROM ubuntu:15.04
RUN apt-get update
RUN apt-get install python-psycopg2 cython
RUN pip install cython
COPY . /app
CMD python /app/app.py

Con esto podemos incluir nuestra aplicación y sus dependencias en una imagen. Por defecto al crear un contenedor y arrancarlo, se ejecuta dento nuestra app.

Docker Compose

Aquí un ejemplo de cómo orquestrar una serie de contenedores podría ser:

version: '3'
services:
master-webserver:
image: nginx:1.14
volumes:
- ./master-webserver/sites-enabled:/etc/nginx/conf.d
- ./www:/var/www
ports:
- "85:85"
- "443:443"
restart: always
php-apache-1:
image: php:7.3-apache-stretch
volumes:
- ./www/php-apache-1:/var/www/html/

Esto se guarda como “docker-compose.yml” y al ejecutar “docker-compose up” lanza los dos contenedores a la vez, y los interconecta.

¿Qué os parece? ¿Interesante? En las próximas semanas iré publicando la serie de artículos que tengo sobre cómo crear un servidor LAMP ultra seguro, seguid atentos!

Infraestructura de desarrollo web con python

Feliz año nuevo! Empezamos el 2019 y he querido rescatar un artículo que tenía pendiente hace bastante. Cada vez más veo más gente pensando en hacer desarrollo web con Python en vez de con PHP.

¿Cual es la ventaja de Python sobre PHP? PHP nació anticuado y por mucho que intentan mejorarlo, las bases sobre las que se fomentan son arenas movedizas como en Javascript. Python es mucho más robusto y más seguro que PHP.

Pero las cosas claras, en cuanto a velocidad del lenguaje per-sé, Python es varias veces más lento que PHP. Y si ya lo era antes, ahora que PHP en su versión 7 ha mejorado la velocidad muchísimo, ciertamente Python se queda muy atrás en este tema.

Aunque ya he dicho un montón de veces, Python es rápido al final por el ecosistema en sí, si se sabe usar correctamente. Todos los llenguajes son lentos si se usan mal; por mucho que trabajes en C++, he visto programas en Python hacer lo mismo en menos tiempo, únicamente porque estaban mejor hechos. Y éste lenguaje facilita mucho hacer las cosas bien.

PHP por otro lado tiende a fallar, perder memoria por el camino y la plataforma que tiene para funcionar vía web hace que tenga un coste significativo sólo lanzar el programa. Como programador web os puede gustar (a gustos colores), pero como Dev-Ops o Administrador de Sistemas sólo puedes odiarlo. Es bastante coñazo de mantener un sistema estable en un servidor web con varias páginas complejas.

Si en este año nuevo os estáis planteando probar Python para web, os comento por donde empezar:

uWSGI

Python se conecta al servidor web (Apache, Nginx, etc) de muchas formas. Por defecto la mayoría de frameworks levantan un mini servidor web que podéis conectar vía Proxy HTTP. Pero esto sólo es recomendable para sistemas de desarrollo en local.

En servidores tenemos FastCGI y uWSGI. El segundo es más nuevo, fácil de configurar y más rápido. Por ejemplo en Apache tenéis mod_uwsgi y es sencillo:

ProxyPass /foo uwsgi://127.0.0.1:3032/

Además Python no es como PHP en cuanto a la ejecución de los ficheros. Los programas Python no se dejan en la misma carpeta donde están las imágenes. En nuestro caso hay una clara separación entre lo que son programas y datos. Si vienes de PHP puede parecer un engorro comparado con dejar el fichero x.php donde te plazca, pero en cuanto a seguridad es muchísimo mejor; no hay ninguna posibilidad de que el código fuente sea visible desde la web por accidente y ficheros subidos a la web no se pueden ejecutar nunca.

Comparad esto con PHP, donde a veces basta con subir un adjunto con extensión “.php” y luego ir a la URL.

Flask y Django

Si váis a empezar con Python lo mejor es que uséis un Framework donde ya venga todo. Si os gustan los frameworks y/o empezar desde una base que te guíe, Django es ideal. Si prefieres algo más parecido al PHP en crudo, Flask es muy minimalista y te deja trabajar a tu gusto. Se puede bajar aún más i ir a Werkzeug, pero habiendo probado los tres creo que Flask es la mejor recomendación.

Django está bien si quieres una estructura y poder instalar módulos de terceros que hay un montón. Pero si eres organizado y puedes crear cosas rápido, Flask es flexible y más rápido que Django.

Además estos proyectos tienen todos documentación de cómo conectarlos con servidores web correctamente, cuales son los estándares de seguridad a seguir, etcétera.

PostgreSQL

Para base de datos, he trabajado un montón con MySQL, otro tanto con PostgreSQL y también con SQLServer.

SQLServer no lo recomiendo para nada. MySQL es la solución típica para webs, pero lo único realmente bueno que tiene es una caché integrada en la base de datos. PostgreSQL supera a ambas y no por poco. Es excelente, llena de funcionalidades y robusta a más no poder. La única pega para web es que no tiene una caché integrada, por lo que si lanzas la misma consulta 40 veces, se ejecutará 40 veces. Lo que hay que hacer es cachear nosotros lo que nos interese para evitar esto

Las bases de datos NoSQL no hace falta ni que las miréis. Las pocas ventajas que puedan tener sobre una SQL, o son funcionalidades que puedes tener en PostgreSQL, o no le sacas partido porque no tienes suficiente cantidad de datos para que valga la pena. En mis pruebas PostgreSQL era más del doble de rápido que MongoDB (la más rápida) bajo las mismas condiciones. Y luego las NoSQL no son ni consistentes ni confiables, parte de su rapidez viene en que pueden perder datos.

Sqlalchemy

A la hora de acceder a las bases de datos, los ORM son muy recomendables porque su abstracción permite que un mismo código pueda trabajar con los datos sin saber de dónde vienen. Además simplifican mucho el desarrollo y mejoran la lectura.

La pega de los ORM (de todos) es que agregan una carga extra al procesar los datos. Después de revisar y probar SqlAlchemy me pareció que daba un buen resultado. Comparado con el ORM de Django, tiene más funcionalidades y es más rápido.

Hay otros (menos funcionales), pero como no los he podido probar no sé decir si son más rápidos.

Nginx

Evitad usar Apache, y si lo usáis al menos que no sea en Prefork. Nginx es mi favorito como Sysadmin ya que no da dolores de cabeza, funciona, tiene lo que necesito y es extremadamente rápido. Los servidores con Nginx siempre me han funcionado extremadamente bien.

Redis

Que queréis un servidor NoSQL, pues tenéis Redis. Que necesitáis una caché, pues Redis. Redis es ideal para datos temporales, de alta frecuencia de acceso y escritura. La pega es que en seguridad va corto, como todos los servidores de este tipo. Así que si lo usáis, procurad que sólo tengan acceso los programas que deben tenerlo.

Redis se puede configurar con casi todos los frameworks, lenguajes de programación o incluso muchos CMS. Y además soporta clústering y sharding! ¿Qué más podéis pedir?

Ansible

En el último año he trabajado con Ansible, que es una herramienta para orquestrar servidores. Aunque es un poco costoso de empezar con ella, lo bueno es que da resultados bastante reproducibles, con la facilidad de que lo que va en una máquina tiende a ir en las demás. Y las instrucciones de instalación se os quedan guardadas en Git, por lo que instalar los siguientes servidores es más fácil.

Además que, el poder desplegar una actualización a todos los equipos en un sólo comando es una pasada. Cuando tienes 50 equipos que necesitan ser actualizados, o instalar un nuevo programa, Ansible consigue que parezca trivial.

Docker

Pero si queremos ir a la reproducibilidad máxima, a poder trabajar exactamente igual en local que en el servidor, Docker es aún mejor que Ansible. Aunque ambos se pueden usar a la vez y complementarse.

Docker además permite tener distintas aplicaciones aisladas entre sí de forma que una aplicación comprometida no pone en riesgo el servidor o las otras aplicaciones.

En las próximas semanas, cada jueves, iré publicando una serie de artículos sobre cómo usar Docker para hacer un servidor LAMP ultra-seguro. Estad atentos!