Un blog mas

Bitácora de vuelo

Una tarea que deben resolver de manera eficiente los servidores web es como manejar los procesos ante un request, algo de lo que se debe encargar específicamente el módulo MPM (Multi-Processing Module) de Apache. Si observamos en la lista de módulos disponibles vamos a encontrar tres diferentes MPMs: Pre-fork, Worker y Event

Nosotros nos enfocaremos en el módulo MPM-Event que maneja la administración de un proceso principal que administra las tareas repartidas en un pool de sub-procesos y threads asociados a ellos, manteniendo la conexión abierta por 5 segundos (valor por default) y cerrandola si no existen nuevos eventos para estar listo para recibir nuevas conexiones entrantes.

MPM Event (Multi-Processing Module Event)

Deshabilitar mpm_prefork y habilitar mpm_event

systemctl stop apache2
a2dismod php8.1
a2dismod mpm_prefork
a2enmod mpm_event
apt install libapache2-mod-fcgid
a2enconf php8.1-fpm
a2enmod proxy_fcgi
systemctl start apache2

Configurar MPM-Event en /etc/apache2/mods-available/mpm_event.conf

<IfModule mpm_event_module>
       StartServers            4
       MinSpareThreads         25
       MaxSpareThreads         75
       ThreadLimit             64
       ThreadsPerChild         25
       MaxRequestWorkers       128
       MaxConnectionsPerChild  0
</IfModule>

En este link se puede descargar un script para obtener una configuración inicial de los parámetros

https://gist.github.com/Josh5/ff6ccfe4c75ae27a3f1efebcb645e7c4

PHP-FPM (FastCGI Process Manager)

PHP- FMP es el administrador de procesos FastCGI para PHP basado en Common Gateway Interface (CGI), un protocolo que trabaja entre la aplicación PHP y Apache y permite aislar a los desarrolladores del comportamiento del servidor web. Los programas se ejecutan en procesos independientes y comunican el resultado al servidor web a través de este protocolo. Por cada nueva conexión que necesite ser procesada se creará un nuevo proceso.

Configurar PHP FPM en /etc/php/8.1/fpm/pool.d/site1.conf

[site1]  
user = usuario-site1  
group = grupo-site1
listen.owner = www-data
listen.group = www-data
listen = /run/php/php8.1-fpm-site1.sock  
pm = dynamic
pm.max_children = 25
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 8
pm.status_path = /status
pm.status_listen = localhost:9001
php_admin_value[open_basedir] = /var/www/html/site1/htdocs/

En este link se puede obtener una aproximación inicial de como configurar los parámetros de pm iniciales.

https://spot13.com/pmcalculator/

Configurar PHP FPM en Apache

<FilesMatch .php$>
     SetHandler "proxy:unix:/run/php/php8.1-fpm-site1.sock|fcgi://localhost/"
</FilesMatch>

<LocationMatch "/status">
     Order Allow,Deny
     Allow from 127.0.0.1
     ProxyPass fcgi://localhost:9001/status
</LocationMatch>

Combinando MPM en Apache con PHP FastCGI Process Manager el sitio web se cargará mas rápido y podrá manejar las conexiones simultáneas utilizando menos recursos. A su vez, en servidores web multi-sitio, MPM permite definir una configuración independiente para cada uno de los sitios que agrega una capa de seguridad extra en donde cada proceso se ejecutará con un usuario diferente.

Chequear los módulos de Apache cargados

apachectl -M

Chequear si PHP está utilizando PHP-FPM

echo "<?php phpinfo(); ?>" > /var/www/site1/info.php

y desde un browser acceder a http://sitio1/info.php

Chequear el status de FPM desde consola

SCRIPT_NAME=/status 
SCRIPT_FILENAME=/status 
REQUEST_METHOD=GET 
cgi-fcgi -bind -connect localhost:9001/status
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Content-type: text/plain;charset=UTF-8

pool:                 site1
process manager:      dynamic
start time:           12/Feb/2023:10:39:18 +0000
start since:          56108
accepted conn:        3354
listen queue:         0
max listen queue:     0
listen queue len:     0
idle processes:       73
active processes:     0
total processes:      73
max active processes: 48
max children reached: 0
slow requests:        0

Con este procedimiento se modificará la pila LAMP original para que Apache atienda una mayor cantidad de conexiones y el código PHP sea manejado de manera mas eficiente por FPM. Se recomienda consultar la documentación del proyecto Apache HTTP para obtener más información sobre los diferentes módulos y proyectos relacionados.

