Ferm, poderoso cortafuegos (parte 3)

Este artículo continúa la serie sobre ferm, poderoso cortafuegos para GNU/Linux. En el artículo anterior, mostré algunos ejemplos de cómo estructurar la configuración de un cortafuegos mediante ferm. En esta ocasión mostraré cómo realizar una lista automática de bloqueo temporal, además de ejemplificar otros casos de uso.

Listas automáticas de bloqueo

Una forma de lidiar con los equipos remotos que envíen paquetes maliciosos a nuestra red, es monitorear las trazas e ir agregando manualmente las direcciones IP de origen frecuentemente rechazadas a una lista de bloqueo (también conocida como lista negra) de manera que todos los futuros paquetes provenientes de direcciones IP contenidos en dicha lista sean rechazados, hasta que las direcciones sean eliminadas de la lista.

Un problema es que realizar esto manualmente es tedioso para un administrador de redes, porque obliga a dedicar tiempo y atención a una tarea repetitiva, cuando bien podría hacerlo un programa. Y es justamente ese el propósito de proyectos como fail2ban.

Sobre fail2ban

Muchos conocen la existencia de fail2ban, una herramienta indudablemente útil como apoyo al administrador de redes, asumiendo que uno se tome el tiempo de estudiar su funcionamiento.

En este artículo no pretendo mostrar como usar fail2ban, pero muchos instalan y usan esta herramienta con poca o ninguna modificación a la configuración predeterminada, sin detenerse a pensar si lo que dejan en el sistema es realmente lo más eficiente para el caso de uso que tienen, de manera que me gustaría alertar sobre las implicaciones de depender excesivamente de fail2ban.

Comenzaré explicando brevemente el principio de funcionamiento de fail2ban: se evalúan varios logs (trazas) mediante expresiones regulares definidas en los filtros de la configuración, para extraer las direcciones IP de origen relacionadas con intentos potencialmente maliciosos.

Entonces, se agregan al kernel reglas mediante iptables que rechacen las conexiones provenientes de dichas direcciones. Estas reglas permanencen por el tiempo de vida definido en el jail (jaula, bloques de configuración relativos a un servicio), hasta que finalmente se ejecuta otro comando de iptables para eliminar dichas reglas.

Esto indudablemente tiene muchas aplicaciones útiles, pero también tiene inconvenientes.

En primer lugar, leer constantemente las trazas consume recursos de procesamiento, que aunque sean bajos se acumulan con el tiempo, a medida que las trazas crecen. Aunque fail2ban incluya mecanismos para leer las trazas sólo cuando estas cambien y en tal caso comenzar a leer sólo desde la posición de última lectura, puede que en una red muy activa o que reciba muchos ataques, las trazas de un día alcancen varios cientos de megabytes (incluso configurando logrotate para que se roten y compriman las trazas diariamente).

En segundo lugar, la evaluación de expresiones regulares consume recursos de procesamiento, y dado que fail2ban no sabe de antemano en qué lugar de las trazas encontrar los casos conflictivos, tiene que evaluarlas en su totalidad.

En tercer lugar, fail2ban no cubre por defecto todos los posibles casos de actividad maliciosa, solo algunos de los más frecuentes. Por ejemplo, fail2ban no impedirá con su configuración predeterminada que un equipo remoto realice un portscan (exploración de puertos) a nuestra red.

De modo que conviene concebir las reglas del cortafuegos de manera que cubran una cantidad razonable de casos para que fail2ban se aplique solamente cuando sea necesario.

Sobre el módulo recent

Hay una extensión muy útil para netfilter que quizás por desconocimiento, no se utiliza con frecuencia. Se trata de la coincidencia recent, que permite crear una lista dinámica de direcciones IP, contra la cual pueden comprobarse los paquetes de diferentes maneras. Naturalmente, ferm incluye un módulo que permite usar este tipo de coincidencia.

No explicaré detalladamente el funcionamiento de la extensión recent porque la página del proyecto tiene ejemplos de uso y el manual de iptables-extensions documenta los parámetros, pero quisiera ilustrar brevemente cómo usarlo.

Ejemplo básico del módulo recent

La forma de uso recomendada por el autor de la extensión, adaptada a un ejemplo de ferm para rechazar el acceso mediante protocolo TCP a cualquier servicio excepto HTTP/HTTPS, podría lucir así:

Con el ejemplo anterior, se consigue un comportamiento muy interesante. Tomemos como ejemplo el caso de un equipo remoto que intenta hacer una exploración de todos los puertos TCP abiertos en nuestro equipo, comenzando por el puerto 0.

