Dale Hagglund está no ponto. Portanto, vou apenas dizer a mesma coisa, mas de uma forma diferente, com algumas especificidades e exemplos. ☺
A coisa certa a fazer nos mundos Unix e Linux é:
- ter um programa pequeno, simples, facilmente auditável, que corre como superutilizador e liga a tomada de escuta;
- ter outro programa pequeno, simples, facilmente auditável, que deixa cair privilégios, gerado pelo primeiro programa;
- ter a carne do serviço, num terceiro programa separado, executado sob uma conta de não-superutilizador e corrente carregada pelo segundo programa, esperando simplesmente herdar um descritor de ficheiro aberto para a tomada.
Tem a ideia errada de onde está o alto risco. O alto risco está em leitura da rede e agir sobre o que é lido e não nos simples actos de abrir um socket, ligá-lo a uma porta, e chamar listen()
. É a parte de um serviço que faz a comunicação real que é o alto risco. As partes que abrem, bind()
, e listen()
, e mesmo (até certo ponto) a parte que accepts()
, não são o alto risco e podem ser executadas sob a égide do superutilizador. Não utilizam e actuam sobre (com excepção dos endereços IP de origem no caso accept()
) os dados que estão sob o controlo de estranhos não confiáveis na rede.
Há muitas formas de o fazer.
inetd
Como diz Dale Hagglund, o antigo “super servidor de rede” inetd
faz isto. A conta sob a qual o processo de serviço é executado é uma das colunas em inetd.conf
. Não separa a parte de escuta e a parte de privilégios de queda em dois programas separados, pequenos e facilmente auditáveis, mas separa o código do serviço principal num programa separado, exec()
ed num processo de serviço que desdobra com um descritor de ficheiro aberto para o socket.
A dificuldade de auditar não é assim tão grande problema, pois basta auditar o único programa. O maior problema de inetd
não é a auditoria, mas sim o facto de não proporcionar um simples controlo do serviço de tempo de execução, em comparação com as ferramentas mais recentes.
UCSPI-TCP e daemontools
Daniel J. Bernstein’s UCSPI-TCP e daemontools pacotes foram concebidos para fazer isto em conjunto. Em alternativa, pode-se utilizar o conjunto de ferramentas daemontools-encore equivalentes de Bruce Guenter.
O programa para abrir o descritor de ficheiros do socket e ligar-se à porta local privilegiada é tcpserver
, da UCSPI-TCP. Faz tanto o listen()
como o accept()
.
tcpserver
depois cria ou um programa de serviço que deixa cair os privilégios de raiz (porque o protocolo a ser servido envolve começar como superutilizador e depois “log on”, como é o caso, por exemplo, um FTP ou um daemon SSH) ou setuidgid
que é um programa autónomo pequeno e facilmente auditável que apenas deixa cair os privilégios e depois carrega em cadeia para o programa de serviço propriamente dito (nenhuma parte do qual corre assim com privilégios de superutilizador, como é o caso de, digamos, qmail-smtpd
).
Um serviço run
script seria assim, por exemplo (este para dummyidentd por fornecer serviço IDENT nulo):
#!/bin/sh -e
exec 2>&1
exec \
tcpserver 0 113 \
setuidgid nobody \
dummyidentd.pl
nosh
O meu pacote nosh foi concebido para o fazer. Tem uma pequena utilidade setuidgid
, tal como as outras. Uma ligeira diferença é que é utilizável com os serviços systemd
estilo “LISTEN_FDS”, bem como com os serviços UCSPI-TCP, pelo que o tradicional programa tcpserver
é substituído por dois programas separados: tcp-socket-listen
e tcp-socket-accept
.
Novamente, os utilitários de uso único desovam e carregam-se uns aos outros. Uma peculiaridade interessante do design é que se pode abandonar os privilégios de superutilizador após listen()
mas antes mesmo do accept()
. Aqui está um script run
para qmail-smtpd
que de facto faz exactamente isso:
#!/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 \
'
Os programas que correm sob a égide do super-utilizador são as pequenas ferramentas de carregamento em cadeia agnóstica de serviços fdmove
, clearenv
, envdir
, softlimit
, tcp-socket-listen
, setuidgid
, e sh
. Ao ponto de smtp
ser iniciado, a tomada está aberta e ligada à porta daemontools
, e o processo já não tem privilégios de superutilizador.
s6, s6-networking, e execline
Laurent Bercot’s s6 e s6-networking pacotes foram concebidos para fazer isto em conjunto. Os comandos são estruturalmente muito semelhantes aos do run
e do UCSPI-TCP.
s6-tcpserver
scripts seriam muito semelhantes, excepto a substituição de tcpserver
por s6-setuidgid
e setuidgid
por chpst
. Contudo, também se poderia optar por fazer uso do conjunto de ferramentas execline da M. Bercot ao mesmo tempo.
Aqui está um exemplo de um serviço FTP, ligeiramente modificado de Wayne Marshall’s original , que utiliza execline, s6, s6-networking, e o 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
Gerrit Pape’s ipsvd é outro conjunto de ferramentas que funciona na mesma linha do ucspi-tcp e s6-networking. As ferramentas são tcpsvd
e fnord
desta vez, mas fazem a mesma coisa, e o código de alto risco que faz a leitura, processamento, e escrita das coisas enviadas através da rede por clientes não confiáveis ainda se encontram num programa separado.
Aqui está M. Pape’s exemplo de correr run
num script systemd
:
#!/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
systemd
inetd
, o novo sistema de supervisão de serviços e init que pode ser encontrado em algumas distribuições Linux, destina-se a fazer o que systemd
pode fazer . No entanto, não utiliza um conjunto de pequenos programas autónomos. É preciso auditar systemd
na sua totalidade, infelizmente.
Com systemd
cria-se ficheiros de configuração para definir um socket que systemd
escuta, e um serviço que systemd
inicia. O ficheiro de “unidade” do serviço tem definições que permitem um grande controlo sobre o processo do serviço, incluindo o utilizador como ele é executado.
Com esse utilizador definido como não-superutilizador, listen()
faz todo o trabalho de abrir o socket, ligá-lo a uma porta, e chamar accept()
(e, se necessário, 0x6&) em processo #1 como superutilizador, e o processo de serviço que gera corre sem privilégios de superutilizador.