Para verificar una conexión con TLS/SSL publicada con un certificado de servidor firmado por una CA interna se debe incorporar el certificado de esta CA en el parámetro -CAfile

pablo@notebook:~$ echo -e "GET /index.html HTTP/1.1\r\nHost:$(hostname)\r\n\r\n" | openssl s_client -connect vm-linux.local:443 -CAfile ca.pem -quiet 

depth=1 C = AR, ST = CABA, L = CABA, O = Mi Empresa SA, OU = IT, emailAddress = admin@miDominio.com.ar, CN = miDominio-CA
verify return:1
depth=0 C = AR, ST = CABA, L = CABA, O = Mi Organizacion SA, OU = IT, emailAddress = admin@miorganizacion.com.ar, CN = vm-linux.local
verify return:1
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)

Si no definimos dicho parámetro con la cadena certificante obtendríamos un 200 OK con los siguientes errores

pablo@notebook:~$ echo -e "GET /index.html HTTP/1.1\r\nHost:$(hostname)\r\n\r\n" | openssl s_client -connect vm-linux.local:443  -quiet 

depth=0 C = AR, ST = CABA, L = CABA, O = Mi Organizacion SA, OU = IT, emailAddress = admin@miorganizacion.com.ar, CN = vm-linux.local
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 C = AR, ST = CABA, L = CABA, O = Mi Organizacion SA, OU = IT, emailAddress = admin@miorganizacion.com.ar, CN = vm-linux.local
verify error:num=21:unable to verify the first certificate
verify return:1
HTTP/1.1 200 OK

Ahora el mismo ejemplo pero con CURL

pablo@notebook:~$ curl --cacert ca.pem  https://vm-linux.local:443

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

y el error correspondiente si no enviamos la cadena en el parámetro –cacert

pablo@notebook:~$ curl https://vm-linux.local:443

curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Ahora verificamos una conexión incorporando autenticación con mutual SSL (certificado del lado del cliente). Para ello se debe definir el certificado de cliente en el parámetro -cert y la clave privada del cliente en el parámetro -key

pablo@notebook:~$ echo -e "GET /index.html HTTP/1.1\r\nHost:$(hostname)\r\n\r\n" | openssl s_client -connect vm-linux.local:443 -cert cliente1.pem -key cliente1.key -CAfile ca.pem -quiet 

depth=1 C = AR, ST = CABA, L = CABA, O = Mi Empresa SA, OU = IT, emailAddress = admin@miDominio.com.ar, CN = miDominio-CA
verify return:1
depth=0 C = AR, ST = CABA, L = CABA, O = Mi Organizacion SA, OU = IT, emailAddress = admin@miorganizacion.com.ar, CN = vm-linux.local
verify return:1
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 24 Jun 2022 00:00:26 GMT
Content-Type: text/html
Content-Length: 612
Last-Modified: Sat, 18 Jun 2022 03:00:21 GMT
Connection: keep-alive
ETag: "62ad3fc5-264"
Accept-Ranges: bytes

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Si no se envían estos parámetros el error de autenticación se vería de la siguiente manera

pablo@notebook:~$ echo -e "GET /index.html HTTP/1.1\r\nHost:$(hostname)\r\n\r\n" | openssl s_client -connect vm-linux.local:443  -CAfile ca.pem -quiet 

depth=1 C = AR, ST = CABA, L = CABA, O = Mi Empresa SA, OU = IT, emailAddress = admin@miDominio.com.ar, CN = miDominio-CA
verify return:1
depth=0 C = AR, ST = CABA, L = CABA, O = Mi Organizacion SA, OU = IT, emailAddress = admin@miorganizacion.com.ar, CN = vm-linux.local
verify return:1
HTTP/1.1 400 Bad Request
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 24 Jun 2022 00:05:51 GMT
Content-Type: text/html
Content-Length: 246
Connection: close

<html>
<head><title>400 No required SSL certificate was sent</title></head>

Mismo caso pero con CURL, definiendo el certificado de cliente en el parámetro –cert y la clave privada en el parámetro –key

pablo@notebook:~$ curl --cacert ca.pem --cert cliente1.pem --key cliente1.key https://vm-linux.local:443

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Y el siguiente es el resultado que obtendríamos si se intenta acceder a una publicación con MUTUAL sin los certificados correspondientes