El nuevo paquete será de tipo SYN, por lo que no será evaluado por la regla que acepta paquetes de conexiones ya establecidas y las relacionadas a éstas. Tampoco será un paquete proveniente de una interfaz local, por lo que será evaluado por el próximo bloque de reglas. El paquete no provendrá de una dirección IP recientemente observada, por lo que saltará la regla que contiene el módulo recent con el parámetro update. No obstante, como no será un paquete destinado a un puerto válido, saltará la regla que lo acepta y se le aplicará la que lo descarta, pero además se agregará la dirección IP de origen a una lista, junto con el momento exacto del intento de acceso.

A los efectos del equipo remoto, la sonda que envió al puerto 0 simplemente quedará sin respuesta, lo cual sugiere que el acceso está filtrado para este puerto. Pero nuestro cortafuegos ya estará vigilando esta dirección IP. Si el equipo remoto vuelve a enviar un paquete de solicitud de nueva conexión antes que hayan transcurrido 60 segundos, entonces el módulo recent detectará que la dirección IP ya se encuentra en la lista y la última actividad fue suficientemente reciente como para que haya coincidencia, por lo que descartará de inmediato el paquete, y además actualizará en la lista el momento de última actividad desde esa dirección IP.

Mientras el equipo remoto continúe intentando realizar nuevas conexiones de manera consecutiva sin un tiempo de espera de al menos un minuto entre intentos, a los efectos prácticos el acceso le será automáticamente bloqueado hasta el fin de los tiempos, lo cual es práctico por ejemplo para frenar ataques de botnets.

Alguien podría preguntarse, ¿pero durante el tiempo que el equipo remoto esté boqueado, que pasaría con las peticiones que nuestro propio equipo pudiera realizar a ese mismo equipo remoto? Pues recibiríamos la respuesta normalmente, porque la regla que permite las conexiones establecidas y relacionadas, así lo dispone.

Tras un período de inactividad de un minuto por parte del equipo remoto, si envía entonces un nuevo paquete de solicitud de conexión a un puerto válido, aunque la dirección IP pueda encontrarse aún en la lista, no habrá coincidencia entre el lapso de tiempo de última actividad y el momento de intento de acceso, por lo que todo funcionará normalmente. Eventualmente, si no vuelve a haber actividad inapropiada proveniente de la dirección remota, será eliminada automáticamente de la lista.

El uso del módulo recent es un acercamiento elegante a un problema relativamente complejo de abordar mediante otro tipo de reglas. Y al menos para el caso de prevenir intentos de ataques a puertos donde no hay servicios escuchando, es más eficiente que fail2ban.

Gestionando la lista de direcciones recientes

Por defecto, si no se especifica un nombre a la lista del módulo recent, se asumirá el nombre DEFAULT. Las listas de direcciones recientes se almacenan en archivos virtuales, bajo el directorio /proc/net/xt_recent/, por lo que si deseamos conocer de vez en cuando qué direcciones están en lista reciente y cuántos intentos tuvo cada una, podríamos hacer un alias como este:

Supongamos entonces que ejecuto el alias bloqueados y obtengo un resultado como el siguiente:

(Por cierto, si se pretende usar este alias con frecuencia, solo tendría que agregarse el al archivo de entorno para el intérprete de comandos, por ejemplo, en el caso de bash, puede agregarse al archivo ~/.bash_aliases).

Supongamos que consulto la zona inversa de la tercera dirección que se obtuvo, para verificar a qué corresponde:

O sea, que es simplemente un explorador de vulnerabilidades. Si quisiera eliminar sólo esta dirección de la lista reciente, pero ahora mismo no estoy como usuario root, podría entonces hacer algo como esto:

En realidad, como usuario root el comando sería ligeramente más corto:

Si quiero volver a agregarla:

Y si quiero vaciar por completo la lista de direcciones recientemente observadas:

Como puede verse, gestionar este tipo de listas no es particularmente complejo, aunque normalmente no necesitaremos hacerlo. De todas maneras, incluso si se vacía manualmente la lista, volverá a llenarse automáticamente en cuanto comience a detectarse tráfico inapropiado, de modo que si se prentende excluir permanentemente direcciones, deberán tomarse otras medidas sobre las que hablaré en breve.

Riesgos de bloquear direcciones IP

Supongamos que tenemos un cliente auténtico con dirección IP 203.0.113.127 que necesita consultar regularmente nuestro servicio web, y un atacante se mantiene durante todo un día enviando a nuestra red paquetes TCP SYN cada 30 segundos, destinados a puertos donde no hay servicios escuchando, pero falseando la dirección IP de origen de los paquetes a la dirección 203.0.113.127

