Desde la series 1.1, el kernel de Linux posee en mayor o menor medida capacidad para filtrar tramas. Originalmente (1994), ipfwadm era la herramienta proporcionada con Linux para la implementación de políticas de filtrado de paquetes en este clon de Unix; derivaba del código de filtrado en BSD (ipfw), y debido a sus limitaciones (por ejemplo, sólo puede manejar los protocolos TCP, UDP o ICMP) ipfwadm fue reescrito para convertirse en ipchains a partir del núcleo 2.1.102 (en 1998). Esta nueva herramienta (realmente, todo el subsistema de filtrado de los núcleos 2.2) introdujo bastantes mejoras con respecto a la anterior, pero seguía careciendo de algo fundamental: el stateful; era difícil ver a un sistema tan potente como Linux sin una herramienta de firewalling decente, libre, y `de serie' con el sistema, mientras otros clones de Unix, también gratuitos hacía tiempo que la incorporaban, como es el caso de FreeBSD e IPFilter.
De esta forma, no es de extrañar que a partir del núcleo 2.3.15 (por tanto, en todos los kernels estables, de la serie 2.4, desde mediados de 1999) ipchains fuera sustituido por iptables, que de nuevo introducía importantes mejoras con respecto a su predecesor. Sin duda la más importante era que ya incorporaba el stateful no presente en ipchains, pero no era la única; además, iptables ofrecía - y de hecho, ofrece - un sistema de NAT (Network Address Translation) mucho más avanzado, incorpora mejoras en el filtrado (llegando incluso a filtrar en base a la dirección física de las tramas) e inspección de paquetes, y presenta un subsistema de log mucho más depurado que ipchains. Por tanto,iptables es en la actualidad el software de firewalling en Linux IPv4; aunque todas las versiones de Linux lo incorporan por defecto, se puede descargar una versión actualizada desde http://netfilter.samba.org/##.
Históricamente, todos los sistemas de firewalling nativos de Linux han sido orientados a comando: esto significa, muy por encima, que no leen su configuración de un determinado fichero, por ejemplo durante el arranque de la máquina, sino que ese archivo de arranque ha de ser un script donde, línea a línea, se definan los comandos a ejecutar para implantar la política de seguridad deseada; esta es una importante diferencia con respecto a otros cortafuegos, como IPFilter (del que hablaremos a continuación), orientados a archivo: en estos la política se define en un simple fichero ASCII con una cierta sintaxis, que el software interpreta y carga en el sistema.
La sintaxis de iptables (o la de ipchains, bastante similar) puede llegar a resultar muy compleja si se invoca al sistema de filtrado desde línea de órdenes; por fortuna (o no por fortuna), existen diferentes interfaces para el administrador, algunos tan cómodos e intuitivos como el de Firewall-1, capaces de presentar las políticas de una forma gráfica basada en objetos y de transformar después esas políticas en scripts con las órdenes de iptables o ipchains equivalentes. Un ejemplo de estos interfaces es fwbuilder, disponible libremente desde http://www.fwbuilder.org/##.
Para conocer mejor todo el subsistema de filtrado en Linux, así como sus herramientas de gestión, consultas imprescindibles son los HowTo [Rus00], [Rus02] y [Gre00]; la mayor parte de esta sección está basada en estas obras. Otros documentos que pueden resultar especialmente interesantes son [Mou00] y [Zie01].
iptables o ipchains son herramientas flexibles, potentes e, igual de importante, gratuitas, que funcionan sobre un sistema operativo también gratuito; quizás para una organización de I+D o para una empresa no muy grande sea difícil permitirse soluciones comerciales cuyo precio puede ascender a varios millones de pesetas, especialmente si se van a instalar cortafuegos internos o arquitecturas DMZ de varios niveles. Sin embargo, no hay excusa para no utilizar este software de filtrado: un pequeño PC corriendo Linux es más que suficiente para, en muchas ocasiones, garantizar - o al menos incrementar - la seguridad de un laboratorio, un aula informática o un conjunto de despachos.
Arquitectura
En Linux el filtrado de paquetes está construido en el kernel (se habla con más detalle del núcleo de este sistema operativo en la sección 10.6); en la serie 2.2, para poder utilizar ipchains hemos de compilar el núcleo con las opciones CONFIG/SMALL>_FIREWALL y CONFIG/SMALL>_IP/SMALL>_FIREWALL activadas, mientras que en las 2.4, para iptables, hemos de activar CONFIG/SMALL>_NETFILTER: es toda la `instalación' (aparte de las herramientas de gestión de espacio de usuario, que vienen de serie con Linux) que nuestro firewall va a necesitar, de ahí que en este caso no dediquemos una subsección específica a la instalación del cortafuegos.
Cuando ya estamos ejecutando un núcleo con el firewalling activado utilizaremos las herramientas de espacio de usuario ipchains e iptables para insertar y eliminar reglas de filtrado en él; al tratarse de información dinámica, cada vez que el sistema se reinicie las reglas establecidas se perderán, por lo que es recomendable crear un script que se ejecute al arrancar el sistema y que las vuelva a definir. Para ello nos pueden resultar útiles un par de shellscripts que acompañan a las herramientas de espacio de usuario: se trata de ipchains-save e ipchains-restore (núcleos 2.2) y de iptables-save e iptables-restore (núcleos 2.4); en ambos casos, la primera orden vuelca en pantalla las reglas definidas en el núcleo y la segunda carga dichas reglas desde un archivo.
El núcleo de Linux agrupa las diferentes reglas definidas por el administrador en tres listas denominadas chains: input, output y forward (en mayúsculas para los kernels 2.4); en función de las características de una trama, Linux aplica las reglas definidas en cada una de estas listas para decidir qué hacer con el paquete. En primer lugar, al recibir una trama utiliza las reglas de la chain input (su nombre es autoexplicativo) para decidir si la acepta o no; si las reglas definidas en esta lista indican que se ha de aceptar el paquete, se comprueba a dónde ha de enrutarlo, y en el caso de que el destino sea una máquina diferente al cortafuegos se aplican las reglas de la lista forward para reenviarlo a su destino. Finalmente, la lista output se utiliza obviamente antes de enviar un paquete por un interfaz de red, para decidir si el tráfico de salida se permite o se deniega.
Como hemos dicho, los elementos de cada lista se denominan reglas y definen - junto a los targets, de los que hablaremos a continuación - qué hacer con los paquetes que cumplen ciertas características; si un paquete no cumple ninguna de las reglas de la lista que le corresponde, lo mejor si queremos un sistema seguro es rechazarlo o denegarlo, para lo cual podemos definir un tratamiento por defecto. Mediante ipchains e iptables podemos crear listas, modificarlas y eliminarlas17.1 y, lo realmente importante, definir las reglas para cada lista; para estudiar las opciones de ambas órdenes se pueden consultar las páginas ipchains(8), ipfw(4), ipchains-restore(8), ipchains-save(8) e iptables(8).
Cuando un paquete cumple cumple una determinada regla de una chain definimos qué hacer con él mediante lo que se denomina `objetivo' o target (quizás una traducción menos literal pero más clarificadora sería `acción'). Aunque existen más targets, son tres los que más se suelen utilizar: ACCEPT permite el paso de un paquete, DENY lo bloquea, y REJECT también lo bloquea pero a diferencia del anterior envía al origen una notificación mediante un mensaje ICMP de tipo DEST/SMALL>_UNREACH (siempre que el paquete bloqueado no sea también de tipo ICMP). Realmente, aunque REJECT y DENY nos parezcan igual de seguros - y de hecho en la mayoría de situaciones lo sean - siempre es más recomendable utilizar DENY, ya que mediante mensajes ICMP un posible atacante podría conseguir información sobre nuestro entorno que en ciertos casos puede comprometer nuestra seguridad, tal y como hemos comentado cuando hablábamos de Firewall-1.
Gestión
Vamos a ver un ejemplo de definición de una política de seguridad básica utilizando tanto iptables como ipchains; por cuestiones de simplicidad nos centraremos exclusivamente en el filtrado de paquetes, no en otros aspectos como la redirección de puertos o el NAT.
Lo primero que posiblemente nos interese antes de comenzar a definir reglas de filtrado sea `vaciar' las chains, es decir, eliminar todas las reglas asociadas a cada lista, de forma que no interfieran con las que vamos a comenzar a definir; para ello podemos utilizar la opción `-F' tanto de iptables como de ipchains (recordemos que en el primer caso, los nombres de las chains son los mismos, pero en mayúsculas). Además, podemos definir una política por defecto mediante la opción `-P' de ambas herramientas; esta política será la que se aplicará cuando un paquete no sea contemplado por ninguna de las reglas de una determinada chain:
luisa:~# /sbin/ipchains -P input DENY
luisa:~# /sbin/ipchains -F input
luisa:~# /sbin/ipchains -P output ACCEPT
luisa:~# /sbin/ipchains -F output
luisa:~# /sbin/ipchains -P forward DENY
luisa:~# /sbin/ipchains -F forward
luisa:~#
Como vemos, lo que vamos a hacer por defecto es denegar todo el tráfico que se dirija al cortafuegos, tanto si va dirigido a él como si se ha de reenviar a otro sistema, y permitir todo el tráfico de salida. Como vemos, estas políticas por defecto se pueden definir antes de `limpiar' cada chain, ya que la limpieza sólo afecta a las reglas en sí (y esta acción por defecto no se considera una regla).
Una vez aplicadas las primeras acciones, nos interesará sobre todo, ya que la salida la permitimos por completo (y de las redirecciones de tráfico ya hemos dicho que no vamos a entrar en detalle), definir accesos permitidos a nuestro sistema; por ejemplo, es posible que necesitemos un acceso total al puerto 80 para que todo el mundo pueda maravillarse de esas páginas web que hemos hecho con vi. Si es ese el caso, podemos permitir dicho acceso mediante una regla similar a la siguiente:
luisa:~# /sbin/ipchains -A input -p tcp -j ACCEPT -d 158.42.22.41 80
luisa:~#
Estamos indicando que se añada (`-A') en la chain `input' (tramas de entrada) una regla que permita (`ACCEPT') el tráfico TCP (`-p') cuyo destino (`-d') sea el puerto 80 de la dirección 158.42.22.41 - en principio, la IP de nuestro servidor web -. Con iptables, la sintaxis cambia ligeramente:
luisa:~# /sbin/iptables -A INPUT -p TCP -j ACCEPT -d 158.42.22.41 --dport 80
luisa:~#
Una vez definidas estas reglas, mediante la opción `-L' de ipchains e iptables podemos comprobar que efectivamente se están aplicando (utilizamos también `-n' para que no se haga resolución DNS):
luisa:~# /sbin/ipchains -L -n
Chain input (policy DENY):
target prot opt source destination ports
ACCEPT tcp
0.0.0.0/0 158.42.22.41 * -> 80
Chain forward (policy DENY):
Chain output (policy ACCEPT):
luisa:~#
Ahora pensemos que quizás también queremos acceder a nuestro servidor de forma remota, utilizando SSH, pero en este caso no desde cualquier lugar de Internet sino desde una dirección concreta; la regla a añadir a la chain `input' en este caso sería la siguiente:
luisa:~# /sbin/ipchains -A input -p tcp -j ACCEPT -s 158.42.2.1 -d \
> 158.42.22.41 22
luisa:~#
Podemos ver que ahora especificamos la dirección origen (`-s') desde la que vamos a permitir el tráfico (en este ejemplo, 158.42.2.1). Si utilizáramos iptables la sintaxis sería la siguiente:
luisa:~# /sbin/iptables -A INPUT -p TCP -j ACCEPT -s 158.42.2.1 -d \
> 158.42.22.41 --dport 22
luisa:~#
Tal y como hemos definido hasta ahora nuestra política de seguridad, sólo estamos permitiendo conexiones al puerto 80 desde cualquier máquina y al puerto 22 desde la especificada; el resto del tráfico de entrada está siendo denegado gracias a la política por defecto que hemos establecido para la chain input (DENY). Así, tráfico como los mensajes ICMP de vuelta, o las llamadas al servicio ident que realizan ciertos servidores cuando se les solicita una conexión no alcanzarán su destino, lo cual puede repercutir en la funcionalidad de nuestro entorno: simplemente hemos de pensar en una comprobación rutinaria de conectividad vía ping o en el acceso a un servidor FTP externo que sea denegado si no se consigue la identidad del usuario remoto. Para evitar estos problemas, podemos permitir el tráfico de ciertos paquetes ICMP y el acceso al servicio auth (puerto 113):
luisa:~# /sbin/ipchains -A input -p icmp --icmp-type \
> destination-unreachable -j ACCEPT
luisa:~# /sbin/ipchains -A input -p icmp --icmp-type source-quench -j ACCEPT
luisa:~# /sbin/ipchains -A input -p icmp --icmp-type time-exceeded -j ACCEPT
luisa:~# /sbin/ipchains -A input -p icmp --icmp-type parameter-problem \
> -j ACCEPT
luisa:~# /sbin/ipchains -A input -p icmp --icmp-type echo-reply -j ACCEPT
luisa:~# /sbin/ipchains -A input -p tcp -j ACCEPT -d 158.42.22.41 113
luisa:~#
Como vemos, hemos ido definiendo las reglas que conforman nuestra política desde línea de comando; ya hemos comentado que toda esta configuración se pierde al detener el sistema, por lo que es necesario crear un script que las vuelva a generar y planificarlo para que se ejecute en el arranque de la máquina. Para ello no tenemos que escribir línea a línea la configuración vista en este punto (mejor, la configuración adecuada a nuestro entorno), o utilizar ipchains-restore o iptables-restore para leer esa configuración de un fichero; si elegimos esta opción, antes de detener al sistema hemos de ejecutar ipchains-save o iptables-save para guardar dicha política en el fichero que posteriormente leeremos. De esta forma, en la parada de la máquina hemos de ejecutar una orden similar a:
luisa:~# /sbin/ipchains-save >/etc/rc.d/policy
Saving `input'.
luisa:~#
Mientras que en el arranque, en nuestro script cargaremos la política guardada al detener la máquina con una orden como:
luisa:~# /sbin/ipchains-restore </etc/rc.d/policy
luisa:~#
El sistema de log
Evidentemente, tanto ipchains como iptables están preparados para generar logs en el sistema: ambos permiten registrar mediante syslogd los paquetes que cumplan cierta regla - por norma general, todos -. Un registro exhaustivo de las acciones que se toman en el núcleo con respecto al filtrado de paquetes no es conveniente: la gran cantidad de información guardada hace imposible detectar actividades sospechosas, y además no es difícil que se produzcan ataques de negación de servicio, ya sea por disco ocupado o por tiempo consumido en generar y guardar registros. Por tanto, lo habitual es almacenar sólamente los paquetes que no sean rutinarios (por ejemplo, intentos de conexión desde direcciones no autorizadas, ciertos paquetes ICMP no habituales...).
En el caso de ipchains el núcleo de Linux, a través de klogd y de syslogd, registra estos eventos con prioridad `info', y al provenir del kernel (no olvidemos que el subsitema de filtrado forma parte del núcleo del operativo), su tipo es obviamente `kernel'. Para que cuando un paquete haga match con una regla se genere un registro hemos de utilizar la opción `-l'; por ejemplo, si deseamos que cada vez que alguien intente hacer un finger contra la máquina el tráfico, aparte de ser denegado, registre un log, ejecutaremos una orden como la siguiente:
luisa:~# /sbin/ipchains -A input -p tcp -j DENY -l -s 0.0.0.0/0 \
> -d 158.42.22.41 79
luisa:~#
Así, si estos registros se almacenan en el fichero /var/adm/fwdata, sus entradas serán de la siguiente forma:
rosita:~# tail -1 /var/adm/fwdata
Apr 3 02:03:13 rosita kernel: Packet log: input DENY eth0 PROTO=6 \
158.42.2.1:1032 158.42.22.41:79 L=34 S=0x00 I=18 F=0x0000 T=254
rosita:~#
El anterior mensaje nos dice principalmente que un paquete con protocolo 6 (corresponde a TCP en nuestro archivo /etc/protocols) proveniente de la dirección 158.42.2.1 y destinado a nuestro servicio finger ha sido denegado; el resto de la información no la veremos aquí (se puede consultar la documentación del producto para ver el significado concreto de todos y cada uno de los campos registrados).
En el caso de iptables el registro de eventos generado por el subsistema de filtrado es algo diferente al de ipchains, tanto al hablar de su funcionamiento como de su sintaxis. Ahora el log se realiza a través de un target independiente (LOG) que registra las tramas que hacen match con una regla determinada, y la prioridad de registro ya no es `info' sino que se puede indicar en la propia línea de órdenes (por defecto es `warning'). Además, se ha definido una nueva extensión denominada `limit' que permite restringir el número de registros que una regla puede generar por unidad de tiempo, lo cual es evidentemente útil para evitar que alguien ataque con éxito nuestros recursos mediante un flood del log en el subsistema de filtrado.
Volvamos de nuevo al ejemplo anterior, en el que registrábamos los intentos de acceso a nuestro puerto 79 (finger) para tener constancia de cuándo alguien trataba de obtener información de los usuarios de nuestro sistema; en el caso de iptables deberíamos definir una regla como la siguiente:
luisa:~# /sbin/iptables -A INPUT -p TCP -m limit -j LOG --log-prefix \
> "FINGER ATTEMPT:" -d 158.42.22.41 --dport 79
luisa:~#
Lo que indicamos mediante esta orden es que genere un mensaje cada vez que alguien envíe tráfico al puerto 79 de la dirección 158.42.22.41 (esto es, cada vez que alguien haga finger contra la máquina). Podemos observar que ahora definimos el target LOG (opción `-j'), y que además aplicamos la extensión `limit' (parámetro `-m') para limitar el registro y evitar así ciertas negaciones de servicio; cada vez que esta regla genere un log añadirá al principio del mismo la cadena `FINGER ATTEMPT:', lo que nos permite identificar de una forma más clara el mensaje. Podemos fijarnos en que a través de esta regla no estamos ni aceptando ni negando el tráfico, sino sólo registrando su existencia: para detener la trama, hemos de indicarlo explícitamente o bien a través de la acción por defecto que hayamos especificado para la chain INPUT (mediante la opción `-P').