pablo@notebook:~$ curl --cacert ca.pem https://vm-linux.local:443

<html>
<head><title>400 No required SSL certificate was sent</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<center>No required SSL certificate was sent</center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

ca.pem contiene la cadena certificadora asociada a los certificados de servidor con los que se publican los servicios web.

cliente1.key es la clave privada generada por el cliente que se quiere autenticar al servicio publicado con Mutual SSL.

cliente1.pem es un certificado firmado por la CA definida en el servidor para autenticar a los clientes.

TIP: Verificar que el CSR y el certificado firmado por la CA pertenecen a la misma clave privada comparando los hash SHA256 de cada uno

pablo@notebook:~$ openssl pkey -in cliente1.key -pubout -outform pem | sha256sum
528c0019e27100d2879759fdb3072951e555f1ef9a1c61f98e3dca747c092eed  -
pablo@notebook:~$ openssl x509 -in cliente1.pem -pubkey -noout -outform pem | sha256sum 
528c0019e27100d2879759fdb3072951e555f1ef9a1c61f98e3dca747c092eed  -
pablo@notebook:~$ openssl req -in cliente1.csr -pubkey -noout -outform pem | sha256sum
528c0019e27100d2879759fdb3072951e555f1ef9a1c61f98e3dca747c092eed  -

Es necesario contar con la aplicación Google Authenticator en el celular para obtener el segundo factor. Esta aplicación esta disponible en los repos de IOS y Android.

El primer paso es instalar el módulo de Google Authenticator de PAM

sudo apt-get install libpam-google-authenticator

Modificar la autenticación del servicio de SSH en PAM para requerir el 2FA editando el archivo /etc/pam.d/sshd agregando la línea auth required pam_google_authenticator.so luego del common-auth.

# PAM configuration for the Secure Shell service
#auth required pam_google_authenticator.so 
# Standard Un*x authentication.
@include common-auth

auth required pam_google_authenticator.so

# Disallow non-root logins when /etc/nologin exists.
account    required     pam_nologin.so
.....

Cambiar a yes el parámetro KbdInteractiveAuthentication en el archivo de configuración del servicio de SSH para solicitar el 2FA, editando el archivo /etc/ssh/sshd_config

# Change to yes to enable challenge-response passwords (beware issues with
# some PAM modules and threads)
KbdInteractiveAuthentication yes

# Kerberos options

y reiniciar el servicio de SSH

sudo service ssh restart

Ahora debemos generar la clave para el 2FA ejecutando con nuestro usuario el comando google-authenticator . Durante el proceso de configuración se generará un código QR que debemos escanear con la aplicación Google Authenticator instalada en nuestro celular para obtener el TOTP y completar la instalación (luego de varios yes como respuesta).

Para finalizar solo resta probar el acceso en donde se nos solicitará la clave de usuario y el código de un solo uso generado por la aplicación.

pablo@pc:~$ ssh pablo@vm-linux
Password: ************
Verification code: 123 456

Welcome to Ubuntu 22.04 LTS (GNU/Linux 5.15.0-37-generic x86_64)

pablo@vm-linux:~$ 

FluentD es un reenviador de datos rápido y ligero el cual se puede configurar como complemento de salida de Loki para enviar los registros «interesantes» y poder monitorearlos con Grafana. A continuación el docker-compose para correr estos tres componentes con docker.

version: "3"

volumes:
  grafana-data:
  loki-data:

networks:
  lokinet:

Se definen la versión del docker file, volúmenes permanentes y red

  loki:
    image: grafana/loki:latest
    volumes:
     - ./conf/loki-config.yaml:/etc/config/loki.yaml
    command: 
      - -config.file=/etc/config/loki.yaml
      - loki-data:/loki
    networks:
      - lokinet

El contenedor Loki se genera a partir de la imagen oficial de Grafana. Se define el archivo de configuración y el volumen permanente donde se almacenarán los datos. El archivo de configuración ejemplo se puede descargar del siguiente link: loki-config.yaml

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana-data:/var/lib/grafana
      - ./conf/grafana.ini:/etc/grafana/grafana.ini
    ports:
      - 3000:3000
    networks:
      - lokinet

