Dale Hagglund da en el clavo. Así que voy a decir lo mismo pero de una manera diferente, con algunos detalles y ejemplos. ☺
Lo correcto en el mundo Unix y Linux es:
- tener un programa pequeño, sencillo, fácilmente auditable, que se ejecute como superusuario y enlace el socket de escucha;
- tener otro programa pequeño, sencillo, fácilmente auditable, que baje privilegios, engendrado por el primer programa;
- tener la carne del servicio, en un tercer programa separado, ejecutado bajo una cuenta de no superusuario y cargado en cadena por el segundo programa, esperando simplemente heredar un descriptor de archivo abierto para el socket.
Tienes una idea equivocada de dónde está el alto riesgo. El alto riesgo está en leer de la red y actuar sobre lo leído no en los simples actos de abrir un socket, vincularlo a un puerto, y llamar a listen()
. Es la parte de un servicio que hace la comunicación real que es el alto riesgo. Las partes que abren, bind()
, y listen()
, e incluso (hasta cierto punto) la parte que accepts()
, no son el alto riesgo y pueden ejecutarse bajo la égida del superusuario. No utilizan ni actúan sobre (con la excepción de las direcciones IP de origen en el caso de accept()
) datos que están bajo el control de extraños no confiables a través de la red.
Hay muchas maneras de hacer esto.
inetd
Como dice Dale Hagglund, el viejo “superservidor de red” inetd
hace esto. La cuenta bajo la cual se ejecuta el proceso de servicio es una de las columnas en inetd.conf
. No separa la parte de la escucha y la parte de la caída de privilegios en dos programas separados, pequeños y fácilmente auditables, pero sí separa el código principal del servicio en un programa separado, exec()
ed en un proceso de servicio que genera con un descriptor de archivo abierto para el socket.
La dificultad de la auditoría no es un gran problema, ya que uno sólo tiene que auditar el programa. El mayor problema de inetd
no es tanto la auditoría, sino que no proporciona un control de servicios en tiempo de ejecución sencillo y detallado, en comparación con otras herramientas más recientes.
UCSPI-TCP y daemontools
Los paquetes UCSPI-TCP y daemontools de Daniel J. Bernstein fueron diseñados para hacer esto en conjunto. También se puede utilizar el conjunto de herramientas daemontools-encore de Bruce Guenter, en gran medida equivalente.
El programa para abrir el descriptor de archivo del socket y enlazar con el puerto local privilegiado es tcpserver
, de UCSPI-TCP. Hace tanto el listen()
como el accept()
.
tcpserver
entonces genera un programa de servicio que deja caer los privilegios de root por sí mismo (porque el protocolo que se está sirviendo implica comenzar como el superusuario y luego “iniciar sesión”, como es el caso de, por ejemplo, un FTP o un demonio SSH) o setuidgid
que es un pequeño programa autocontenido y fácilmente auditable que únicamente pierde privilegios y luego carga en cadena al programa de servicio propiamente dicho (ninguna parte del cual se ejecuta con privilegios de superusuario, como es el caso de, por ejemplo, qmail-smtpd
).
Un script de servicio run
sería por tanto, por ejemplo (este para dummyidentd para proporcionar un servicio de IDENT nulo):
#!/bin/sh -e
exec 2>&1
exec \
tcpserver 0 113 \
setuidgid nobody \
dummyidentd.pl
nosh
Mi paquete nosh está diseñado para hacer esto. Tiene una pequeña utilidad setuidgid
, igual que las otras. Una ligera diferencia es que es utilizable con servicios “LISTEN_FDS” del estilo de systemd
así como con servicios UCSPI-TCP, por lo que el programa tradicional tcpserver
es reemplazado por dos programas separados: tcp-socket-listen
y tcp-socket-accept
.
De nuevo, las utilidades de propósito único generan y cargan en cadena unas con otras. Una peculiaridad interesante del diseño es que uno puede dejar los privilegios de superusuario después de listen()
pero antes incluso de accept()
. Aquí hay un script de run
para qmail-smtpd
que de hecho hace exactamente eso:
#!/bin/nosh
fdmove -c 2 1
clearenv --keep-path --keep-locale
envdir env/
softlimit -m 70000000
tcp-socket-listen --combine4and6 --backlog 2 ::0 smtp
setuidgid qmaild
sh -c 'exec \
tcp-socket-accept -v -l "${LOCAL:-0}" -c "${MAXSMTPD:-1}" \
ucspi-socket-rules-check \
qmail-smtpd \
'
Los programas que se ejecutan bajo la égida del superusuario son las pequeñas herramientas de carga de cadenas agnósticas fdmove
, clearenv
, envdir
, softlimit
y tcp-socket-listen
. En el momento en que se inicia setuidgid
, el socket está abierto y vinculado al puerto sh
, y el proceso ya no tiene privilegios de superusuario.
s6, s6-networking, y execline
Los paquetes s6 y s6-networking de Laurent Bercot fueron diseñados para hacer esto en conjunto. Los comandos son estructuralmente muy similares a los de smtp
y UCSPI-TCP. Los scripts
daemontools
serían muy parecidos, excepto por la sustitución de run
por s6-tcpserver
y tcpserver
por s6-setuidgid
. Sin embargo, también se podría optar por utilizar el conjunto de herramientas execline de M. Bercot al mismo tiempo.
Aquí hay un ejemplo de un servicio FTP, ligeramente modificado del original de Wayne Marshall , que utiliza execline, s6, s6-networking, y el programa servidor FTP de publicfile :
#!/command/execlineb -PW
multisubstitute {
define CONLIMIT 41
define FTP_ARCHIVE "/var/public/ftp"
}
fdmove -c 2 1
s6-envuidgid pubftp
s6-softlimit -o25 -d250000
s6-tcpserver -vDRH -l0 -b50 -c ${CONLIMIT} -B '220 Features: a p .' 0 21
ftpd ${FTP_ARCHIVE}
ipsvd
El ipsvd de Gerrit Pape es otro conjunto de herramientas que se ejecuta en la misma línea que ucspi-tcp y s6-networking. Las herramientas son setuidgid
y chpst
esta vez, pero hacen lo mismo, y el código de alto riesgo que hace la lectura, el procesamiento y la escritura de cosas enviadas por la red por clientes no confiables sigue estando en un programa separado.
Aquí está el ejemplo de M. Pape de ejecutar tcpsvd
en un script fnord
:
#!/bin/sh
exec 2>&1
cd /public/10.0.5.4
exec \
chpst -m300000 -Uwwwuser \
tcpsvd -v 10.0.5.4 443 sslio -v -unobody -//etc/fnord/jail -C./cert.pem \
fnord
run
systemd
, el nuevo sistema de supervisión de servicios e init que puede encontrarse en algunas distribuciones de Linux, pretende hacer lo que systemd
puede hacer . Sin embargo, no utiliza un conjunto de pequeños programas autocontenidos. Uno tiene que auditar inetd
en su totalidad, desafortunadamente.
Con systemd
uno crea archivos de configuración para definir un socket en el que systemd
escucha, y un servicio que systemd
inicia. El archivo de la “unidad” de servicio tiene ajustes que permiten un gran control sobre el proceso de servicio, incluyendo el usuario con el que se ejecuta.
Con ese usuario configurado para ser un no-superusuario, systemd
hace todo el trabajo de abrir el socket, vincularlo a un puerto, y llamar a systemd
(y, si es necesario, a listen()
) en el proceso #1 como superusuario, y el proceso de servicio que genera se ejecuta sin privilegios de superusuario.