En este post tratamos de implementar una forma avanzada de proxy inverso en docker. Nos centramos en la parte que afecta a la configuración de un WAF o Web Application Firewall que permita aumentar la seguridad de nuestros servicios montados en docker afectando lo menos posible a los servicios que ya hay en marcha en nuestro sistema de docker.

Proxy inverso con docker

Existen herramientas como nginx-proxy que nos permiten, de una forma muy sencilla, exponer servicios en contenedores vía HTTPS sin necesidad de configurar manualmente cada servicio. nginx-proxy se configura automáticamente en base a los servicios que hay corriendo en ese momento en docker.

La imagen acme-companion trabaja junto nginx-proxy para generar los certificados SSL y asociarlos a la configuración del proxy inverso.

WAF

Los logs del sistema de docker se vuelcan a Graylog (tengo más información en este post previo) y he estado observando peticiones sospechosas, muy seguramente de bots que intentan localizar agujeros de seguridad que poder explotar.

Suspicious requests
TEST

Concretamente estas peticiones pueden no suponer un riesgo inmediato, pero son un indicador de lo expuestos que se encuentran unos servicios que escuchan en nuestro puerto 443.

Por ello empecé a estudiar la posibilidad de implantar un WAF o Web Application Firewall. Este tipo de firewalls filtran peticiones a nivel de capa 7 del modelo OSI para prevenir que atacantes exploten vulnerabilidades conocidas, inyección de comandos, etc.

Fue cuando encontré ModSecurity, una herramienta Open Source integrable en nginx (además de Apache o IIS). Esta herramienta se “configura” mediante reglas (SecRules).

Proxy inverso + docker + WAF

Hemos cambiado la configuración que ya tenía en docker con nginx-proxy + acme-companion por nginx (con ModSecurity) + docker-gen + acme-companion intentando afectar en lo menos posible el gigantesco docker-compose que ya tengo.

Para eso he empezado separando el actual nginx-proxy en dos contenedores: nginx, usando la imagen owasp/modsecurity:3-nginx y docker-gen.

version: "2.4"
services:
  nginx-proxy:
    image: owasp/modsecurity:3-nginx
    container_name: nginx-proxy
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - certs:/etc/nginx/certs:ro
      - ./proxy/network_internal.conf:/etc/nginx/network_internal.conf
      - ./proxy/modsecurity:/etc/modsecurity
      - ./proxy/modsecurity.d/include.conf:/etc/modsecurity.d/include.conf
      - ./proxy/modsecurity.d/modsecurity.conf:/etc/modsecurity.d/modsecurity.conf
    
  docker-gen:
    image: jwilder/docker-gen
    command: -notify-sighup nginx-proxy -watch /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf
    container_name: docker-gen
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - certs:/etc/nginx/certs:ro
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./proxy/nginx.tmpl:/etc/docker-gen/templates/nginx.tmpl
    labels:
      - "com.github.jrcs.letsencrypt_nginx_proxy_companion.docker_gen"
    
  acme-companion:
    image: nginxproxy/acme-companion:latest
    container_name: acme-companion
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - certs:/etc/nginx/certs
      - acme:/etc/acme.sh
      - /var/run/docker.sock:/var/run/docker.sock:ro
    environment:
      - NGINX_DOCKER_GEN_CONTAINER=docker-gen
      - NGINX_PROXY_CONTAINER=nginx-proxy

  # Resto de servicios...

volumes:
  conf:
  vhost:
  html:
  dhparam:
  certs:
  acme:

Estos tres contenedores trabajan en “tandem”. El contenedores dockergen genera el fichero de configuración todas las rutas de nuestros contenedores accesibles desde el exterior. letsencrypt generará los certificados para todos los servicios.

En este post nos vamos a centrar en la configuración de ModSecurity. Para configurar docker-gen y acme-companion y el contenedor de nginx hay varios ejemplos que puedes usar, seguramente más elegantes y limpios que el ejemplo que estoy usando yo. (Ejemplos).

Configuración contenedor de docker

La configuración que afecta a ModSecurity se incluye en el directorio ./proxy/modsecurity de mi proyecto, que contiene esta estructura de ficheros.

Folder structure

Dockergen