Si teníamos activo el ejemplo de configuración que ilustré hace un momento, nuestro cortafuegos habría denegado por un día completo el servicio a un cliente que realmente tenía asignada la dirección IP que suplantó el atacante (para quienes crean que puse un ejemplo excesivo, no hace mucho monté un VPS al cual estuvieron realizando un ataque coordinado de inundación desde decenas de direcciones IP diferentes, que duró más de dos meses sin parar por un solo segundo).

De modo que en ocasiones puede ser necesaria una lista de exclusión, donde mantengamos las direcciones de equipos o redes con las que nos comunicamos regularmente.

Uso de ipset

Se recordará que ferm permite declarar una variable con un conjunto de direcciones, para facilitar declarar una sola regla refiriéndose a todas estas direcciones como si se tratase de una.

No obstante, aunque en sintaxis de ferm esto se vea muy compacto, a la hora de generar las reglas que se cargan en netfilter, se creará una regla por cada dirección IP. Cuando se trata de pocas direcciones no hay mayor problema, pero cuando necesitamos listas de cientos o miles de direcciones, el kernel podría requerir considerables recursos para analizar cada paquete entrante con una cantidad de reglas equivalente a la cantidad direcciones IP que contenía la variable.

Afortunadamente, para evitar esto netfilter tiene el paquete ipset, que permite verificar eficientemente si una dirección IP se encuentra en un set (conjunto) de direcciones previamente declarado. Si bien en este artículo no pretendo hacer una explicación a fondo sobre el uso de sets, me gustaría al menos mostrar cómo usarlos para listas de exclusión, pues ferm también es compatible con éstos.

Por cierto, si el comando ipset no está disponible en el sistema, puede instalarse el paquete ipset de la manera habitual, por ejemplo, en Debian y derivadas:

Creación de un set

En este caso, digamos que deseo crear un set de nombre exclusion (intencionalmente sin acento, para evitar problemas de codificación de caracteres), donde agregaré las direcciones de 4 nameservers públicos y una subred:

En este caso, utilicé el tipo de hash para redes, lo cual me permitirá agregar al set tanto direcciones IPv4 puntuales, como subredes completas en notación CIDR. A continuación, añadí las direcciones al set, y finalmente lo guardé en un archivo persistente, para uso futuro. El parámetro -exist lo muestro porque es útil para evitar fallos en usos sucesivos del mismo comando ipset.

Para comprobar el contenido del set, puede ejecutarse el siguiente comando:

El parámetro -sorted es para que los resultados aparezcan ordenados. Si se omite el nombre del set, se listan todos los sets declarados.

Uso de sets desde ferm

Se recordará que en la primera parte de esta serie, mencioné que ferm soporta la ejecución de comandos externos mediante la función @hook. Pues bien, he aquí una buena aplicación, modificando el ejemplo que puse al inicio de este artículo:

El efecto de la función @hook aquí es que al cargarse la configuración de ferm, se ejecutará primero el comando que carga el set en memoria; de esta manera no fallarán las reglas que hagan referencia a dicho set. Si el set ya estaba cargado en memoria y solo estábamos recargando la configuración de ferm tras una modificación menor, el parámetro -exist hará que se ignoren los fallos durante su carga.

En cuanto a las reglas, se habrá observado que se mantiene el uso del módulo recent, solo que en este caso se cambia el orden de evaluación para los paquetes TCP nuevos. Si el puerto de destino es válido, se acepta el paquete, en caso contrario, hay dos casos posibles:

  • La dirección IP de origen está en el set de exclusión; no conviene ponerla entonces en la lista de direcciones recientemente observadas, solo hay que descartar silenciosamente el paquete y nada más.
  • La dirección IP de origen es otra cualquiera, entonces puede colocarse en la lista de direcciones recientes a observar, y descartarse el paquete.

Con este acercamiento, no bloquearemos accidentalmente aquellas redes con las que nos comunicamos frecuentemente, pero no por ello aceptaremos de dichas redes paquetes inapropiados. Nuestra red estará entonces razonablemente protegida.

Naturalmente que los sets pueden utilizarse para mucho más que el ejemplo anterior. Podríamos usar un set para agregar todas las subredes de nuestro pais y hacer que una petición al servicio Web desde el mismo país sea reenviada a un virtualhost que devuelva una página con información local, irrelevante para el resto del mundo.

O mediante un hook, podríamos hacer que se cargue en un set una lista de direcciones reportadas como maliciosas, antes de cargar nuestras reglas y entonces bloquear directamente cualquier paquete proveniente de dichas direcciones, etc.

Ejemplo con los módulos set y recent

