Ferm, poderoso cortafuegos (parte 1)

En este artículo pretendo dar un poco de divulgación a ferm, poderoso cortafuegos para GNU/Linux, que aun resulta desconocido para muchos. Puede considerarse este como un artículo de continuación a los que hice sobre netfilter. Por cierto, las siglas FERM representan simplemente For Easy Rule Making (para la creación fácil de reglas).

Todo administrador de redes familiarizado con GNU/Linux conoce iptables, comando que permite configurar las reglas para netfilter, el cortafuegos tradicionalmente integrado en el kernel. Es un comando indudablemente potente, pero también trabajoso; un script de iptables puede terminar teniendo cientos o hasta miles de líneas, lo cual dificulta el mantenimiento. Algunas amistades me han pasado scripts o salvas de iptables-save para que les ayude a encontrar problemas y salta a la vista que no es raro caer en redundancias o reglas que entran en conflicto.

No resulta sorprendente entonces, que con el paso del tiempo hayan surgido varios proyectos que buscan simplificar la gestión de reglas mediante una sintaxis menos repetitiva, para finalmente generar como tarea de fondo reglas de netfilter, que son las que entonces se cargan en el kernel de manera transparente al usuario. A este tipo de proyectos se les conoce como front-end, aunque por convención se acepta llamarles cortafuegos también.

Se supone que el proyecto nftables debería haber sido un reemplazo del venerable iptables, pero su funcionalidad aun no es del todo equivalente. Recientemente está emergiendo el proyecto eBPF, cuya arquitectura tiene potencial para crear cortafuegos poderosos, eficientes en términos de rendimiento. Pero es un proyecto de cierta complejidad (integra una máquina virtual que usa instrucciones en lenguaje propio, entre otros detalles), que aun no ha alcanzado suficiente madurez.

Y bueno, entre los front-end, está ferm. Conocí de este proyecto un día que bromeábamos en un grupo de Telegram sobre cuál cortafuegos era peor, y un colega mencionó que estaba probando ferm y le gustó porque generaba reglas de iptables muy limpias. Al menos yo había mirado previamente fwbuilder, shorewall, ufw y firehol, pero ninguno de estos me convencía lo suficiente como para usarlo sobre el propio iptables (los gustos son personales), por lo que solía permanecer escéptico en relación a los front-end de netfilter en general. Sin embargo, como me picó la curiosidad, decidí probar ferm. Terminó gustándome tanto, que es lo que utilizo actualmente para la configuración de mis cortafuegos. Francamente, me resulta curioso que este proyecto no se haya mencionado más.

Instalación

Ferm es una herramienta que sólo requiere Perl 5.24 o superior, por lo que debería funcionar en cualquier distribución de GNU/Linux. De hecho la instalación de ferm es simple, pues está en los repositorios de las principales distribuciones. Por ejemplo, en el caso de Debian y derivadas, basta con ejecutar como superusuario el comando habitual:

El proceso de instalación pregunta si deseamos iniciarlo con el sistema, que es lo recomendado. Y sin más, el paquete se instala, creando un archivo de configuración básica. La documentación de ferm es bastante completa y puede consultarse como de costumbre:

Algo a tener en cuenta es que no conviene tener varios cortafuegos configurados para iniciar con el sistema, o las reglas podrían entrar en conflicto. Como ferm soporta los comandos iptables, ip6tables, arptables y ebtables, recomiendo que una vez instalado, configurado y comprobado, se desactiven estos otros, y también se desinstalen otros cortafuegos como ufw, etc.

Nota importante: A los efectos de este artículo, asumiré que la versión de ferm es 2.6 o superior (o los ejemplos de configuración podrían fallar); si no es el caso, después de instalarse desde los repositorios, puede actualizarse a la versión más reciente disponible en GitHub, por ejemplo ejecutando esto como root:

Configuración básica

Por defecto, ferm crea durante la instalación el archivo de configuración básica /etc/ferm/ferm.conf. Personalmente, no lo uso porque prefiero crear mis propias reglas personalizadas, de modo que suelo vaciarlo dejando únicamente esta línea:

Con esto, ferm intentará cargar cualquier archivo en la ruta /etc/ferm/ferm.d/, lo cual demuestra una de las características de la herramienta: es modular. Pueden concebirse archivos con bloques de configuración e incluirlos desde otros archivos, lo cual simplifica el trabajo en configuraciones complejas, donde sólo hay que modificar algo concreto. Un archivo de configuración extenso solo dificultaría encontrar lo que es necesario modificar.