Dockergen es en nuestro sistema el encargado de generar el fichero default.conf que contiene la configuración del reverse proxy para enrutar las peticiones a los contenedores.

Se basa en una plantilla nginx.tmpl que hemos extraído del repositorio original de nginx-proxy.

El fichero network_internal.conf es usado por la plantilla para definir las direcciones IP internas para restringir, si así se define, el acceso a algunos contenedores desde equipos de la misma red privada link.

Reglas CRS

La imagen owasp/modsecurity:3-nginx no incluye reglas (SecRules), por eso las incorporamos mediante un submódulo el repositorio coreruleset en el directorio ./proxy/modsecurity/crs. OWASP ModSecurity Core Rule Set incluye un set de reglas genéricas que podemos usar con ModSecurity.

En este directorio ./proxy/modsecurity definimos también la configuración de OWASP ModSecurity Core Rule Set (crs-setup.conf, ejemplo aquí) y también excepciones a las reglas que hemos importado de coreruleset en dos ficheros.

Los ficheros RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf y REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf permiten establecer excepciones y modificar el comportamiento de las reglas que hemos importado, sin necesidad de editarlas directamente. En el repositorio se incluye ejemplos de estos dos ficheros que incluyen documentación que indican como deben de usarse ambos ficheros.

Este es un ejemplo del fichero REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf, donde establecemos unas variables que serán usadas durante la ejecución del resto de reglas.

# Enable nextcloud in nextcloud.mydomain.com
# Only apply in nextcloud.domain.com, not in other services
SecRule REQUEST_HEADERS:Host "@streq nextcloud.mydomain.com" "id:900130,phase:1,nolog,pass,t:none,setvar:tx.crs_exclusions_nextcloud=1"

# Enable PUT method
# Apply to all services
SecAction \
 "id:900200,\
  phase:1,\
  nolog,\
  pass,\
  t:none,\
  setvar:'tx.allowed_methods=GET HEAD POST OPTIONS PUT'"

Configuración ModSecurity

La configuración de ModSecurity las he definido en el directorio ./proxy/modsecurity.d.

El fichero include.conf referencia todos los ficheros de configuración que hemos definido, incluido el conjunto de reglas que hemos importado del proyecto coreruleset.

# Include the recommended configuration
Include /etc/modsecurity.d/modsecurity.conf
# OWASP CRS v3 rules
Include /etc/modsecurity/crs-setup.conf
Include /etc/modsecurity/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
Include /etc/modsecurity/crs/rules/*.conf
Include /etc/modsecurity/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

Para el fichero modsecurity.conf he partido del fichero de ejemplo en el repositorio. Muy posiblemente necesites modificar algunos parámetros para ajustar su funcionamiento. Incluyo aquí un extracto de algunos parámetros que he tenido que modificar.

SecRuleEngine DetectionOnly
#...

# Aumentamos el tamaño máximo de fichero en un POST
SecRequestBodyLimit 5000000

# ...

# Mejora la gestión de los logs. En vez de dejarlos en un fichero local al contenedor,
# se vuelcan en /dev/stdout, de forma que podems analizarlos en graylog
SecAuditLogType Serial
SecAuditLogFormat JSON
SecAuditLog /dev/stdout

#....

El parámetro más importante es el primero que se muestra: SecRuleEngine. Este parámetro establece el modo de funcionamiento de ModSecurity. Usando SecRuleEngine DetectionOnly solo se detectan y generan logs, pero no cancela ninguna petición. Es ideal para empezar a configurar el servicio e ir ajustándolo antes de empezar a restringir peticiones.

Testear la configuración

Arrancamos el servicio y comenzamos a analizar los logs generados por ModSecurity. Al estar en modo DetectionOnly los servicios no se ven afectados y podemos empezar a depurar si es necesario ajusta alguna regla por un falso positivo.

Después de varios días que veamos que ninguna petición legítima es interceptada por ModSecurity cambiamos a SecRuleEngine On.

Conclusiones

Esta es una configuración muy básica de ModSecurity con las reglas básicas. Es necesario pulir la configuración inicial para evitar falsos positivos y de esta forma que el servicio funcione correctamente.

Esta base puede servir para añadir nuevos filtros y adaptarlo a nuestras necesidades, como protección contra fuerza bruta, configuración específica para servicios concretos, etc.

Referencias