En sistemas Unix y derivados (Linux, *BSD, etc.) la implementación de TCP/IP considera especiales a los puertos debajo del 1024 (llamados "puertos privilegiados"), en el sentido de que a los usuarios no privilegiados no se les permite iniciar servicios o escuchar peticiones en los mismos. Los puertos TCP y UDP entre 1 y 1023 están reservados para procesos corriendo con privilegios de superusuario (es decir, como root). A aquellos procesos corriendo con un ID de usuario distinto de 0 no se les permite escuchar peticiones en dichos puertos (bind).
Esta característica de seguridad es una especie de protección para los clientes en el sentido de que, si se conectan a un servicio escuchando en un puerto bajo, pueden estar seguros de que se trata de un servicio confiable (y no uno falso levantado por un impostor) ya que para ello debe haber sido autorizado por el administrador del servidor. Esta protección es importante, ya que en la mayoría de los servicios los clientes envían credenciales de autenticación. Si un usuario no privilegiado es capaz de recibir conexiones entrantes en puertos privilegiados, podría llegar a impersonar servicios y robar credenciales.
Sin embargo, la implicancia que tiene esta protección (desde el lado servidor), es que todos los servicios deben correr como superusuario (root). Ante un bug o vulnerabilidad en cualquiera de los servicios, resulta comprometido todo el servidor, ya que se logra automáticamente el acceso como root.
Lo que la mayoría de los servicios implementan para minimizar la superficie de ataque (por ejemplo Apache), consiste en iniciar primero un proceso mínimo corriendo como root (el cual abre el puerto bajo necesario para trabajar) para luego iniciar hijos corriendo con un usuario no privilegiado, los cuales se encargan de procesar las peticiones. De esta forma, ante una vulnerabilidad en el procesamiento, resulta comprometido un proceso corriendo como usuario no privilegiado.
root@www:~# ps -C apache2 -o user,pid,time,stat,start,comm USER PID TIME STAT STARTED COMMAND www-data 7681 00:00:00 S 10:32:04 apache2 www-data 7808 00:00:00 S 10:36:53 apache2 www-data 7812 00:00:00 S 10:36:54 apache2 www-data 7819 00:00:00 S 10:37:01 apache2 www-data 7832 00:00:00 S 10:37:32 apache2 www-data 7833 00:00:00 S 10:37:33 apache2 www-data 7846 00:00:00 S 10:38:13 apache2 www-data 7849 00:00:00 S 10:38:35 apache2 www-data 7851 00:00:00 S 10:38:39 apache2 www-data 7852 00:00:00 S 10:38:39 apache2 www-data 7853 00:00:00 S 10:38:40 apache2 www-data 7854 00:00:00 S 10:38:40 apache2 www-data 7857 00:00:00 S 10:38:59 apache2 www-data 7871 00:00:00 S 10:39:01 apache2 www-data 7872 00:00:00 S 10:39:01 apache2 www-data 7873 00:00:00 S 10:39:01 apache2 www-data 7874 00:00:00 S 10:39:01 apache2 www-data 7877 00:00:00 S 10:39:02 apache2 www-data 7879 00:00:00 S 10:39:02 apache2 www-data 7881 00:00:00 S 10:39:02 apache2 www-data 7883 00:00:00 S 10:39:02 apache2 www-data 7885 00:00:00 S 10:39:07 apache2 root 19377 00:07:40 Ss May 28 apache2
El proceso principal (el cual escucha en los puertos 80 y 443) corre como root, y sus hijos (los cuales se encargan de procesar las solicitudes) corren como el usuario no privilegiado "www-data" (o "apache" en Red Hat y derivados). Notar además, en las fechas de inicio (STARTED), que los procesos hijos pueden ser iniciados a demanda, e incluso "reciclados" cada determinado período de tiempo.
Pero, ¿qué pasa si tenemos un servicio que requiere escuchar en un puerto bajo y no implementa esta separación de privilegios? Por ejemplo, alguna bazofia desarrollada en Java (mi experiencia dicta que los desarrolladores Java son los que menos idea tienen o menos se preocupan por la seguridad, a tono con dicha tecnología, por cierto). Lógicamente no queremos que dicho servicio tenga privilegios de root en nuestro servidor (si no es capaz de implementar una separación adecuada de privilegios, poco podemos esperar de su seguridad, confiabilidad y robustez en general). Ante estos casos afortunadamente existen diferentes técnicas que permiten implementar una correcta separación de privilegios, por fuera del servicio. En este artículo se presenta la herramienta authbind
. Otra técnica conocida consiste en levantar el servicio en un puerto alto e implementar una redirección de tráfico con iptables
(siempre que el servicio permita configurar su rango de puertos de trabajo).
authbind
permite que un programa que no corre, o no debe correr, como root pueda abrir puertos bajos de manera controlada. Para ello se debe invocar al programa utilizando authbind
, el cual se encarga de configurar algunas variables de entorno, incluyendo LD_PRELOAD
, quien permite que el programa (y cualquier programa o subproceso que pueda lanzar) sea capaz de abrir y escuchar peticiones en un puerto bajo (<1024).
¿Cómo funciona authbind
? La librería compartida LD_PRELOAD
sobrescribe la llamada al sistema (system call) bind
. Cuando un programa invocado vía authbind
invoca a bind
para abrir un socket TCP/IP asociado a un puerto bajo, y dicho programa no tiene el ID de usuario efectivo 0, la versión de bind
que incorpora authbind
hace un fork y ejecuta un subprograma con setuid de root. En cambio, para sockets no TCP/IP, puertos altos, o programas que ya poseen privilegios de root (uid efectivo igual a 0), authbind
pasa la llamada a la syscall bind
original.
Manos a la obra
Para instalar authbind
en Debian y derivados, ejecutar:
apt-get install authbind
El acceso a puertos bajos es controlado examinando los permisos y el contenido de los archivos en el esquema de configuración bajo el directorio /etc/authbind
:
root@debian:~# ls -l /etc/authbind/ total 20 drwxr-xr-x 2 root root 4096 jun 10 2012 byaddr drwxr-xr-x 2 root root 4096 jun 15 11:53 byport drwxr-xr-x 2 root root 4096 jun 10 2012 byuid
Primero se verifica el archivo byport/[número de puerto]
(control de acceso por puerto), por ejemplo byport/80
. Si el usuario que invoca tiene permiso de ejecución (a nivel sistema de archivos) sobre este archivo, el acceso es autorizado. Si el usuario no tiene permiso de ejecución sobre el archivo, o el archivo no existe, el acceso es denegado.
Luego se verifica el archivo byaddr/[dirección IP]:[puerto]
(control de acceso por dirección IP), por ejemplo byport/192.168.1.10:80
. El test es idéntico, salvo que la dirección IP sobre la cual intenta abrir el puerto debe coincidir.
Finalmente se revisa el archivo byuuid/[ID de usuario]
(control de acceso por ID de usuario). Este archivo debe existir y debe contener una línea que habilite el acceso, la cual utiliza un formato específico (ver más detalle en la página de manual).
En este ejemplo se desea permitir que un determinado servicio sea capaz de abrir el puerto 80 sobre cualquier dirección IPv4 configurada. Para ello, crear el archivo 80
:
touch /etc/authbind/byport/80
Se observa que root es el dueño del archivo y nadie tiene permiso de ejecución, por ende inicialmente nadie (excepto root, pues utiliza la llamada a bind
original) puede abrir dicho puerto:
root@debian:~# ls -l /etc/authbind/byport/ total 8 -rw-r--r-- 1 root root 0 jun 15 11:53 80
El objetivo es que el servicio en cuestión pueda correr como usuario no privilegiado y a su vez abrir un puerto bajo. Entonces, si aún no posee un usuario y grupo es necesario crearlos (a modo de ejemplo "srv"):
groupadd srv useradd -m -s /usr/sbin/nologin -g srv -c "Servicio" srv
A continuación, si el directorio base del servicio es /opt/srv
, cambiar el dueño y grupo y quitar permisos para el resto del mundo:
chown -R srv:srv /opt/srv chmod -R o-wrx /opt/srv
Volviendo a la configuración de authbind
, se debe permitir abrir el puerto 80 al nuevo usuario "srv". Para ello, cambiar el dueño del archivo /etc/authbind/byport/80
a "srv" y otorgar sólo permiso de ejecución sobre el mismo:
cd /etc/authbind/byport/ chown srv:root 80 chmod 500 80
Ahora sólo los usuarios "srv" y root pueden abrir el puerto 80:
root@debian:/etc/authbind/byport# ls -l total 8 -r-x------ 1 srv root 0 jun 15 11:53 80
El último paso consiste en modificar el script de inicio del servicio, de forma que sea iniciado invocando a authbind
para autorizar la apertura del puerto 80 con el usuario sin privilegios:
nano /etc/init.d/srv
Si el inicio de servicio se realizaba de la siguiente forma (como root):
/opt/srv/start.sh > /opt/srv/log/srv.log 2>&1 &
Ahora se realizará de la siguiente forma:
$SRV_USER="srv" $SRV_HOME="/opt/srv" $SRV_START="authbind --deep $SRV_HOME/start.sh > $SRV_HOME/log/srv.log 2>&1 &" su --login $SRV_USER --command "${SRV_START}"
Mediante su
se inicia el servicio como el usuario "srv", y utilizando authbind
se permite que el usuario "srv" pueda abrir el puerto 80. La opción --deep
permite que authbind
autorice no sólo al proceso inicial, sino además a cualquier proceso hijo.
A partir de este momento es posible iniciar el servicio:
service srv start
Se verifica que efectivamente el servicio corre como el usuario "srv":
root@debian:~# ps -C srv -o pid,user,comm PID USER COMMAND 32291 srv srv
Y ha sido capaz de abrir el puerto 80 con éxito:
root@debian:~# netstat -nlp | grep LISTEN | grep 80 tcp 0 0 192.168.1.10:80 0.0.0.0:* LISTEN 32291/srv
Gracias a esta configuración se logra mejorar notablemente la seguridad de un servidor, al quitar privilegios de root a todos los servicios en ejecución (especialmente a aquellos poco confiables/seguros), sin afectar su correcto funcionamiento.
Para mayor información, recurrir a las siguientes páginas de manual:
man touch man groupadd man useradd man chown man chmod man authbind man su man service man ps man netstat