Sintaxis

La sintaxis de ferm es simple pero flexible, permitiendo configuraciones compactas, legibles y expresivas. Para lograr el equivalente mediante el comando iptables se requeriría de trabajo extra mediante el uso de scripts personalizados. En cambio, ferm trae integrado soporte para variables, funciones definidas por el usuario, y ejecución de comandos externos.

En ferm, los bloques de reglas inician y terminan con llaves y las reglas terminan con punto y coma. Se ignoran los saltos de línea, permitiéndose usar para una regla un criterio de división en líneas totalmente arbitrario. Los identificadores también pueden separarse por una cantidad arbitraria de espacios, permitiendo flexibilidad de alineación visual entre reglas.

Una salvedad es que donde aparezca el símbolo de número (#), se ignora todo desde ese punto hasta el final de la línea, lo cual es práctico para agregar comentarios a las líneas (sin necesidad de utilizar el identificador comment o de insertar líneas sólo para comentarios).

En otras palabras, ferm permite crear configuraciones según el criterio de estructuración que cada quien prefiera.

Ejemplo minimalista

Una configuración habitual en ferm suele ser fácil de entender. Lo ilustraré mejor con un ejemplo concreto. Un script minimalista de iptables para el cortafuegos de una estación de trabajo podría lucir mas o menos así (utilizaré una declaración explícita mediante parámetros largos para que se vea más claramente el significado de las reglas):

Esta misma configuración declarada en sintaxis de ferm podría lucir asi:

En realidad esta es sólo una posible forma de configuración. Debido a la flexibilidad de estructuración que permite ferm, este ejemplo bien podría haberse declarado en una sola línea (aunque evidentemente, esto reduciría su legibilidad).

La configuración inicia con la declaración de un dominio de aplicación de las reglas, pues es relativo a la herramienta de que se trate. Como dije antes, ferm permite declarar reglas para diferentes herramientas: iptables, ip6tables, arptables y ebtables. En este artículo me centraré en el dominio ip (correspondiente al comando iptables).

Algunas semejanzas y diferencias

Quien esté suficientemente familiarizado con iptables podrá apreciar que adaptar las reglas a ferm es relativamente fácil, pues la sintaxis no es drásticamente diferente (fue una de las cosas que me gustó). Esto es algo que personalmente no puedo decir de otros front-end que he probado, cuya sintaxis sí es bien diferente.

Los principios utilizados en ferm son los mismos que se usan en iptables: tablas, cadenas, políticas por defecto, control de estado, interfaces, direcciones, protocolos, coincidencias, agumentos para las coincidencias, destinos de salto, etc. Solo varía la forma de declaración.

En la tabla que muestro a continuación, pueden apreciarse similitudes entre algunos identificadores (palabras reservadas) de iptables y sus equivalentes en ferm:

iptablesferm
––table (-t)table
––flush––flush
(aunque se usa de una manera diferente)
––policy (-P)policy
(aunque se declara de una manera diferente)
––in-interface (-i)interface (if)
––out-interface (-o)outerface (of)
––source (-s)saddr
––destination (-d)daddr
––protocol (-p)protocol (proto)
––source-port (––sport)sport
––destination-port (––dport)dport
––match (-m)module (mod)
––jump (-j)jump
(puede omitirse, a menos que el destino de salto sea una cadena personalizada)

Debido a la naturaleza estructurada de ferm, comandos de iptables como ––append (-A), ––insert (-I), ––check (-C), ––replace (-R), ––delete (-D) se vuelven innecesarios.

La forma de declarar cadenas personalizadas también hace innecesarios comandos de iptables como ––new-chain (-N), ––rename-chain (-E), ––delete-chain (-X).

Podría decirse que iptables fue concebido para agregar al kernel reglas individuales desde la línea de comandos, mientras que ferm se concibió para usar un archivo de configuración donde las reglas forman un todo armónico. No obstante, como ferm es simplemente un front-end de netfilter, nada impide agregar manualmente al kernel una regla de iptables desde la línea de comandos, si así lo deseamos.

En el ejemplo de configuración que puse anteriormente, se aprecia una ventaja de ferm: permite declarar parte de una regla y en la misma línea iniciar un nuevo bloque que herede la funcionalidad, lo cual facilita evitar reiteraciones. Solo declaré el módulo de rastreo de conexiones una vez, para entonces iniciar desde esa misma línea bloques conteniendo tres reglas que usan este módulo, sin necesidad de declararlo de manera explícita para cada regla.

En este caso puede que esta ventaja ho haya resultado muy evidente, pero ahorra muchísimo trabajo en configuraciones complejas donde un grupo extenso de reglas tiene características en común, como la interfaz, la dirección IP, el estado en el rastreo de conexiones, etc.

Migración de reglas existentes

Cambiar de cortafuegos es algo que a algunos les causa un poco de temor, lo cual es comprensible, porque uno sale de su «zona de confort» para probar algo desconocido, que suele requerir de un proceso de aprendizaje y práctica mediante ensayo-error. A veces termina siendo una experiencia traumática, en que uno vuelve corriendo a lo conocido para no mirar jamás otras alternativas.

Afortunadamente, ferm facilita un comando para convertir las reglas. Por ejemplo, en un sistema con reglas de iptables activas en memoria, basta con ejecutar como superusuario algo como esto:

A propósito de esta ubicación: generalmente elijo crear un archivo dentro del directorio ferm.d/ porque con esto evito que una posible actualización del paquete termine remplazando accidentalmente mis reglas.

De hecho, en realidad suelo dejar el archivo /etc/ferm/ferm.conf de esta manera:

La diferencia es que en este caso se especifica cuál será el archivo a cargar automáticamente; esto me permite mantener en ferm.d/ configuraciones que no me interesa cargar automáticamente, como archivos de prueba para nuevas reglas, salvas de reglas funcionales anteriores, configuraciones minimalistas seguras, etc.

Volviendo a la migración, también pueden convertirse reglas guardadas en formato de iptables-save, por ejemplo si se usaba el paquete iptables-persistent, pueden convertirse las reglas que este gestiona de esta manera:

Para la carga y aplicación de las reglas en el kernel, ferm tiene un práctico modo de ejecución en forma interactiva (similar al comando iptables-apply), que requiere de confirmación y en caso de errores revierte automáticamente a las reglas previamente existentes en memoria, permitiéndose incluso establecer el tiempo de espera en segundos:

Si se omite el tiempo de espera, ferm asume el valor predeterminado: 30 segundos. Si solo deseamos cargar las reglas directamente sin esperas o que se requiera confirmación, basta con omitir además el parámetro de interactividad:

El inconveniente de hacerlo de esta manera es que si estamos trabajando remotamente y ocurren errores, podríamos quedar sin acceso al equipo. Así que es preferible (especialmente si hacemos trabajo a distancia) acostumbrarse a usar el modo interactivo, ahorrará malos ratos.

Si uno desea ver cómo lucen en formato de iptables-save las reglas que uno ha declarado en un archivo, basta con ejecutar ferm con los siguientes parámetros:

O utilizando parámetros cortos, con un archivo imaginario de pruebas:

También podría usarse el parámetro –remote que, cuando se acompaña del parámetro –shell, genera un script para uso en un equipo remoto, donde al ejecutarse, cargará las reglas mediante el comando iptables-restore.

Desactivar temporalmente las reglas

No hace mucho un amigo me preguntó: ¿como hago en ferm para eliminar todas las reglas en memoria y que quede un acceso permisivo? La pregunta es interesante, pues en una configuración compleja de iptables que involucre las 4 tablas, con políticas por defecto modificadas o cadenas personalizadas, esto puede llegar a ser un poco tedioso:

Con ferm, basta con ejecutar algo así:

(Se necesita especificar el archivo de configuración para que ferm pueda determinar qué dominios y tablas afectar, recordemos que es multi-dominio).

Hasta aquí, podría pensarse que nada de esto justifica realmente el uso de ferm, pero es a partir de este punto donde las cosas se ponen más interesantes.

Variables

Las redes rara vez se mantienen intactas; lo normal es que necesiten adaptarse. Por ejemplo, supongamos que en nuestra red local hay un PC con dirección IP 10.0.0.2 y tenemos varias reglas en las cuales aparece esta IP. Si el día de mañana la red cambia, tendríamos que modificar cada línea de la configuración donde aparezca. Las variables son muy útiles para estos casos.

Indudablemente que podríamos hacer un script de iptables usando variables, pero este acercamiento no sólo es trabajoso; un script largo puede tardar en cargar, y durante ese tiempo nuestro cortafuegos podría dejar a nuestro equipo expuesto.

Puede que no lo parezca, pero ferm evalúa las reglas muy rápido y por defecto genera una configuración en formato de iptables-save, que el kernel puede cargar en memoria más rápido que lo que tarda en interpretar y agregar reglas de iptables una a una desde un script.

En ferm las variables están previstas sin necesidad de modificaciones, y se definen asi:

Por ejemplo, para declarar la dirección IP de un equipo de la red local:

Esto permitiría declarar más adelante reglas como esta en la cadena INPUT:

De esta manera, si en el futuro la dirección IP del equipo de Jose cambia, solo necesitaremos modificar el valor de la variable, y todas las reglas donde aparezca la variable, seguirán funcionando correctamente.

También puede utilizarse la negación (mediante el signo de cierre de exclamación, similar a como lo permite iptables). Por ejemplo, si para nuestra LAN utilizamos el bloque 10.0.0.0/16 sería muy sospechoso recibir por la interfaz LAN un paquete con la IP de origen 192.168.43.1

De modo que si la interfaz LAN es eth0 y no usamos servicio DHCP, podríamos hacer declarar como esto:

Si usamos el servicio DHCP, tenemos que permitir además los paquetes de descubrimiento del protocolo DHCP. Arreglando el ejemplo:

Incluso, ferm facilita un parámetro que permite modificar al vuelo el valor de variables, lo cual permite hacer pruebas, sin necesidad de modificar archivos de configuración:

Variables con múltiples valores

Supongamos que deseamos permitir acceso SSH a nuestro equipo, pero solo desde tres direcciones IP. Ferm permite asignar las tres direcciones a una misma variable, mediante un array (arreglo, colección o lista) delimitado por paréntesis, conteniendo valores del mismo tipo, separados por espacio:

De hecho, como ferm ignora los saltos de línea, sería válido declarar esto mismo de una manera más cómoda de identificar:

Aunque se parezca a la declaración de un bloque, hay que tener presente que es simplemente una regla de declaración de una variable en varias líneas, por lo que debe llevar el punto y coma al final.

Esta variable podrá usarse entonces normalmente como parte de una regla. Por ejemplo, para permitir a estos tres equipos el acceso por SSH, en la cadena INPUT podríamos declarar esto:

En iptables habría sido necesario declarar 3 reglas, una para cada equipo. O preparar un script con un ciclo, que para un caso tan simple sería equivalente a usar una ametralladora como matamoscas.

Las variables pueden utilizarse no sólo para direcciones IP, sino para casi cualquier cosa, como interfaces y puertos, por ejemplo.

Continuando con nuestro ejemplo, supongamos que queremos especificar que estos tres equipos se conectan al nuestro para iniciar una sesión SSH por la interfaz de red eth0, correspondiente a nuestra LAN, pero no deben tener acceso en absoluto por la interfaz que dedicamos para la DMZ, digamos eth1. La parte relevante de la configuración podría modificarse entonces quedando más o menos así:

¿Que ventajas ofrece esto? Para empezar, hemos declarado solo dos reglas en lugar de las 6 que hubiésemos necesitado, de usar iptables. Pero además, en el futuro podríamos agregar o quitar equipos de la lista, invertir las interfaces o cambiar el puerto SSH a uno superior al 50,000 y no necesitaremos tocar regla alguna, sólo editar las variables.

Un script de iptables similarmente informativo, resultaría más reiterativo e incómodo de revisar:

Anidamiento de variables

Supongamos que habilitamos un servicio web por los protocolos http (puerto 80) y https (puerto 443), y también tenemos habilitado servicio de correo electrónico mediante los protocolos pop, pop3s, imap, imaps, smtp, smtps y submission (puertos 110, 995, 143, 993, 25, 465 y 587, respectivamente).

Podríamos hacer la declaración de variables así:

Este ejemplo ilustra cómo ferm permite usar variables como parte de otra variable, sin imponer límites prácticos de anidamiento.

Finalmente, podríamos emplear la variable de los puertos TCP a los que permitimos el acceso con una única regla en la cadena INPUT, así:

A propósito, en esta regla muestro la palabra reservada dports, que en realidad no es más que un atajo conveniente a la expresión más larga mod multiport destination-ports.

Dicho sea de paso, muchos administradores de red desconocen que incluso en iptables, la coincidencia multiport permite reducir mucho las reglas, lo cual favorece un mejor rendimiento. O sea, que con un script de iptables también podríamos haber logrado una declaración de puertos similar. El inconveniente es que la coincidencia multiport impone un límite de 15 puertos por regla, de manera que en iptables hay que llevar cuenta de los puertos que se asignan en cada regla.

En cambio, ferm no impone límite alguno a su módulo multiport; podemos crear una única regla usando una variable que hayamos declarado previamente con decenas de puertos y para cargarla en el kernel, simplemente se generarán automáticamente tantas reglas de netfilter como sean necesarias, usando hasta 15 puertos por regla.

Uso de archivos y comandos externos

Supongamos que en el archivo /home/miusuario/cidr_pais.txt tenemos los bloques de direcciones en notación CIDR que IANA ha asignado para uso por un país, una entrada por línea. Podríamos hacer que ferm cargue todas las direcciones del archivo con máscara de red /24 en una variable, usando algo como esto:

En otras palabras, podemos asignar a una variables la salida de comandos externos sin necesidad de hacer malabares, con sólo incluir éstos entre acentos graves.

Ejecución condicional de comandos externos

En ocasiones podríamos necesitar que se ejecute un script externo que cree interfaces virtuales o algo similar, justo antes de iniciar la carga de nuestras reglas; con ferm también puede hacerse, colocando algo como esto en la configuración:

Los hooks (ganchos) son de tres tipos:

pre – Se ejecuta antes de cargar las reglas.
post – Se ejecuta tras cargar las reglas.
flush – Se ejecuta cuando se limpian por completo las reglas (para lo cual se usa el comando ferm –flush)

Continuará

Para no aturdir con demasiado contenido, dejaré aquí esta primera parte sobre ferm. En la próxima parte, hablaré acerca de algunas funciones y variables incluídas por defecto, mostraré cómo declarar funciones definidas por el usuario, y usaré ejemplos más complejos de configuración que ilustren cómo aprovechar algunas de las ventajas de ferm.

Si deseas que se trate algo concreto en las próximas partes, o crees que en este artículo algo 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: 7

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

3 comentarios

  1. Google Chrome 91.0.4472.120 Google Chrome 91.0.4472.120 Android 9 Android 9
    Mozilla/5.0 (Linux; Android 9; Redmi S2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36

    Muy bueno el artículo, la verdad lo estaba esperando hace rato. Gracias. El comentario de JMIO abarca muchas cosas que quisiera saber sobre ferm, y bueno ya ví que le vas a dar cobertura a todo. Sobre todo el tema NAT. Saludos

  2. Google Chrome 89.0.4389.105 Google Chrome 89.0.4389.105 Android 7.1.2 Android 7.1.2
    Mozilla/5.0 (Linux; Android 7.1.2; LM-X410.F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Mobile Safari/537.36

    Hugo muy bueno, felicitaciones. Sé estabas por sacarlo desde hace ratón… el articulo sobre ferm; créeme q da para mucho mucho más…

    1- Poner ejemplos con enlace a ipset.
    2- Poner algunas de las protecciones más comunes vs ddos, scans, etc; en modo protección quizás integrando la herramienta con algún IDS/IPS. Acá pudiera ser interesante tocar el tema anclaje ip-mac para evitar el falseo de direcciones, suplantación, evitar q lleguen y se te conecten así como así… con un ip de tu rango local quizás clonando una dirección física: Esto sabemos q lleva + cosas por el medio de port security en switch ports, dhcp config, etc pero sería muy bueno tener algo con el FW en sí (Algo q he visto q se ha discutido en el canal y a nuestros amiguitos en las inpecciones les gusta estar haciendo). Algún método q podamos aplicarlo quizás con el propio ferm.
    4- Tema nat en ferm.

    Sé q esto es un mundo, pero sí creo q se pudiera ir de a poco enriqueciendo con muchos de estos temas relacionados con la seguridad.

    Lo anterior pa darte algunos caminos q quizás pudieran servirte para ir profundizando en este maravilloso. front-end.

    Muy bueno, a la verdad.

    • Firefox 78.0 Firefox 78.0 Windows 10 x64 Edition Windows 10 x64 Edition
      Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:78.0) Gecko/20100101 Firefox/78.0

      Gracias.

      De hecho, prácticamente todos los contenidos que mencionaste los tenía previsto cubrir de todas formas (te gustará el truco que cociné para el anclaje IP-MAC).

      Lo relacionado a ipset lo dejaré como mínimo para la 3ra parte, antes quiero mostrar otras cosas.

Dejar una contestacion

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


*