El contenedor de Grafana se genera a partir de la imagen oficial de Grafana. Se definen el volumen permanente donde se almacenan los datos de Grafana y el archivo grafana.ini donde se pueden agregar los parámetros de configuración que sobre escriben a la configuración default. Se expone el puerto 3000 al Host para poder accederlo desde un browser. Un archivo de configuración de Grafana ejemplo se puede dercargar desde aquí: grafana.ini

  fluentd:
    image: grafana/fluent-plugin-loki:master
    command:
      - "fluentd"
      - "-v"
      - "-p"
      - "/fluentd/plugins"
    volumes:
      - "./conf/fluent.conf:/fluentd/etc/fluent.conf:ro"
    environment:
      LOKI_URL: http://loki:3100
      LOKI_USERNAME:
      LOKI_PASSWORD:
    ports:
      - "24224:24224"
      - "24224:24224/udp"
    networks:
      - lokinet

El contenedor de Fluent se genera a partir de la imagen oficial, pero no de FluentD sino de Grafana, ya que contiene el plugin para conectar a Loki. Se define el volumen permanente al archivo de configuración de Fluent y definen los parametros de acceso al loki. Como está dentro del mismo grupo de contenedores el acceso se logra sin la necesidad de publicar el puerto 3100 del Loki al Host. Sí es necesario exponer los puertos 24224tcp/udp al Host para publicar el servicio de forward del Fluent, esperando logs de otros Hosts que serán reenviados al Loki. Un archivo de configuración de ejemplo de Fluent se puede descargar desde este link: fluent.conf

Resta acceder al Grafana y activar el Data Sources Loki como se ve en la siguiente captura.

El docker-compoose.yaml completo se puede dercargar del siguiente link: https://gitlab.com/pablosarubbi/grafanalokifluentddocker-compose/-/blob/main/docker-compose.yaml

Poste.io es una imagen para implementar un servidor de correo completo, con todas las funciones necesarias, como ser SMTP, IMAP, POP3, webmail, antivirus, antispam, y un panel de administración muy completo desde donde se pueden agregar multiples dominios, cuantas de correo, certificados, y modificar la configuración de cada uno de los componentes.

El único requisito para su instalación es tener Docker corriendo sobre una máquina con 1GB de RAM y una IP pública la cual asignaremos en nuestro DNS a mail.sarubbi.com.ar

pablo@mail:~# sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
pablo@mail:~# sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
pablo@mail:~# sudo apt update
pablo@mail:~# sudo apt install docker.io docker-compose

Es necesario crear un archivo docker-compose.yaml con los siguientes parámetros:

pablo@mail:~# cat /root/docker-compose.yaml 
version: "3.4"
services:
  poste:
    image: analogic/poste.io
    restart: always
    network_mode: "host"
    expose:
      - 25
      - 80
      - 443
      - 110
      - 143
      - 465
      - 587
      - 993
      - 995
    volumes:
      - /mnt/mail:/data
    environment:
      - HTTPS=ON
      - DISABLE_CLAMAV=TRUE

restart: always para que se inicie el contenedor cada vez que la máquina inicie.

netkwork_mode: «host» hace que el contenedor use la IP pública de la máquina y no una IP interna de Docker.

expose: – <port> para publicar todos los puertos que necesitamos.

volumes : – <dir> para almacenar lo archivos de configuración y toda la información de los mails.

Para luego correr la imágen de la siguiente manera:

pablo@mail:~# sudo docker-compose -f /root/docker-compose.yaml up -d

Con docke ps podemos verificar que el contenedor este corriendo

pablo@mail:~# sudo docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                  PORTS               NAMES
b2ab960cdc6c        analogic/poste.io   "/init"             5 seconds ago          Up 5 seconds (healthy)                       root_poste_1

Para terminar la configuración debermos acceder desde unn browser a https://mail.sarubbi.com.ar/

La primera vez fallará el HTTPS porque aún no tenemos un certificado definido. Continuamos aunque diga que no es seguro (porque sabemos que es seguro no?)

Completamos los datos:

Mailserver hostname: mail.sarubbi.com.ar

Administrator email: administrador@sarubbi.com.ar

Password: ******************

Para crear un certificado con letsencrypt accedemos al menú «System settings» -> «TLS Certificate» -> «issue free letsencrypt.org certificate» -> «enabled» -> «Save changes»

Automáticamente se conectará a letsencrypt.org, generará el certificado y lo instalará en nuestro contenedor.

Algo para destacar es revisar el panel de «Server status» -> «Connection diagnostics» desde donde podemos verificar que tengamos todos los servicios funcionando.

Gracias tocayo por la data: https://youtu.be/K4-uD1VHCz0

Stop SOPA