Ahora bien, hay un escenario que no se ha contemplado aquí: un ataque de inundación con paquetes SYN desde una botnet. La configuración mostrada anteriormente no deja de ser válida para tales casos, pero no es la más eficiente, porque al estar las verificaciones en la tabla filter, los paquetes TCP previamente son procesados por el rastreo de conexiones.

Se recordará que en el artículo anterior mencioné cómo la tabla raw podía resultar provechosa para el filtrado temprano de paquetes maliciosos. Tomando esto en consideración, una posible forma de optimizar la configuración, podría quedar entonces más o menos así:

En este caso, he movido el módulo recent a la cadena PREROUTING de la tabla raw. Si la dirección IP ya estaba siendo observada, se actualiza su momento de última actividad y descarta de inmediato el paquete, sin darle la oportunidad de avanzar a otras tablas.

De no ser el caso, si el paquete va destinado a un puerto válido, se acepta en la cadena, para que se continúe evaluando en las otras tablas. Si el destino es un puerto inválido, se toma la acción de descartar el paquete en caso de que la IP de origen estuviera en la lista de exclusión, o descartarlo y enviar la IP de origen a la lista de direcciones observadas con actividad reciente.

Puede que algunos se hayan fijado que en la tabla mangle también aparece la evaluación del módulo recent, pero es sólo para los paquetes nuevos que no tengan el protocolo TCP (ya lo cubrimos en la tabla raw). Esto cubre el caso de que un equipo cuya dirección IP ya estaba siendo monitoreada, envie un paquete con un protocolo como UDP, ICMP u otro sin que hayan transcurrido 60 segundos desde que envió su último paquete de solicitud. En tal caso, se hará una actualización del momento de último acceso a la dirección IP de origen.

A propósito, conviene aclarar que aunque el protocolo UDP carezca de control de estado, desde que el paquete sale de la tabla raw, el kernel de Linux le activa el rastreo de conexiones, por lo que es capaz de determinar si el paquete está relacionado con tráfico previo, o es un paquete completamente nuevo. De ahi que haya utilizado la tabla mangle para revisar si se trata de un paquete nuevo.

Finalmente, se declaran en la tabla filter las reglas para las cadenas INPUT, FORWARD y OUTPUT sin necesidad de hacer referencia ni al módulo set, ni al módulo recent.

Si bien este ejemplo es más complejo que el anterior, ahorra al microprocesador tiempo de procesamiento habilitando rastreo de conexiones y evaluando innecesariamente paquetes que en definitiva resultarán siendo inválidos.

NAT (traducción de direcciones de red)

En el espacio de direcciones IPv4 (que aún sigue siendo el predominante en muchos lugares del mundo), a menudo se necesita que los paquetes de una red privada tengan salida al exterior, o viceversa. Para esto se usa la tabla nat (Network Address Translation) y algunos destinos de salto especiales. No pretendo aquí detallarlo todo, solo quisiera ilustrar algunos casos mediante ejemplos.

Redirección

Tomemos de ejemplo una empresa que tiene su propio nameserver y servicio de sincronización de horario. Podría no desearse que los clientes de la red local realicen consultas directas a servidores externos, pues existen formas de usar los puertos de estos servicios para encapsular tráfico a fin de escapar a las políticas de seguridad informática de la empresa, etc.

Suponiendo que se trata de una microempresa donde el mismo equipo a la vez hace de cortafuegos, resuelve direcciones para la red local y mantiene el horario sincrónico por la interfaz eth0, puede hacerse algo como esto en la tabla nat:

De esta manera, cualquier petición de sincronización de horario o consulta DNS que vaya destinada a direcciones externas, serán redirigidas a la dirección de la interfaz por donde el cortafuegos recibió la petición.

El mismo principio de reenvío podría ser útil para otras cosas como obligar el uso de un proxy transparente, por ejemplo. Este artículo no es el lugar apropiado para explicar la instalación y configuración de un proxy transparente, pero asumiendo que se usa Squid como servidor proxy y que escucha en el puerto tradicional (3128), podría declararse algo como esto:

Enmascaramiento

Si deseamos configurar un equipo como cortafuegos para navegación sin necesidad de proxy, lo primero es verificar que el bit de reenvío esté habilitado; para hacerlo de manera persistente recomiendo agregar el siguiente valor al archivo /etc/sysctl.d/local.conf:

Luego, para que se active el parámetro en memoria basta con recargar como superusuario:

Ejemplo simple de MASQUERADE

Una de las formas mas simples de enmascaramiento es MASQUERADE, que está mas bien concebida para modem-routers con dirección IP pública dinámica. Para permitir a los equipos de la red local la salida solo al servicio de navegación, podría crearse una configuración como esta:

