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.
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.
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.