A propósito, cuando la cadena FORWARD de la tabla filter tiene políticas de denegación por defecto (y nunca debería usarse de otra forma), de usarse MASQUERADE para habilitar enmascaramiento en la interfaz pública, basta con la regla que acabo de mostrar en la cadena POSTROUTING de la tabla nat; es innecesario declarar reglas más complejas en dicha tabla, pues como el reenvío de paquetes a otros equipos estará deshabilitado por defecto, no hay riesgo.

Para declarar con precisión qué paquetes de la subred local tienen salida hacia otros equipos usando el enmascaramiento, está la cadena FORWARD de la tabla filter. En el ejemplo anterior, en cuanto a conexiones nuevas desde la subred LAN sólo se permite el protocolo TCP y los puertos de destino 80 o 443, cualquier otra petición será descartada por la política por defecto.

La regla que permite la respuesta a las conexiones establecidas o relacionadas es muy importante para el flujo de las comunicaciones, por eso la puse de primera después de declarar la política por defecto para la cadena.

Ejemplos con SAME y SNAT

Supongamos que tenemos un enlace dedicado con una dirección pública de máscara 29. De las 8 direcciones IP públicas que daría dicho enlace, normalmente una de ellas será la dirección de la red, otra la dirección de difusión y una tercera la dirección del router (enrutador), lo cual dejaría 5 direcciones disponibles para realizar enmascaramiento.

De estas 5 direcciones, supongamos que queremos dejar una reservada para uso del propio cortafuegos y distribuiremos las demás, declarando en la tabla POSTROUTING reglas mas explícitas sobre la forma de realizar el enmascaramiento:

En este ejemplo, el propio cortafuegos saldría al mundo con su propia dirección IP pública, el enmascaramiento asignaría para el administrador de red siempre la misma dirección pública, y al caso del resto de los empeados se le asignaría de manera aleatoria una de las 3 direcciones IP públicas restantes.

Algo importante a señalar del ejemplo anterior, es que al usar el destino de salto SNAT, como establecí un rango para 3 direcciones IP usando dos variables, esto se comporta como una concatenación, por lo que se debe rodear la expresión con comillas dobles.

Reenvío de puertos

Como mismo puede ser necesario hacer que equipos con dirección de una subred privada puedan acceder a direcciones IP públicas de una red externa, también puede ser necesario que equipos de una subred externa tengan acceso a servicios que tenemos en una subred empresarial, sin que por ello se les otorgue acceso al resto de la red. En otras palabras, una DMZ (DeMilitarized Zone, zona desmilitarizada).

En este caso lo que se utiliza es una redirección de puertos mediante el destino especial de salto DNAT, pero para que el servidor de correo en la DMZ pueda enviar correo al resto del mundo, también habrá que habilitar SNAT. Como se trata de un caso de uso bastante típico, dejaré un posible ejemplo de configuración.

Ejemplo de DNAT para servicio de correo

Algo importante a observar aquí es que la dirección IP pública se declara en la tabla nat, no en la tabla filter.

Conclusiones

Con este artículo doy por concluída la serie de presentación sobre ferm, con la que pretendía demostrar al menos parcialmente su potencia y flexibilidad.

No obstante, hay más funcionalidad que no he mencionado; en parte porque los artículos terminarían siendo tediosamente largos, pero principalmente porque mi aspiración era más bien crear la curiosidad por probar y conocer más sobre este excelente front-end a netfilter. Espero haber conseguido eso al menos.

Si crees que algo en este artículo no quedó suficientemente claro, deja un comentario.

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Promedio de puntuación 5 / 5. Recuento de votos: 5

Hasta ahora, ¡no hay votos!. Sé el primero en puntuar este contenido.

Sobre Hugo Florentino 11 artículos
Administrador de redes y sistemas. Usuario regular de GNU/Linux desde Octubre de 2008. Miembro fundador del Grupo de Usuarios de Tecnologías Libres (GUTL).

1 comentario

  1. Firefox 91.0 Firefox 91.0 Windows 10 x64 Edition Windows 10 x64 Edition
    Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0

    Gracias a JMIO por señalar que en el ejemplo con los modulos set y recent había un error en la cadena OUTPUT, donde había puesto la directiva if (abreviatura de interface) en lugar de of (abreviatura de outerface).

    Convoco al resto de los colegas a encontrar errores en mis artículos, personalmente prefiero las críticas a los elogios (uno aprende más), por lo que son bienvenidas.

Dejar una contestacion

Tu dirección de correo electrónico no será publicada.


*